Appearance
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)
| Method | Path | Auth | operationId | Notes |
|---|---|---|---|---|
GET | /v1/auth/start | Public | startAuthentication | Browser entry; 302 to the identity environment's authorize endpoint |
GET | /v1/auth/callback | Public (identity redirect only) | handleIdentityCallback | Fixed central redirect URI; sets gateway cookie |
GET | /v1/auth/resume | Gateway cookie | resumeAuthentication | Called by the identity UI after each onboarding step; not by platforms |
POST | /v1/auth/exchange | Platform Basic (BFF key) | exchangeAuthenticationHandoff | Server-to-server; one-time handoff_ code, default 60 s |
POST | /v1/auth/introspect | Platform Basic (BFF or backend key) | introspectGatewaySession | Server-to-server; is this gatewayToken a live session of my platform? Always 200, { active: false } for unusable tokens |
POST | /v1/auth/logout | Gateway cookie + allowlisted Origin | logout | Revokes the gateway session; returns logoutUrl |
GET | /v1/auth/logged-out | Public (identity redirect only) | completeLogout | Fixed post-logout redirect URI; 302 to the logout's destination |
Start query parameters
| Param | Required | Rules |
|---|---|---|
platformKey | yes | ^[A-Z][A-Z0-9_]{1,63}$ |
returnTo | yes | 1–2048 chars; allowlisted |
onboardingMode | no | none | platform | central; must match registry |
defaultScreen | no | in (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:
| Field | Type | Meaning |
|---|---|---|
session.id | uuid | Gateway session id |
session.expiresAt | date-time | Session expiry |
session.gatewayToken | string | Opaque credential for Central cookie auth |
user.id | uuid | Local user id |
user.sub | string | Identity subject (unique only together with issuer) |
user.issuer | string | Issuer of the identity environment (additive field) |
user.picture | string | Optional; only when known |
platform.key | string | Platform key |
environment.key / environment.tier | string | Identity environment the user signed in through |
onboarding.status | enum | NOT_STARTED | IN_PROGRESS | COMPLETE |
onboarding.requirementVersion | integer | Current platform requirement version |
onboarding.missingFields | string[] | Required field keys still empty |
onboarding.canCompleteCentrally | boolean | From platform central_onboarding_enabled |
profile.status | enum | COMPLETE | INCOMPLETE: the global profile requirements, enforced before every handoff |
profile.missingFields | string[] | Required global profile keys still empty |
permissions | string[] | Gateway session permission snapshot (empty outside PROD) |
returnTo | string | Absolute 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=%2Fjson
{ "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" }
}| Response | Meaning | What your backend does |
|---|---|---|
200 { "active": true, … } | Live session created by your platform's login | Accept 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 login | Answer 401; the user must log in again |
400 | Malformed body | Fix the request |
401 | Your platform credentials are wrong (never the user's session) | Fix configuration; do not sign the user out |
5xx / timeout | Central is unavailable | Answer 503; do not sign the user out |
Recommended caching: 30–60 seconds.
- Cache only
active: trueresults, keyed bysha256(token)(never the raw token), for at most 30–60 s and never beyondexpiresAt. - 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)
| Code | When |
|---|---|
302 | Successful start / callback / resume / logged-out redirect |
400 | VALIDATION_ERROR (missing/malformed parameters, malformed JSON, also an identity error instead of a code on callback); INVALID_REDIRECT (malformed returnTo) |
401 | Bad 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 |
403 | returnTo or onboardingMode not registered for platform; ACCOUNT_DELETED / ACCOUNT_INACTIVE; logout Origin not allowlisted |
404 | Unknown platform; invalid/expired/consumed state or handoff |
409 | PLATFORM_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) |
415 | UNSUPPORTED_MEDIA_TYPE: a body that is not JSON |
502 | IDENTITY_DISCOVERY_INVALID (discovery issuer mismatch or missing endpoint) |
503 | OIDC_CLIENT_NOT_CONFIGURED; IDENTITY_UNAVAILABLE |
Session (session)
| Method | Path | Auth | operationId | Notes |
|---|---|---|---|---|
POST | /v1/session/bootstrap | User (Bearer user token or gateway cookie; machine tokens → 403) | bootstrapCentralSession | Compatibility endpoint: idempotent local user, membership and onboarding bootstrap. Body { "platformKey": "…" } |
Users (users)
| Method | Path | Auth | operationId | Notes |
|---|---|---|---|---|
GET | /v1/users/me | User | getMyUserDetails | { identity: IdentityAccount, localUser, profile }; identity is the local account view (no identity service call) |
Profile (profile)
| Method | Path | Auth | operationId |
|---|---|---|---|
GET | /v1/profile/me | User | getMyProfile |
PATCH | /v1/profile/me | User | patchMyProfile |
GET | /v1/profile/me/details | User | getMyLocalProfileDetails |
POST | /v1/profile/me/avatar/upload-request | User | createAvatarUploadRequest |
Account (account)
| Method | Path | Auth | operationId | Notes |
|---|---|---|---|---|
GET | /v1/account/me | User | getMyIdentity | Local view, no identity service call |
DELETE | /v1/account/me | User (cookie needs allowlisted Origin) | deleteMyAccount | 204; 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)
| Method | Path | Auth | operationId | Purpose |
|---|---|---|---|---|
GET | /v1/onboarding/{platformKey}/requirements | User (Bearer or gateway cookie) | getMyOnboardingRequirements | Fields + current values |
GET | /v1/onboarding/{platformKey}/status | User | getMyOnboardingStatus | Calculated status |
PUT | /v1/onboarding/{platformKey}/values | User | putMyOnboardingValues | Upsert values (cookie writes need allowlisted Origin) |
POST | /v1/onboarding/{platformKey}/complete | User | completeMyOnboarding | Complete when no missing required fields |
GET | /v1/onboarding/global/requirements | User | getMyGlobalOnboardingRequirements | Global profile fields + current values |
GET | /v1/onboarding/global/status | User | getMyGlobalOnboardingStatus | COMPLETE / INCOMPLETE with missing fields |
PUT | /v1/onboarding/global/values | User | putMyGlobalOnboardingValues | Save 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).
| Method | Path | operationId | Notes |
|---|---|---|---|
GET | /v1/platforms | listPlatforms | Filter ?identityEnvironmentKey=; archived platforms only with ?archived=true |
POST | /v1/platforms | createPlatform | redirectPairs required (min 1); identityEnvironmentKey optional (default PROD); returns one BFF key per pair, once |
GET | /v1/platforms/{platformKey} | getPlatform | |
PATCH | /v1/platforms/{platformKey} | updatePlatform | redirectPairs? (updated in place by id, else matched by returnUrl; omitted pairs and their keys are deleted), identityEnvironmentKey?, … |
DELETE | /v1/platforms/{platformKey} | deactivatePlatform | Soft deactivate |
POST | /v1/platforms/{platformKey}/activate | activatePlatform | Re-activate; 409 PLATFORM_ARCHIVED for an archived platform |
POST | /v1/platforms/{platformKey}/archive | archivePlatform | Inactive only (409 otherwise); revokes all keys, keeps data and the key reserved; idempotent |
POST | /v1/platforms/{platformKey}/restore | restorePlatform | Back to inactive, without keys; idempotent |
POST | /v1/platforms/{platformKey}/credentials | createPlatformCredential | { "kind": "bff", "redirectPairId" } or { "kind": "backend" }; one per pair, one backend; secret once (201) |
POST | /v1/platforms/{platformKey}/credentials/{credentialId}/rotate | rotatePlatformCredential | New secret once; previous one valid for 60 min unless revokePreviousImmediately or rotateClientId |
DELETE | /v1/platforms/{platformKey}/credentials/{credentialId}/previous-secret | revokePreviousPlatformCredentialSecret | Ends the grace period |
DELETE | /v1/platforms/{platformKey}/credentials/{credentialId} | deletePlatformCredential | Revokes 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.
| Method | Path | operationId | Notes |
|---|---|---|---|
GET | /v1/identity-environments | listIdentityEnvironments | { items }; deployment first, then production/staging/development, then key |
POST | /v1/identity-environments | createIdentityEnvironment | 201 |
GET | /v1/identity-environments/{environmentKey} | getIdentityEnvironment | Adds platforms: [{ platformKey, displayName, active }] |
PATCH | /v1/identity-environments/{environmentKey} | updateIdentityEnvironment | At least one field; deployment is read-only (409) |
DELETE | /v1/identity-environments/{environmentKey} | deactivateIdentityEnvironment | Soft; 409 while active platforms use it, or for deployment |
POST | /v1/identity-environments/{environmentKey}/activate | activateIdentityEnvironment | |
POST | /v1/identity-environments/{environmentKey}/validate | validateIdentityEnvironment | Read-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.
| Method | Path | operationId | Notes |
|---|---|---|---|
GET | /v1/admin/users | listUsers | ?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_admin | grantPlatformAdmin | Idempotent; 409 ROLE_REQUIRES_DEPLOYMENT_ENVIRONMENT for non-PROD users |
DELETE | /v1/admin/users/{userId}/roles/platform_admin | revokePlatformAdmin | Idempotent; strips permissions from active sessions immediately; 409 LAST_PLATFORM_ADMIN |
POST | /v1/admin/users/{userId}/sessions/revoke | revokeUserSessions | Gateway 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.
| Method | Path | operationId |
|---|---|---|
GET | /v1/platforms/{platformKey}/onboarding/fields | listOnboardingFields |
POST | /v1/platforms/{platformKey}/onboarding/fields | createOnboardingField |
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.
| Method | Path | operationId |
|---|---|---|
GET | /v1/admin/onboarding/global-fields | listGlobalOnboardingFields |
POST | /v1/admin/onboarding/global-fields | createGlobalOnboardingField |
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)
| Method | Path | operationId | Behavior |
|---|---|---|---|
GET | /health/live | getLiveness | No dependency checks |
GET | /health/ready | getReadiness | Checks 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 allowlistedOriginuserBearerAuth— JWT access token of a signed-in user from an active identity environmentserviceBearerAuth— machine (client_credentials) JWT access token with scopes
Bearer token rules
- The token's
issselects 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(defaulthttp://localhost:8080). Request the token withresource=<audience>at the token endpoint, otherwise identity-api issues an opaque token. - ID tokens (with
nonce) are rejected. - Machine token: no
emailandsubmissing or equal toazp/client_id. Permissions come fromscope(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.