Skip to content

API reference ​

Narrative summary of the gateway endpoints (58 operations). Authoritative request/response schemas live on the running service:

  • OpenAPI 3.1: {CENTRAL_BASE}/openapi.json
  • Swagger UI: {CENTRAL_BASE}/docs

Example first

For a complete, working integration with the token kept off the browser see Quickstart: React SPA + Express BFF or Quickstart: Next.js (App Router). This page is the reference behind them.

Generate clients from OpenAPI. All operations have stable operationId values. Errors have the shape { "code": "…", "message": "…", "details"?: … }.

Authentication flow (auth-flow) ​

MethodPathAuthoperationIdNotes
GET/v1/auth/startPublicstartAuthenticationBrowser entry; 302 to the identity environment's authorize endpoint
GET/v1/auth/callbackPublic (identity redirect only)handleIdentityCallbackFixed central redirect URI; sets gateway cookie
GET/v1/auth/resumeGateway cookieresumeAuthenticationCalled by the identity UI after each onboarding step; not by platforms
POST/v1/auth/exchangePlatform Basic (BFF key)exchangeAuthenticationHandoffServer-to-server; one-time handoff_ code, default 60 s
POST/v1/auth/introspectPlatform Basic (BFF or backend key)introspectGatewaySessionServer-to-server; is this gatewayToken a live session of my platform? Always 200, { active: false } for unusable tokens
POST/v1/auth/logoutGateway cookie + allowlisted OriginlogoutRevokes the gateway session; returns logoutUrl
GET/v1/auth/logged-outPublic (identity redirect only)completeLogoutFixed post-logout redirect URI; 302 to the logout's destination

Start query parameters ​

ParamRequiredRules
platformKeyyes^[A-Z][A-Z0-9_]{1,63}$
returnToyes1–2048 chars; allowlisted
onboardingModenonone | platform | central; must match registry
defaultScreennoin (default) | up; the identity UI page an anonymous user lands on: in → /sign-in, up → /sign-up. Forwarded to the identity environment as screen_hint=signup

Exchange body ​

json
{ "code": "handoff_…" }

code must start with handoff_ (max length 200).

Exchange 200 body (runtime) ​

Returned by HandoffService.exchange:

FieldTypeMeaning
session.iduuidGateway session id
session.expiresAtdate-timeSession expiry
session.gatewayTokenstringOpaque credential for Central cookie auth
user.iduuidLocal user id
user.substringIdentity subject (unique only together with issuer)
user.issuerstringIssuer of the identity environment (additive field)
user.picturestringOptional; only when known
platform.keystringPlatform key
environment.key / environment.tierstringIdentity environment the user signed in through
onboarding.statusenumNOT_STARTED | IN_PROGRESS | COMPLETE
onboarding.requirementVersionintegerCurrent platform requirement version
onboarding.missingFieldsstring[]Required field keys still empty
onboarding.canCompleteCentrallybooleanFrom platform central_onboarding_enabled
profile.statusenumCOMPLETE | INCOMPLETE: the global profile requirements, enforced before every handoff
profile.missingFieldsstring[]Required global profile keys still empty
permissionsstring[]Gateway session permission snapshot (empty outside PROD)
returnTostringAbsolute allowlisted destination from start

Logout ​

Send the gateway cookie and an allowlisted Origin (a BFF forwards Cookie: iesf_auth_session=<gatewayToken> with its own Origin). Always clears the cookie (Max-Age=0, Cache-Control: no-store). Optional returnTo, either as a query parameter (like /v1/auth/start) or in the JSON body (no body, an empty body or {} are all accepted). When both are sent, the body wins:

http
POST /v1/auth/logout?returnTo=%2F
json
{ "returnTo": "/" }

