Skip to content
Talk to our solutions team

IAM API

Every route the iam.svc binary mounts, with its exact path, payloads and gate. Concepts live in the IAM block pages: authentication flows, tokens and keys, authorization, the identity schema and operating the service. The full error catalogue is on the IAM error page.

Routes are mounted at the root. There is no version prefix — the path in this reference is the path on the wire.

Every route except GET /ready and GET /health resolves a tenant from four headers. Examples below abbreviate them as $H.

HeaderExample
X-Customeracme
X-Productshop
X-Envprod
X-Tenanteu1
Terminal window
H='-H "X-Customer: acme" -H "X-Product: shop" -H "X-Env: prod" -H "X-Tenant: eu1"'

Headers that resolve to no configured tenant get 400 with the plain-text body invalid tenant — not a JSON envelope.

ChannelWhere it is read
Authorization: Bearer <token>Every authenticated route. PASETO v4.public; a legacy EdDSA JWT still verifies
Authorization: ApiKey <id>.<secret>POST /auth/token (required), plus GET /rest/:entity and PATCH /rest
Cookie: jwt=<token>/rest, /anon/rest, /admin/*, /schema/*, /cache, /superadmin/tenant/* — the shared library extractor accepts the token from this cookie as well as the header. The /auth/* routes read the header only
Cookie: kis_session=<opaque>Set by login on a managed tenant; exchanged for a token by the gateway, never verified as a credential by these routes

A token whose tenant claim disagrees with the CPET headers is rejected with 401 tenant_mismatch. There is no superadmin exception: a control-plane token presented under a tenant’s headers is rejected like any other mismatch.

Three shapes ship. Key on the pair (status, code) — several codes carry different statuses on different routes.

SurfaceBody
/auth/*, /superadmin/registry/*, /superadmin/impersonate, /internal/session/*{"error": "…", "code": "<slug>"}
/rest, /anon/rest, /admin/user, /admin/role, /schema, /cache, /superadmin/tenant/*{"error": "…", "code": "v2.<slug>", "details": {…}}
/admin/data/*{"error": "…"} — no code field at all

Set on every response, including pre-auth and pre-tenant failures:

X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Cache-Control: no-store
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'
Strict-Transport-Security: max-age=63072000; includeSubDomains

Cache-Control: no-store applies to GET /.well-known/jwks.json too, so a relying party must cache the key set in its own process rather than relying on HTTP caching.

1 MiB on every request; 8 KiB on POST /superadmin/impersonate. An over-limit body surfaces as a 400 JSON decode error, not a 413.

Six profiles are declared. The bucket key is (profile, tenant, client IP); requests with no resolved tenant are not bucketed. Client IP is taken from X-Forwarded-For (first entry), then X-Real-IP, then the socket.

ProfileRoutesCapacityRefill
auth_loginPOST /auth/login300.5/s
auth_mfaPOST /auth/mfa/verify300.5/s
auth_resetPOST /auth/password/reset/request, /confirm150.25/s
auth_magicPOST /auth/magic/request, /verify150.25/s
auth_tokenPOST /auth/token601/s
auth_apikeyPOST /auth/apikey150.25/s

A denial returns 429 with Retry-After, X-RateLimit-Limit and X-RateLimit-Remaining:

{ "error": "rate_limited", "profile": "auth_login", "retry_after_seconds": 2 }

Every other route is unlimited. POST /superadmin/impersonate is wrapped with a profile name that is not declared, and an undeclared profile passes through untouched — impersonation is not rate-limited.


Method + pathAuthPurpose
POST /auth/loginnonePassword login; also the operator login
POST /auth/refreshnone (refresh token is the credential)Rotate the token pair
POST /auth/logoutnoneRevoke a refresh token’s rotation family
POST /auth/magic/requestnoneSend a magic-link code
POST /auth/magic/verifynoneConsume a code and log in
POST /auth/webauthn/assert/beginnoneBegin a passkey login
POST /auth/webauthn/assert/finishnoneComplete a passkey login
GET /auth/oauth/:provider/startnoneBegin a delegated login
GET /auth/oauth/:provider/callbacknoneProvider redirect target

POST /auth/login

{
"identity": "[email protected]",
"password": "",
"identity_type": "mobile",
"realm": "patient"
}

identity and password are required. identity_type names a declared identifier column; naming an undeclared column returns 400 bad_identity_type with the declared set in the message. realm selects the realm; omitted means the default realm. "realm": "superadmin" short-circuits to the control-plane login — there is no /superadmin/login route.

Three success shapes, all 200:

{
"token": "v4.public.…",
"refresh_token": "",
"expires_in": 900,
"user": { "id": "U01K…", "email": "", "displayname": "" }
}
{ "mfa_required": true, "mfa_token": "", "methods": ["totp"] }
{ "session_id": "", "expires_at": "2026-07-23T12:00:00Z", "user": { "id": "U01K…" } }

The third is a managed tenant. It also sets an HttpOnly, Secure, SameSite=Lax cookie named kis_session, and carries no access token and no refresh token.

StatusCode
400bad_request, missing_fields, no_tenant, unknown_realm, bad_identity_type
401invalid_credentials — identical for unknown identity, no stored password, and wrong password
403provider_not_allowed, hook_denied
409session_limit
429login_abuse (a scripted denial), rate_limited (the built-in limiter)
500engine_unavailable, user_query_failed, mfa_lookup_failed, sign_failed, issue_failed
503policy_unavailable, session_service_unavailable

POST /auth/refresh{"refresh_token": "…"}200 {token, refresh_token, expires_in}.

The presented token dies on use. Replaying an already-rotated token returns 401 token_reuse and revokes the whole rotation family descended from that login. On a managed tenant this route returns 403 managed_session — refresh tokens do not exist there.

POST /auth/logout{"refresh_token": "…"}200 {"ok": true}. An absent or empty refresh_token also returns 200 {"ok": true}.

Other statuses on both: 400 bad_request / no_tenant · 401 invalid_token · 403 hook_denied · 500 engine_unavailable, lookup_failed, rotate_failed, issue_failed, sign_failed.

RouteBodyResponse
POST /auth/magic/request{"email": "…"}200 {"ok": true} unconditionally — the same answer for an unknown address
POST /auth/magic/verify{"email": "…", "code": "…"}the login response, or the mfa_required shape

Codes are delivered through the configured notification backend. With no backend configured the code is dropped and the request still returns 200.

Statuses: 400 bad_request / missing_fields / no_tenant · 401 invalid_magic (unknown, expired or already consumed) / invalid_token · 500 engine_unavailable, code_failed, persist_failed, delete_failed, mfa_lookup_failed, sign_failed, issue_failed.

Four routes are mounted and every one of them answers 503 webauthn_disabled. The deployable binary wires no WebAuthn provider and there is no configuration key that turns one on.

RouteBodyResponse when enabled
POST /auth/webauthn/register/begin{"name": "laptop"}{session_id, options} — a CredentialCreation object
POST /auth/webauthn/register/finish{"session_id": "…", "response": {…}}{"ok": true}
POST /auth/webauthn/assert/begin{"email": "…"}{session_id, options} — a CredentialAssertion object
POST /auth/webauthn/assert/finish{"session_id": "…", "response": {…}}the login response

The register routes require an authenticated principal; the assert routes do not.

Two routes are mounted and every one of them answers 404 unknown_provider. The deployable binary never wires a provider. Declaring a provider in the block’s definitions wires no credentials — a declaration is a name gate only, and nothing reads its type or template at request time.

RouteRequestResponse when a provider exists
GET /auth/oauth/:provider/startprovider in the path302 Found with Location set to the provider’s authorization URL (PKCE S256)
GET /auth/oauth/:provider/callbackquery code, state, or errorthe login response, or the mfa_required shape

Statuses: 400 no_tenant / missing_callback · 401 provider_error, invalid_state, exchange_failed, invalid_token · 403 hook_denied · 404 unknown_provider · 500 engine_unavailable, state_failed, persist_failed, userinfo_failed, no_email, user_provision_failed.


TOTP is the only method. methods in the mfa_required response is always ["totp"].

Method + pathAuthBodyResponse
POST /auth/mfa/totp/setupBearernone200 {"secret": "…", "otpauth_url": "otpauth://…"}
POST /auth/mfa/totp/enableBearer{"code": "…"}200 {"ok": true}
POST /auth/mfa/totp/disableBearer{"code": "…"}200 {"ok": true}, also when nothing was enrolled
POST /auth/mfa/verifynone — mfa_token is the credential{"mfa_token": "…", "code": "…"}the login response

Disable requires a currently valid code, so a hijacked session alone cannot strip MFA. A code is single-use inside its step window.

StatusCode
400missing_fields, no_tenant, no_pending_setup
401auth_required, invalid_code, code_reused, invalid_mfa_token, mfa_not_enabled, unknown_realm
409already_enabled
500engine_unavailable, lookup_failed, persist_failed, user_lookup_failed, sign_failed, issue_failed

An MFA-pending token is rejected on every protected route with 401 invalid_token.


RouteBodyResponse
POST /auth/password/reset/request{"email": "…"}200 {"ok": true} unconditionally
POST /auth/password/reset/confirm{"email": "…", "code": "…", "new_password": "…"}200 {"ok": true}

Statuses: 400 bad_request / missing_fields / no_tenant · 401 invalid_reset_token · 403 hook_denied · 500 engine_unavailable, code_failed, persist_failed, update_failed.

POST /auth/apikey — Bearer or ApiKey; a resolved principal is required.

{ "service": "reporting", "capabilities": { "scope": ["acme:shop:prod:*"] }, "expires_in_days": 90 }

service defaults to default; expires_in_days defaults to 365. Requested capabilities (scope grants, roles, is_superadmin) are checked against the issuer’s own authority.

Response is 200 — not 201:

{ "id": "", "key": "<id>.<base64url-secret>", "expires_at": "", "capabilities": { "…": "" } }

key is returned once and stored hashed. Present it later as Authorization: ApiKey <id>.<secret>.

DELETE /auth/apikey/:id — Bearer or ApiKey → 200 {"ok": true}. The :id is the part before the dot in the wire key.

Statuses: 400 bad_request / missing_id / no_tenant · 401 auth_required · 403 scope_exceeds_authority / hook_denied · 500 engine_unavailable, generate_failed, persist_failed.

POST /auth/token with Authorization: ApiKey <key>. A Bearer is refused with 401 apikey_required. No body; the optional query parameter ?audience=<svc> sets the token’s aud.

{ "token": "v4.public.…", "token_type": "Bearer", "expires_in": 900 }

Only a key whose capabilities carry a scope list can be exchanged; an ordinary user key returns 403 not_a_service_key. 401 apikey_invalid is opaque — it never distinguishes an unknown key from a bad secret from an expired one, and a missing or unparseable expiry is treated as expired.

GET /.well-known/jwks.json — no credential, but the CPET headers are required because key sets are per-tenant. One tenant’s document never contains another’s keys, and it lists inactive keys alongside active ones so tokens signed before a rotation keep verifying.

{
"keys": [
{ "kid": "", "kty": "OKP", "crv": "Ed25519", "x": "<base64url>", "alg": "EdDSA", "use": "sig" }
]
}

400 no_tenant without the headers; 500 jwks_failed on assembly error. There is no HTTP route that rotates a signing key.

POST /auth/bootstrap — no credential; gated by a shared secret from cluster config. Creates the first operator in the control plane governing the request’s product and environment, not in the request’s own tenant.

{ "token": "<shared secret>", "email": "", "password": "", "firstname": "", "lastname": "", "mobile": "" }

200 {"ok": true, "id": "U01K…", "email": "…"}. The created operator carries the root role.

403 already_bootstrapped is one envelope for three causes — the secret is unset, the supplied token is wrong, or an operator already exists. Other statuses: 400 bad_request / missing_fields / no_tenant · 403 hook_denied · 500 persist_failed · 503 superadmin_not_deployed.

Terminal window
curl -s localhost:5030/auth/bootstrap -H 'X-Customer: default' -H 'X-Product: iam' -H 'X-Env: dev' -H 'X-Tenant: default' -d '{"token":"…","email":"[email protected]","password":"…","firstname":"Admin","lastname":"User"}'

Four mesh-internal routes. id is the control-plane row id; the opaque session_id credential is returned only at login and never by enumeration.

Method + pathBodyResponse
POST /internal/session/exchange{"session_id": "…", "audience": "…"}200 {token, format, expires_at, cpet}format is v4.public
POST /internal/session/revoke{"id": "…", "reason": "admin_revoke"}204 No Content
POST /internal/session/rotate{"id": "…", "aal": 2}200 {session_id, absolute_expiry}aal: 0 leaves the level unchanged
GET /internal/session/list?principal=<id>none200 {"sessions": [{id, principal_id, cpet, status, aal, auth_method, user_agent_label, created_at, last_seen_at}]}

Statuses: 400 bad_request · 401 invalid_session (identical for revoked, expired and unknown) · 404 unknown_session on rotate · 500 exchange_failed, revoke_failed, rotate_failed, list_failed.


Every entity in the tenant’s composed schema is served generically. Authorization is the entity’s own access: rules plus row-level security — see authorization.

Method + pathAuthPurpose
GET /rest/:entityBearer or ApiKeyList → {"data": […], "count": n}
POST /rest/:entityBearerCreate → 201
POST /rest/:entity/searchBearerStructured filter in the body
GET /rest/:entity/id/:idBearerRead one
PUT /rest/:entityBearerReplace, id in the body
PUT /rest/:entity/id/:idBearerReplace, id in the path
PATCH /rest/:entityBearerPartial update, id in the body
PATCH /rest/:entity/id/:idBearerPartial update, id in the path
DELETE /rest/:entity/id/:idBearerDelete — soft or hard per the entity’s traits
POST /rest/:entity/id/:id/restoreBearerRestore a soft-deleted row
POST /restBearerBulk envelope, default action create
PUT /restBearerBulk envelope, default action update
PATCH /restBearer or ApiKeyBulk envelope, default action update
DELETE /restBearerBulk envelope, default action delete

The single-row path is /rest/:entity/id/:id. /rest/:entity/:id is not registered.

A bulk body is an operations envelope. data is a single-key object keyed by entity name, whose value is one row or an array of rows. An operation that omits action inherits the default for the HTTP verb; the explicit values are create, update, delete, upsert and restore.

{
"operations": [
{ "action": "create", "data": { "users": { "email": "[email protected]" } } },
{ "action": "update", "data": { "role": { "id": "01HVK…", "displayname": "Auditor" } } }
]
}
{
"operations": [
{ "action": "create", "entity": "users", "data": [ { "id": "U01K…" } ], "affected_rows": 1 }
],
"stats": { "operations_run": 2, "operations_ok": 2, "operations_fail": 0, "duration_ms": 14 }
}

The whole batch runs in one transaction — any single failure rolls all of it back. A failed operation carries error and error_code. Envelope-level failures are 400 v2.bad_json, 400 v2.empty_envelope and 400 v2.body_read.

List responses carry data and count, plus total, stats, meta, states, allowed_actions and page blocks when the corresponding query parameters ask for them. ?datastore=<name> selects a datastore on any entity route; ?expression= applies a jq expression to the response and returns 400 v2.bad_expression if it fails to compile.

Statuses: 401 plain-text Unauthorized · 403 v2.no_tenant · 404 v2.entity_not_found · 400/403/500 v2.create_failed, v2.update_failed, v2.delete_failed, v2.restore_failed, v2.query_failed — an access-rule denial surfaces through this mapping · 500 v2.engine_unavailable.

Thirteen routes under /anon/rest mirror the authenticated surface with no credential. An entity is reachable there only if its own access: rules admit an anonymous caller.

Method + path
POST · PUT · PATCH · DELETE /anon/rest
GET /anon/rest/:entity
GET /anon/rest/:entity/id/:id
POST /anon/rest/:entity
PUT /anon/rest/:entity · PUT /anon/rest/:entity/id/:id
PATCH /anon/rest/:entity · PATCH /anon/rest/:entity/id/:id
DELETE /anon/rest/:entity/id/:id
POST /anon/rest/:entity/search

Exactly two alias mounts ship, seven routes each. They address a row as <base>/:idnot <base>/id/:id like the generic surface — and they register no restore route.

Method + pathPurpose
GET /admin/user · GET /admin/roleList
POST /admin/user · POST /admin/roleCreate → 201
POST /admin/user/search · POST /admin/role/searchStructured filter in the body
GET /admin/user/:id · GET /admin/role/:idRead one
PATCH /admin/user/:id · PATCH /admin/role/:idPartial update
PUT /admin/user/:id · PUT /admin/role/:idReplace
DELETE /admin/user/:id · DELETE /admin/role/:idDelete

/admin/user writes go through the password-hashing hook, and password is redacted on every read. Role assignment happens here, on the user row’s properties.roles; /admin/role defines role rows and assigns nothing.

Terminal window
curl -s localhost:5030/admin/user -H "Authorization: Bearer $TOKEN" $H -d '{"email":"[email protected]","firstname":"Ada","lastname":"Lovelace","password":"…","active":true,"properties":{"roles":["admin","auditor"]}}'

All Bearer-gated, no role gate.

RouteReturns
GET /schemaComposed schema: name, version, default datastore, and the name sets for datastores, entities, relations, types, enums, traits, embeddables, stateflows, views, functions
GET /schema/entities[{name, fields, relations}]
GET /schema/entities/:entityDetail for one entity
GET /schema/graphqlGraphQL SDL as text/plain
GET /schema/openapi.jsonOpenAPI 3.1 document
GET /schema/jsonschema.jsonJSON Schema 2020-12, full bundle
GET /schema/jsonschema/:entityJSON Schema for one entity
GET /schema/zod.tsZod TypeScript bundle
GET /schema/zod/:entityZod TypeScript for one entity
GET /schema/cueCUE bundle
GET /schema/cue/:entityCUE for one entity
DELETE /cacheDrop every cached resource for the calling tenant → {"cleared": true, "tenant": "<cpet>"}

GET /schema/graphql returns the SDL only. There is no GraphQL execution endpoint.

Statuses: 403 v2.no_tenant · 500 v2.engine_unavailable, v2.export_render_failed, v2.cache_clear_failed.


Data-lifecycle operations against the caller’s own tenant, with separation of duties: a tenant admin generates and reviews, an operator executes. RequireAdmin accepts admin or superadmin; RequireSuperadmin accepts only superadmin. Role matching is case-insensitive. Every route accepts ?datastore=<name>.

Method + pathGateRequest → response
POST /admin/data/plansadmin{datastore, allow_drops}{plan_id, source, max_risk, warnings[], refusals[], steps[]}
GET /admin/data/plans/:idadmin{tenant, plan, steps[]}; 404 when unknown
POST /admin/data/plans/:id/applysuperadmin{datastore, confirm_max_risk, dry_run}{plan_id, status, steps_applied, steps_failed, steps_skipped}; 207 on partial completion
GET /admin/data/appliedadmin?plan_id=<id> required{tenant, plan_id, steps[]}; 400 without it
GET /admin/data/seedsadmin{tenant, datastore, seeds[]} with name, entity, source, strategy, status, applied_at, applied_checksum, error
POST /admin/data/seeds/runssuperadmin{datastore, names[], environment, dry_run, force}{plan_id, dry_run, applied_count, skipped_count, failed_count, outcomes[]}; 207 when any failed
GET /admin/data/locksadmin?prefix, ?op_kind, ?include_expired{locks: [{id, resource, holder_id, op_kind, acquired_at, expires_at, hostname, plan_id, notes, is_expired, seconds_to_expiry}]}
DELETE /admin/data/locks/:resourcesuperadmin→ release confirmation
GET /admin/data/retention/previewadmin?entity → preview payload
GET /admin/data/retention/archivesadmin{archives: [{name, type, datastore, suffix, settings}]}
GET /admin/data/retention/entitiesadmin→ per-entity policy and PII fields
GET /admin/data/retention/handoff/pendingadmin→ pending ETL handoffs
POST /admin/data/retention/runssuperadmin{datastore, entities[], phases[], dry_run, batch_size, max_rows_per_pass, grace_hours} → run result
POST /admin/data/retention/handoff/claimsuperadminclaim body → claim result
POST /admin/data/retention/handoff/confirmsuperadminconfirm body → confirm result
GET /admin/data/actionsadmin→ action catalogue; SQL bodies are omitted
GET /admin/data/materialize/pendingadmin{tasks: [{id, target_entity, target_field, target_row_id, source_entity, source_op, enqueued_at, attempts}]}
GET /admin/data/materialize/statsadmin→ queue statistics
POST /admin/data/materialize/drainsuperadmindrain body → drain result
GET /admin/data/references/:entityadmin→ reference map
GET /admin/data/hashesadmin→ query-hash list

confirm_max_risk is low, medium or high; omitted means low, and any other value is a 400. Apply refuses a plan whose risk exceeds what was confirmed. A non-empty refusals list on generate is the signal not to apply, even though the status is 200.

Seed runs execute synchronously inside the HTTP request. Retention and materialize operations are gated but unsupported on this service’s schema.

Statuses: 401 missing tenant identity (no code field on this surface) · 403 v2.requires_admin, v2.requires_superadmin.


The control plane is the reserved tenant <customer>:<product>:<env>:superadmin — one per product and environment. Operators reach these routes with the plane’s own CPET headers and a token minted by POST /auth/login with "realm": "superadmin".

POST /superadmin/impersonate — the only sanctioned path from an operator to tenant data. Body capped at 8 KiB.

{ "tenant": "acme:shop:prod:eu1", "user_id": "U01K…", "reason": "support ticket 4211", "read_write": false }
{
"token": "v4.public.…",
"session_id": "",
"expires_in": 900,
"mode": "readonly",
"acting_as": "U01K…",
"tenant": "acme:shop:prod:eu1"
}

mode is the literal string readonly or readwrite. reason is mandatory. Omitting read_write yields read-only and requires the exact role impersonate-readonly; "read_write": true requires the exact role impersonate. Neither role implies the other, and root implies neither. The returned token is the target user’s own, signed by the target tenant’s key, carrying an act claim naming the operator. A managed session is force-created so the subject and their admin can list and revoke it.

StatusCode
400bad_request, reason_required, no_tenant
401unauthorized
403not_an_operator, role_required, not_granted, impersonation_not_allowed
404no_such_user
500lookup_failed, sign_failed, session_failed
503tenant_unavailable

not_granted is returned identically for a tenant the operator has no grant on and for a tenant that does not exist, so operators cannot enumerate tenants by probing.

Method + pathRequest → response
POST /superadmin/registry/tenants{"slug": "acme:iam:prod:eu1", "displayname": "Acme EU", "domain": "acme.io"}201 {id, slug}
GET /superadmin/registry/tenants200 {"data": [{id, slug, displayname, active}]}, hard limit 500 rows
POST /superadmin/registry/tenants/provision{"slug", "datastore", "admin_email", "admin_password", "admin_firstname", "admin_lastname"}200 {cpet, bootstrap_steps_applied, admin_id, admin_existed, activated}

Registration records a row; provisioning creates the schema, tables and migration ledger, seeds the tenant admin and activates. Provisioning is idempotent. The database itself must already exist — the service creates the schema, never the database. slug may be a short name or a full CPET; the reserved names superadmin and shared are refused in any segment.

Statuses: 400 bad_request, reserved_or_invalid_name · 403 v2.requires_superadmin · 500 create_failed, list_failed, provision_failed · 503 superadmin_not_deployed.

There is no tenant-deletion route on this family. Removing a registry row is DELETE /rest/tenant/id/:id under the plane’s headers, requires the root role, and deletes the registry record only — no schema, tables or data are dropped. Deactivating is a PATCH setting active to false.

The operator roster and operator grants have no dedicated routes. They are administered through the generic entity surface under the plane’s headers, which works because those entities are split out of every tenant’s schema — the same paths under a tenant’s headers return 404 v2.entity_not_found.

RoutePurpose
GET · POST /rest/superadminList and create operators
GET · POST /rest/superadmin_grantList and create operator→tenant grants
PATCH /rest/superadmin_grant/id/:idRevoke by setting active to false — grants are re-read per request, so revocation is immediate
GET /rest/tenantRegistry rows with full entity query options
DELETE /rest/tenant/id/:idDelete a registry row (root only)
{ "superadmin_id": "U01K…", "tenant_id": "01HVK…", "reason": "EU on-call, ticket OPS-233", "active": true }

tenant_id is the registry row id, not a CPET. Both ids are final on a grant — revoke and re-grant rather than editing.

Twenty-three routes mirror the data-admin plane at another tenant. All require superadmin.

Method + pathPurpose
GET /superadmin/tenant{"tenants": ["acme:shop:prod:eu1", …], "count": n} — tenants this process has built resources for
DELETE /superadmin/tenant/:tenant/cache{"invalidated": "<full cpet>"}
POST /superadmin/tenant/:tenant/plansGenerate a plan
GET /superadmin/tenant/:tenant/plans/:idRead a plan
POST /superadmin/tenant/:tenant/plans/:id/applyExecute a plan
GET /superadmin/tenant/:tenant/appliedLedger for one plan (?plan_id required)
POST /superadmin/tenant/:tenant/seeds/runsRun seeds
GET /superadmin/tenant/:tenant/seedsList seeds
POST /superadmin/tenant/:tenant/retention/runsRun retention
GET /superadmin/tenant/:tenant/retention/previewPreview retention
POST /superadmin/tenant/:tenant/retention/handoff/claimClaim an ETL batch
POST /superadmin/tenant/:tenant/retention/handoff/confirmConfirm an ETL batch
GET /superadmin/tenant/:tenant/retention/handoff/pendingPending ETL batches
GET /superadmin/tenant/:tenant/retention/archivesArchive destinations
GET /superadmin/tenant/:tenant/retention/entitiesRetention policy per entity
GET /superadmin/tenant/:tenant/locksList locks
DELETE /superadmin/tenant/:tenant/locks/:resourceForce-release a lock
GET /superadmin/tenant/:tenant/actionsAction catalogue
GET /superadmin/tenant/:tenant/materialize/pendingPending refresh tasks
GET /superadmin/tenant/:tenant/materialize/statsRefresh queue statistics
POST /superadmin/tenant/:tenant/materialize/drainForce-drain the refresh queue
GET /superadmin/tenant/:tenant/references/:entityReference map
GET /superadmin/tenant/:tenant/hashesQuery hashes

Request and response bodies are identical to the /admin/data/* route each one mirrors.

GET /superadmin/tenant is process state, not the registry: a tenant that is registered and configured but has served no request is absent. For the authoritative list use GET /superadmin/registry/tenants.


Method + pathAuthPurpose
GET /readynoneReadiness probe
GET /healthnoneLiveness probe

Both are mounted by the service bootstrap before any other route, and the tenant middleware short-circuits both paths — they need no CPET headers and no credential. Both report healthy and ready from server start.


Earlier documentation, the checked-in functional test suite and the Go client each name routes the service does not register. Every one of these returns 404.

PathUse instead
/admin/realmRealms are configuration, not rows — see authentication
/admin/tenant/superadmin/registry/tenants and /rest/tenant under the plane’s headers
/superadmin/loginPOST /auth/login with "realm": "superadmin"
/auth/login/password, /auth/validate, /auth/session/refresh, /auth/jwt/refresh, /auth/updatepasswordPOST /auth/login, /auth/refresh, /auth/password/reset/confirm
/auth/realms, /auth/providersNo introspection route exists for either
any OTP routeNo OTP login provider ships — not over SMS, email or chat, and no route accepts a one-time code except MFA, magic link and password reset
/admin/users, /tenant/new, /authorize, /user/signup/admin/user, /superadmin/registry/tenants/provision
/rest/:entity/:id/rest/:entity/id/:id
/rest/:entity/action/:actionDeclared actions are catalogued at /admin/data/actions; there is no execution route
/schema/:entity/schema/entities/:entity
/metricsMetrics are pushed through the telemetry provider; no HTTP route is registered

The login body is {identity, password}. {email, password} is not accepted.