API¶
Base URL examples assume Central listens on http://127.0.0.1:8888 with empty BASE_PATH. Behind Apache, the same paths are usually exposed at the site root. If BASE_PATH is set, prefix every path with it.
Conventions¶
| Topic | Behavior |
|---|---|
| Content type | JSON responses use application/json; charset=utf-8 |
| Errors | Mostly {"error":"<message>"} |
| Auth cookie | acugis_session |
| Auth bearer | Authorization: Bearer agp_... |
| Admin requirement | Authenticated user with IsAdmin |
| Body size | JSON bodies capped at 1 MiB; unknown fields rejected on admin/card/PAT JSON |
| Dual admin prefixes | Every identity/ACL route below exists under both /api/admin/... and /admin/api/... unless noted |
| Health | Liveness only; no dependency checks |
Common status codes¶
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | Success with empty body |
| 303 | Redirect (login/logout/HTML auth) |
| 400 | Validation / malformed payload / invalid id |
| 401 | Authentication required (JSON) or invalid login (HTML) |
| 403 | Admin access required |
| 404 | Missing resource |
| 502 | Downstream discovery failure (admin discovery) |
| 503 | Database/auth not configured or PAT store unavailable |
| 500 | Unexpected server/persistence error |
System¶
GET /api/health¶
Purpose: Liveness probe.
Auth: None.
Response 200:
Notes: Does not check PostgreSQL, disks, or downstream services.
Identity¶
GET /api/me¶
Purpose: Report current authentication state.
Auth: Optional cookie or bearer.
Response 200 (anonymous):
Response 200 (authenticated):
{
"authenticated": true,
"user": {
"username": "admin",
"email": "admin@example.test",
"groups": ["Administrators"]
},
"admin": true,
"server_time": "2026-07-15T10:00:00Z"
}
Errors: 500 if credential lookup fails. Missing database returns anonymous 200.
GET /api/me/tokens¶
Purpose: List caller’s non-revoked PATs.
Auth: Required.
Response 200: {"tokens":[PersonalAccessToken...]}
Errors: 401, 503 (unsupported), 500.
POST /api/me/tokens¶
Purpose: Create a PAT.
Auth: Required.
Body:
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | |
expires_at |
string/null | no | RFC3339 or YYYY-MM-DD (end of that UTC day) |
Response 201:
{
"token": {"id":1,"user_id":1,"name":"ci","token_prefix":"agp_xxxxxxxxxxxx","scopes":[],"created_at":"...","active":true},
"secret": "agp_...",
"message": "Copy it now. It cannot be viewed again."
}
Errors: 400, 401, 503, 500.
DELETE /api/me/tokens/{id}¶
Purpose: Revoke caller-owned PAT.
Auth: Required.
Response: 204.
Errors: 400, 401, 404, 503, 500.
Session pages (non-JSON)¶
| Method | URL | Auth | Purpose | Success |
|---|---|---|---|---|
GET |
/login |
optional | Login form; redirects if already signed in | 200 HTML or 303 |
POST |
/login |
none | Form login (username,password,next) |
303 + cookie |
GET/POST |
/logout |
optional | End session | 303 to /login |
Login errors: 400 bad form, 401 invalid credentials (HTML), 503 auth DB missing, 500 login failure.
Catalog and stubs¶
GET /api/catalog¶
Purpose: Aggregate discovery across registered services.
Auth: Optional; cookie forwarded downstream.
Response 200:
{
"datasets": [],
"maps": [],
"dashboards": [],
"reports": [],
"warnings": ["geovault unavailable"]
}
CatalogItem fields: id, type, title, description?, url, service, category?, tags?, updated_at?, owner?.
Errors: 503 only if aggregator is nil (normal startup always creates it). Downstream failures become warnings, not non-2xx.
Stub collections¶
All return 200 with empty collections today:
| Method | URL | Note |
|---|---|---|
GET |
/api/maps |
not yet wired to qgis-publish-service |
GET |
/api/reports |
not yet wired to Jasper reports |
GET |
/api/forms |
not yet wired to GeoSync forms |
GET |
/api/datasets |
not yet wired to GeoVault datasets |
Shape:
Portal cards¶
GET /api/portal/cards¶
Purpose: List cards visible to the current viewer.
Auth: Optional.
Response 200: {items,total,categories[,note]}
If DB unset: empty items with note portal card database is not configured.
Errors: 500.
Card object fields: id, title, description, card_type, url, icon, image_url, category, sort_order, is_public, source_service, source_resource_id, is_imported, created_at.
Admin card APIs¶
All require admin + configured portal DB unless noted.
GET /api/admin/portal/services¶
Response 200: {items: Service[], total} where Service has service_key, display_name, discovery_url.
GET /api/admin/portal/services/{service_key}/resources¶
Purpose: Live discovery for one registered service.
Response 200: DiscoveryResult (service, resource_type?, items, total, fetched_at).
Errors: 404 unknown service, 502 downstream/JSON/transport error, plus admin auth errors.
GET /api/admin/portal/cards¶
Response 200: all cards {items,total,categories}.
Errors: 503 DB missing, 500.
POST /api/admin/portal/cards¶
Body (CardInput):
| Field | Required | Notes |
|---|---|---|
title |
yes | |
card_type |
yes | map\|dashboard\|report\|dataset\|external\|admin |
url |
yes | http(s) absolute or root-relative |
description |
no | |
icon |
no | default inferred from type |
image_url |
no | same URL rules when set |
category |
no | |
sort_order |
no | |
is_public |
no | |
source_service |
no | |
source_resource_id |
no | |
is_imported |
no |
Response: 201 Card. Errors: 400, 503, 500.
PUT /api/admin/portal/cards/{id}¶
Full replacement-style update with same body/validation as create.
Response: 200 Card. Errors: 400, 404, 503, 500.
DELETE /api/admin/portal/cards/{id}¶
Response: 204. Errors: 400, 404, 503, 500.
POST /api/admin/portal/cards/reorder¶
Body: [{"id":1,"sort_order":10}, ...]
Empty array succeeds. Transactional.
Response: 200 {"status":"ok"}. Errors: 400, 503.
POST /api/admin/portal/thumbnails¶
Auth: Admin (DB for cards not required).
Body: multipart/form-data field file.
Limits: 5 MiB; JPEG/PNG/WebP/GIF.
Response 201: {"url":"/thumbnails/<name>"} (with base path when applicable).
Errors: 400 for upload/validation/write failures surfaced by handler, plus auth errors.
Users (admin)¶
Aliases exist under /admin/api/users....
GET /api/admin/users¶
Query: search (optional; case-insensitive username/email substring).
Response 200: {items: AuthUser[], total}
AuthUser: id, username, email, enabled, is_admin, groups, group_ids, created_at.
POST /api/admin/users¶
Body: username (req), email, password (req), enabled?, group_ids?.
Response: 201 AuthUser. Errors: 400, 500 (incl. unique violations).
GET /api/admin/users/{id}¶
Response: 200 AuthUser. Errors: 400, 404.
PUT /api/admin/users/{id}¶
Applied fields: email, enabled, group_ids.
username/password in body are ignored if present.
Response: 200 AuthUser.
POST /api/admin/users/{id}/password¶
Body: {"password":"..."} (required non-blank).
Response: 200 {"status":"ok"}.
Groups (admin)¶
Aliases under /admin/api/groups....
| Method | URL | Body | Success |
|---|---|---|---|
GET |
/api/admin/groups |
— | 200 {items,total} Group |
POST |
/api/admin/groups |
name, description? |
201 Group |
PUT |
/api/admin/groups/{id} |
name, description? |
200 Group |
DELETE |
/api/admin/groups/{id} |
— | 204 |
POST |
/api/admin/groups/{id}/users |
{"user_id":n} |
200 {"status":"ok"} |
DELETE |
/api/admin/groups/{id}/users/{user_id} |
— | 204 |
Group fields: id, name, description, member_count, created_at.
Protected: cannot rename/delete Administrators or Public (400). Removing a nonexistent membership still returns 204.
Resources and ACLs (admin)¶
Aliases under /admin/api/....
GET /api/admin/resources¶
Query: service, resource_type (exact).
Response: {items: Resource[], total}.
GET /api/admin/resource-access-overview¶
Query: search, service, resource_type, visibility (public|restricted; other values ignored).
Response items include: resource_id, resource_type, resource_key, display_name, service, owner_id, owner_name, visibility, group_names_with_access.
GET /api/admin/resources/{id}/permissions¶
Response:
{
"resource": {"id":1,"resource_type":"service","resource_key":"geovault","display_name":"GeoVault","service":"platform","created_at":"..."},
"visibility": "restricted",
"matrix": [{"group_id":1,"group_name":"Administrators","view":true,"edit":true,"admin":true}]
}
PUT /api/admin/resources/{id}/permissions¶
Body:
{
"visibility": "public",
"matrix": [
{"group_id": 2, "group_name": "Editors", "view": true, "edit": true, "admin": false}
]
}
Replaces all ACL rows transactionally. Public visibility forces Public-group view. Restricted strips Public grants.
Response: 200 {"status":"ok"}.
Legacy permission rows¶
| Method | URL | Body | Success |
|---|---|---|---|
GET |
/api/admin/permissions |
— | 200 {items,total} |
POST |
/api/admin/permissions |
resource_id, group_id, permission (view\|edit\|admin) |
201 Permission |
DELETE |
/api/admin/permissions/{id} |
— | 204 |
HTML pages (selected)¶
These are not JSON APIs but are part of the HTTP surface:
| Path | Auth | Notes |
|---|---|---|
/, /services, /catalog, /administration |
none for page shell | Data loaded via APIs |
/account |
authenticated | PAT management UI |
/admin/portal |
admin | Card admin UI |
/admin/resource-sync |
admin | Links to downstream sync endpoints |
/admin/users, /admin/groups, /admin/resources, /admin/permissions (+ nested) |
admin | Auth admin SPA shell |
/thumbnails/*, other static under web/ |
none | Served as files |
/shared/ui/ |
none | Shared federation UI assets |
OpenAPI¶
Machine-readable contract: openapi.yaml.