returnTo follows the rules of the start returnTo, matched against the redirect pairs of the platform whose login created the session. Without it the destination is the return URL of the platform's default pair (only a session without a platform falls back to the environment's post-logout URI or ACCOUNT_CENTER_URL). logoutUrl always carries post_logout_redirect_uri={gateway}/v1/auth/logged-out and the destination as an encrypted state, and GET /v1/auth/logged-out redirects the browser there after the identity logout. Platform URLs therefore never have to be registered in an identity environment: the return URLs under /admin/platforms are enough.

json
{ "logoutUrl": "https://identity.iesf.gg/api/auth/oauth2/end-session?id_token_hint=…&client_id=iesf-gateway-prod&post_logout_redirect_uri=…" }

Navigate the browser to logoutUrl. { "logoutUrl": null } when there was no valid session (idempotent) or the environment has no reachable end-session endpoint. 403 FORBIDDEN when Origin is not allowlisted or returnTo is not registered for the session's platform; 400 INVALID_REDIRECT for a malformed returnTo. The session is revoked even when returnTo is rejected. A missing, tampered or expired (1 hour) state makes /v1/auth/logged-out redirect to ACCOUNT_CENTER_URL (or IDENTITY_POST_LOGOUT_REDIRECT_URI).

Introspect (session check for platform backends) ​

http
POST /v1/auth/introspect
Authorization: Basic base64(clientId:clientSecret)
Content-Type: application/json

{ "token": "<gatewayToken>" }

Server to server only. Authenticate with the platform's backend key (an API that is not a browser-facing BFF; it needs no registered URL) or any of its BFF keys. The answer always carries Cache-Control: no-store.

Active session (200):

