API Reference

One host, two APIs. Gatekeeper signs your users in (OAuth 2.0 / OIDC). Aethelos Link verifies who a customer is (Ghana Card) and how they spend (Mobile Money), with their consent. Both return JSON and speak the same error format.

Getting started

The base URL for every example on this page:

https://aethelos-gateway.vercel.app

Register an application in the Gateway dashboard to get a client_id and client_secret. There you also pick which grants and scopes your client may use. Confidential clients keep their secret server-side; public clients (SPAs, mobile apps) use PKCE and never hold a secret.

Every endpoint is also listed in the discovery document, so you can wire clients up dynamically:

curl https://aethelos-gateway.vercel.app/.well-known/openid-configuration

Errors

All endpoints return errors in one shape, with an appropriate HTTP status:

{
  "error": "invalid_grant",
  "error_description": "Authorization code expired"
}
FieldTypeDescription
invalid_request400A parameter is missing or malformed.
invalid_client401Client credentials are wrong or the client is inactive.
invalid_token401Access token is missing, expired, or fails verification.
insufficient_scope403Token is valid but lacks the scope this endpoint needs.
invalid_grant400Code or refresh token is expired, already used, or mismatched.
invalid_scope400Requested scope is not allowed for this client.
unauthorized_client400Grant type is not enabled for this client.
unsupported_grant_type400Unknown grant_type value.
authorization_pending400Device flow: the user hasn't approved yet. Keep polling.
expired_token400Device flow: the code expired before approval.
consent_required403Link: no active user consent for the requested scope.
access_denied403The user denied the request, or the account is not active.
not_found404No such resource for this client.
invalid_state409The resource exists but is in the wrong state (e.g. revoking an ungranted consent).

Gatekeeper — sign-in (OAuth 2.0 / OIDC)

Gatekeeper implements the standard authorization code flow with PKCE. Your app sends the browser to Gateway, the user signs in (password, MFA if enforced), and Gateway redirects back with a one-time code you exchange for tokens.

1. Authorize

GET/oauth/authorize

Send the user's browser here. Browser redirect, not a fetch call.

FieldTypeDescription
client_id *stringYour client ID.
redirect_uri *stringMust exactly match a URI registered on your client.
response_type *stringOnly code is supported.
scopestringSpace-separated. Default openid. See scopes below.
statestringOpaque value echoed back. Use it for CSRF protection.
noncestringPassed through to the ID token to bind it to your session.
code_challengestringS256 hash of your PKCE verifier. Required when your client enforces PKCE.
code_challenge_methodstringS256 (default) or plain.

Example:

GET https://aethelos-gateway.vercel.app/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://app.example.com/callback
  &scope=openid%20profile%20offline_access
  &state=9f3k2l
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

If the user isn't signed in, Gateway shows its login page first. After sign-in and consent, the browser lands on your redirect_uri with ?code=…&state=…. If the user denies, you get ?error=access_denied instead.

Authorization codes expire after 10 minutes and work exactly once. Replaying a code revokes every token previously issued from it.

2. Exchange the code for tokens

POST/oauth/token

Form-encoded body. Authenticate with HTTP Basic (client_id:client_secret) or put both in the form. The response always has Cache-Control: no-store.

curl -u "$CLIENT_ID:$CLIENT_SECRET" https://aethelos-gateway.vercel.app/oauth/token \
  -d grant_type=authorization_code \
  -d code=CODE_FROM_REDIRECT \
  -d redirect_uri=https://app.example.com/callback \
  -d code_verifier=YOUR_PKCE_VERIFIER
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid profile offline_access",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "ZT0xQ9wR7p..."
}
FieldTypeDescription
access_tokenstringRS256 JWT (typ at+jwt). Carries sub, scope, and your client's roles claim.
id_tokenstringOIDC ID token with email, name, email_verified, and your nonce.
expires_innumberAccess token lifetime in seconds. Per client, default 3600.
refresh_tokenstringOnly returned when the original scope included offline_access.

Supported grant_type values:

FieldTypeDescription
authorization_codegrantThe flow above. Requires code, redirect_uri, and code_verifier when PKCE is on.
refresh_tokengrantSend the current refresh_token; get a new access token and a rotated refresh token. Scopes can be narrowed, never widened. Each refresh token works once — reuse revokes the whole token family (rotation theft protection).
client_credentialsgrantService-to-service tokens, no user involved. Confidential clients only.
urn:ietf:params:oauth:grant-type:device_codegrantDevice flow, see below.

Userinfo

GET/oauth/userinfo

curl https://aethelos-gateway.vercel.app/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"
{
  "sub": "9b2f...",
  "email": "kwame@example.com",
  "email_verified": true,
  "name": "Kwame Asante",
  "phone_number": "+233244123456",
  "picture": null,
  "roles": ["user"]
}

Introspect

POST/oauth/introspect

Check whether a token is still valid before trusting it. Takes token and optionally token_type_hint=refresh_token. Client authentication required.

{
  "active": true,
  "sub": "9b2f...",
  "iss": "https://aethelos-gateway.vercel.app",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1790000000,
  "scope": "openid profile",
  "token_type": "Bearer"
}

Inactive, revoked, or foreign tokens return { "active": false }.

Revoke

POST/oauth/revoke

Form body: token (and optionally token_type_hint=refresh_token). Revoking a refresh token kills its entire family, so a stolen rotation chain dies with one call. Per RFC 7009 the endpoint returns 200 even when the token was already invalid.

Device flow

For TVs, CLIs, and other input-constrained devices. Request a code, show the user code, poll for tokens.

POST/oauth/device/code

curl -u "$CLIENT_ID:$CLIENT_SECRET" https://aethelos-gateway.vercel.app/oauth/device/code \
  -d scope=openid
{
  "device_code": "xR7...",
  "user_code": "BXFG-KMTR",
  "verification_uri": "https://aethelos-gateway.vercel.app/device",
  "verification_uri_complete": "https://aethelos-gateway.vercel.app/device?user_code=BXFG-KMTR",
  "expires_in": 900,
  "interval": 5
}

Poll /oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code and the device_code, waiting interval seconds between calls. You'll get authorization_pending until the user approves, then the usual token response — or access_denied if they decline.

On their side, the user opens https://aethelos-gateway.vercel.app/device, signs in, enters the code, and approves. Point them at verification_uri_complete when you can so the code is filled in for them.

Device codes typed from a source you don't control hand your account to whoever is asking — only approve codes you requested yourself.

End session

GET/oauth/end-session

Signs the user out of Gateway (SSO included) and ends their OAuth sessions. Send the browser here with post_logout_redirect_uri — it must be a registered redirect URI of an active client, given as client_id. Without a valid redirect, the user lands on the Gateway login page.

JWKS

GET/oauth/jwks

The RS256 public key, so you can verify access and ID tokens locally instead of calling introspect. Access tokens have typ: at+jwt; ID tokens carry your nonce and email claims. Cached for one hour.