Skip to content

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:

{"status":"ok"}

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):

{
  "authenticated": false,
  "user": null,
  "admin": false,
  "server_time": "2026-07-15T10:00:00Z"
}

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:

{"items":[],"total":0,"note":"..."}

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.