json
{
  "active": true,
  "sessionId": "25f654d9-464d-49c9-a27f-97208ea9067b",
  "expiresAt": "2026-08-11T22:00:00.000Z",
  "platformKey": "IESF_COMPETITIONS",
  "user": {
    "id": "6f1c2c3e-8f7d-4b8e-9f1a-2b3c4d5e6f70",
    "issuer": "https://identity.iesf.gg/api/auth",
    "subject": "<identity-subject>",
    "email": "jane.doe@example.org",
    "emailVerified": true,
    "status": "ACTIVE"
  },
  "environment": { "key": "PROD", "tier": "production" }
}
ResponseMeaningWhat your backend does
200 { "active": true, … }Live session created by your platform's loginAccept the request; key the user by (user.issuer, user.subject)
200 { "active": false }Unknown, expired, revoked or logged-out token, a suspended or deleted user, or a session created by another platform's loginAnswer 401; the user must log in again
400Malformed bodyFix the request
401Your platform credentials are wrong (never the user's session)Fix configuration; do not sign the user out
5xx / timeoutCentral is unavailableAnswer 503; do not sign the user out

Recommended caching: 30–60 seconds.

  • Cache only active: true results, keyed by sha256(token) (never the raw token), for at most 30–60 s and never beyond expiresAt.
  • Do not cache active: false, errors or timeouts.
  • Consequence: a logout, an admin session revocation or a suspended account reaches your backend only after the cache period. Choose the shorter end for sensitive operations, or skip the cache for them.

Typical status codes (auth-flow) ​

CodeWhen
302Successful start / callback / resume / logged-out redirect
400VALIDATION_ERROR (missing/malformed parameters, malformed JSON, also an identity error instead of a code on callback); INVALID_REDIRECT (malformed returnTo)
401Bad platform Basic auth; AUTHORIZATION_CODE_EXCHANGE_FAILED, ID_TOKEN_INVALID, NONCE_MISMATCH, TOKEN_RESPONSE_INVALID; missing gateway cookie or session of another environment on resume
403returnTo or onboardingMode not registered for platform; ACCOUNT_DELETED / ACCOUNT_INACTIVE; logout Origin not allowlisted
404Unknown platform; invalid/expired/consumed state or handoff
409PLATFORM_CALLBACK_NOT_CONFIGURED; PLATFORM_CREDENTIALS_NOT_CONFIGURED (matched pair has no BFF key); IDENTITY_ENVIRONMENT_INACTIVE; CENTRAL_ONBOARDING_DISABLED; AUTH_TRANSACTION_ALREADY_BOUND; resume conflict when a handoff row already exists for the transaction (unique on transaction_id)
415UNSUPPORTED_MEDIA_TYPE: a body that is not JSON
502IDENTITY_DISCOVERY_INVALID (discovery issuer mismatch or missing endpoint)
503OIDC_CLIENT_NOT_CONFIGURED; IDENTITY_UNAVAILABLE

Session (session) ​

MethodPathAuthoperationIdNotes
POST/v1/session/bootstrapUser (Bearer user token or gateway cookie; machine tokens → 403)bootstrapCentralSessionCompatibility endpoint: idempotent local user, membership and onboarding bootstrap. Body { "platformKey": "…" }

Users (users) ​

MethodPathAuthoperationIdNotes
GET/v1/users/meUsergetMyUserDetails{ identity: IdentityAccount, localUser, profile }; identity is the local account view (no identity service call)

Profile (profile) ​

MethodPathAuthoperationId
GET/v1/profile/meUsergetMyProfile
PATCH/v1/profile/meUserpatchMyProfile
GET/v1/profile/me/detailsUsergetMyLocalProfileDetails
POST/v1/profile/me/avatar/upload-requestUsercreateAvatarUploadRequest

Account (account) ​

MethodPathAuthoperationIdNotes
GET/v1/account/meUsergetMyIdentityLocal view, no identity service call
DELETE/v1/account/meUser (cookie needs allowlisted Origin)deleteMyAccount204; suspends the identity user, then soft-deletes locally

GET /v1/account/me response:

json
{
  "id": "6f1c2c3e-8f7d-4b8e-9f1a-2b3c4d5e6f70",
  "issuer": "https://identity.iesf.gg/api/auth",
  "subject": "<identity-subject>",
  "environment": { "key": "PROD", "tier": "production" },
  "email": "jane.doe@example.org",
  "emailVerified": true,
  "status": "ACTIVE",
  "accountManagementUrl": "https://identity.iesf.gg/account"
}

Password changes and sign-in methods are not gateway endpoints any more: send the user to accountManagementUrl (issuer origin + /account). DELETE /v1/account/me first suspends the user through the identity service API (which also revokes identity sessions and tokens), then marks the local user SOFT_DELETED, revokes all gateway sessions and clears the cookie when cookie-authenticated. If the identity service fails it returns 503 IDENTITY_UNAVAILABLE and nothing changes locally.

Onboarding (onboarding) ​

MethodPathAuthoperationIdPurpose
GET/v1/onboarding/{platformKey}/requirementsUser (Bearer or gateway cookie)getMyOnboardingRequirementsFields + current values
GET/v1/onboarding/{platformKey}/statusUsergetMyOnboardingStatusCalculated status
PUT/v1/onboarding/{platformKey}/valuesUserputMyOnboardingValuesUpsert values (cookie writes need allowlisted Origin)
POST/v1/onboarding/{platformKey}/completeUsercompleteMyOnboardingComplete when no missing required fields
GET/v1/onboarding/global/requirementsUsergetMyGlobalOnboardingRequirementsGlobal profile fields + current values
GET/v1/onboarding/global/statusUsergetMyGlobalOnboardingStatusCOMPLETE / INCOMPLETE with missing fields
PUT/v1/onboarding/global/valuesUserputMyGlobalOnboardingValuesSave global values to the canonical profile (cookie writes need allowlisted Origin)

Platform requirements list the global fields first (scope: "GLOBAL", platformId: null), then the platform's own (scope: "PLATFORM"). See Global profile requirements.

Platform registry (platform-admin) ​

Requires platforms:admin (see Admin authentication).

MethodPathoperationIdNotes
GET/v1/platformslistPlatformsFilter ?identityEnvironmentKey=; archived platforms only with ?archived=true
POST/v1/platformscreatePlatformredirectPairs required (min 1); identityEnvironmentKey optional (default PROD); returns one BFF key per pair, once
GET/v1/platforms/{platformKey}getPlatform
PATCH/v1/platforms/{platformKey}updatePlatformredirectPairs? (updated in place by id, else matched by returnUrl; omitted pairs and their keys are deleted), identityEnvironmentKey?, …
DELETE/v1/platforms/{platformKey}deactivatePlatformSoft deactivate
POST/v1/platforms/{platformKey}/activateactivatePlatformRe-activate; 409 PLATFORM_ARCHIVED for an archived platform
POST/v1/platforms/{platformKey}/archivearchivePlatformInactive only (409 otherwise); revokes all keys, keeps data and the key reserved; idempotent
POST/v1/platforms/{platformKey}/restorerestorePlatformBack to inactive, without keys; idempotent
POST/v1/platforms/{platformKey}/credentialscreatePlatformCredential{ "kind": "bff", "redirectPairId" } or { "kind": "backend" }; one per pair, one backend; secret once (201)
POST/v1/platforms/{platformKey}/credentials/{credentialId}/rotaterotatePlatformCredentialNew secret once; previous one valid for 60 min unless revokePreviousImmediately or rotateClientId
DELETE/v1/platforms/{platformKey}/credentials/{credentialId}/previous-secretrevokePreviousPlatformCredentialSecretEnds the grace period
DELETE/v1/platforms/{platformKey}/credentials/{credentialId}deletePlatformCredentialRevokes the key; 409 for the last BFF key of an active platform

callbackUrl and allowedReturnUrls were removed from requests (rejected as unknown, 400) and responses. Platform response:

json
{
  "id": "…", "platformKey": "IESF_COMPETITIONS", "displayName": "IESF Competitions", "active": true,
  "requirementVersion": 1, "centralOnboardingEnabled": true, "platformOnboardingPath": "/onboarding",
  "defaultOnboardingMode": "central", "credentialsConfigured": true,
  "credentials": [{ "id": "…", "kind": "bff", "redirectPairId": "…", "clientId": "platform_iesf_competitions_a1b2c3d4e5f6",
    "createdAt": "2026-08-12T09:00:00.000Z", "rotatedAt": null, "lastUsedAt": null, "previousSecretExpiresAt": null }],
  "redirectPairs": [{ "id": "…", "label": "Production", "returnUrl": "https://competition.iesf.gg/", "callbackUrl": "https://competition.iesf.gg/api/auth/callback", "isDefault": true }],
  "identityEnvironment": { "key": "PROD", "displayName": "Production", "tier": "production", "active": true },
  "metadata": {}, "createdAt": "2026-08-12T09:00:00.000Z", "updatedAt": "2026-08-12T09:00:00.000Z"
}

credentialsConfigured is true when the callback of every pair is served by a BFF key. Secrets and their hashes are never returned except the one-time clientSecret of the create, issue and rotate responses ({ "platform", "credentials": [{ "id", "kind", "redirectPairId", "clientId", "clientSecret" }] }).

Unknown environment key → 400 IDENTITY_ENVIRONMENT_NOT_FOUND; inactive → IDENTITY_ENVIRONMENT_INACTIVE.

Identity environments (identity-environment-admin) ​

Requires platforms:admin. Procedures: Identity operations.

MethodPathoperationIdNotes
GET/v1/identity-environmentslistIdentityEnvironments{ items }; deployment first, then production/staging/development, then key
POST/v1/identity-environmentscreateIdentityEnvironment201
GET/v1/identity-environments/{environmentKey}getIdentityEnvironmentAdds platforms: [{ platformKey, displayName, active }]
PATCH/v1/identity-environments/{environmentKey}updateIdentityEnvironmentAt least one field; deployment is read-only (409)
DELETE/v1/identity-environments/{environmentKey}deactivateIdentityEnvironmentSoft; 409 while active platforms use it, or for deployment
POST/v1/identity-environments/{environmentKey}/activateactivateIdentityEnvironment
POST/v1/identity-environments/{environmentKey}/validatevalidateIdentityEnvironmentRead-only checks against the issuer

Create body (oidcClientSecret is write-only, postLogoutRedirectUri and metadata optional):

json
{
  "environmentKey": "DEV",
  "displayName": "Development",
  "tier": "development",
  "issuer": "https://dev.identity.iesf.gg/api/auth",
  "oidcClientId": "iesf-gateway-dev",
  "oidcClientSecret": "gateway-dev-client-secret",
  "postLogoutRedirectUri": "https://card-dev.iesf.gg/"
}

Rules: environmentKey ^[A-Z][A-Z0-9_]{1,31}$; tier development | staging | production; issuer HTTPS without query, fragment or credentials (HTTP only for localhost, 127.0.0.1, [::1], *.localhost) and different from the deployment issuer; oidcClientId ^[0-9A-Za-z@._~-]{1,128}$. PATCH accepts any of displayName, tier, issuer, oidcClientId, oidcClientSecret (string replaces, null removes), postLogoutRedirectUri (string or null), metadata. 409 on duplicate key / issuer / (issuer, client), deployment issuer, or a tier change while platforms are bound; 503 IDENTITY_ENVIRONMENT_SECRET_KEY_MISSING when a secret is sent without IDENTITY_ENVIRONMENT_SECRET_KEY.

IdentityEnvironment:

json
{
  "id": "…",
  "environmentKey": "DEV",
  "displayName": "Development",
  "tier": "development",
  "source": "database",
  "active": true,
  "issuer": "https://dev.identity.iesf.gg/api/auth",
  "oidcClientId": "iesf-gateway-dev",
  "oidcClientSecretConfigured": true,
  "postLogoutRedirectUri": "https://card-dev.iesf.gg/",
  "grantsGatewayPermissions": false,
  "metadata": {},
  "platformCount": 1,
  "activePlatformCount": 1,
  "createdAt": "…",
  "updatedAt": "…"
}

source is deployment for PROD (issuer, client and post-logout URI come from IDENTITY_* config; the only row with grantsGatewayPermissions: true) and database otherwise.

Validate response:

json
{
  "environmentKey": "DEV",
  "ok": true,
  "checks": [
    { "name": "discovery", "status": "ok", "message": "OpenID discovery reports issuer https://dev.identity.iesf.gg/api/auth" },
    { "name": "endpoints", "status": "ok", "message": "…" },
    { "name": "jwks", "status": "ok", "message": "…" },
    { "name": "client", "status": "ok", "message": "…" },
    { "name": "secret", "status": "ok", "message": "…" }
  ]
}

User admin (user-admin) ​

Requires platforms:admin.

MethodPathoperationIdNotes
GET/v1/admin/userslistUsers?search= (case-insensitive e-mail substring), page (default 1), pageSize (default 25, max 100), environmentKey; ordered by lastLoginAt desc
PUT/v1/admin/users/{userId}/roles/platform_admingrantPlatformAdminIdempotent; 409 ROLE_REQUIRES_DEPLOYMENT_ENVIRONMENT for non-PROD users
DELETE/v1/admin/users/{userId}/roles/platform_adminrevokePlatformAdminIdempotent; strips permissions from active sessions immediately; 409 LAST_PLATFORM_ADMIN
POST/v1/admin/users/{userId}/sessions/revokerevokeUserSessionsGateway sessions, then identity sessions

List response (AdminUser items; role endpoints return one AdminUser):

json
{
  "items": [{
    "id": "6f1c2c3e-8f7d-4b8e-9f1a-2b3c4d5e6f70",
    "issuer": "https://identity.iesf.gg/api/auth",
    "subject": "<identity-subject>",
    "environmentKey": "PROD",
    "deploymentEnvironment": true,
    "email": "jane.doe@example.org",
    "emailVerified": true,
    "status": "ACTIVE",
    "roles": ["platform_admin"],
    "lastLoginAt": "2026-10-01T08:00:00.000Z",
    "createdAt": "2026-08-12T09:00:00.000Z"
  }],
  "total": 1,
  "page": 1,
  "pageSize": 25
}

status is ACTIVE | SOFT_DELETED | SUSPENDED; roles is ["platform_admin"] or [].

Session revoke response:

json
{ "revokedGatewaySessions": 2, "identitySessionsRevoked": true }

identitySessionsRevoked is false only when the identity service reports that the user does not exist. On identity failure the call returns 503 IDENTITY_UNAVAILABLE / IDENTITY_SERVICE_NOT_CONFIGURED with details.revokedGatewaySessions (gateway sessions are already revoked).

Onboarding schema (onboarding-admin) ​

Requires onboarding:schema:read (reads) / onboarding:schema:write (writes). Non-central callers must also carry a matching platform claim (iesf_platform / platform_key) unless they have platforms:admin.

MethodPathoperationId
GET/v1/platforms/{platformKey}/onboarding/fieldslistOnboardingFields
POST/v1/platforms/{platformKey}/onboarding/fieldscreateOnboardingField
GET/v1/platforms/{platformKey}/onboarding/fields/{fieldKey}getOnboardingField
PATCH/v1/platforms/{platformKey}/onboarding/fields/{fieldKey}updateOnboardingField
DELETE/v1/platforms/{platformKey}/onboarding/fields/{fieldKey}archiveOnboardingField

Global profile requirements apply to every platform, so they require platforms:admin together with onboarding:schema:read / onboarding:schema:write; a platform-scoped credential cannot change them. {profileFieldKey} is a canonical profile key such as dateOfBirth.

MethodPathoperationId
GET/v1/admin/onboarding/global-fieldslistGlobalOnboardingFields
POST/v1/admin/onboarding/global-fieldscreateGlobalOnboardingField
GET/v1/admin/onboarding/global-fields/{profileFieldKey}getGlobalOnboardingField
PATCH/v1/admin/onboarding/global-fields/{profileFieldKey}updateGlobalOnboardingField
DELETE/v1/admin/onboarding/global-fields/{profileFieldKey}archiveGlobalOnboardingField

Health (health) ​

MethodPathoperationIdBehavior
GET/health/livegetLivenessNo dependency checks
GET/health/readygetReadinessChecks PostgreSQL

Security schemes (OpenAPI) ​

  • platformBasicAuth — exchange only (Central platform credentials, not identity credentials)
  • gatewayCookieAuth — resume, logout and cookie-authenticated user/admin routes; writes need an allowlisted Origin
  • userBearerAuth — JWT access token of a signed-in user from an active identity environment
  • serviceBearerAuth — machine (client_credentials) JWT access token with scopes

Bearer token rules ​

  • The token's iss selects the identity environment (it must be active); the signature is checked against that environment's JWKS; algorithms EdDSA/RS256/ES256.
  • Audience must equal IDENTITY_ACCESS_TOKEN_AUDIENCE (default http://localhost:8080). Request the token with resource=<audience> at the token endpoint, otherwise identity-api issues an opaque token.
  • ID tokens (with nonce) are rejected.
  • Machine token: no email and sub missing or equal to azp / client_id. Permissions come from scope (platforms:admin, onboarding:schema:read, onboarding:schema:write) only when the issuer is PROD; machine tokens from other environments have none. Machine tokens are refused on user endpoints (403).
  • User token: provisioned as a user by (issuer, sub); permissions come only from local roles and only for PROD.

Admin authentication ​

Admin endpoints accept any of: serviceBearerAuth (PROD machine token with the scope), userBearerAuth (PROD user with platform_admin), or gatewayCookieAuth (session of a PROD platform_admin). Missing credentials → 401 UNAUTHORIZED; missing permission → 403 FORBIDDEN.