Skip to Content
We are live but in Staging 🎉
API ReferenceIssuer API

Issuer API

The public surface your app and its users talk to. Base URL — the pool issuer:

https://appid.dodil.io/{org}/{pool}

No platform auth anywhere here: endpoints are anonymous, credential-bearing, or pool-token-bearing as noted. JSON in/out unless stated; the token endpoint also accepts application/x-www-form-urlencoded for OAuth-library compatibility.

Discovery & keys

PathReturns
GET /.well-known/openid-configurationOIDC discovery — endpoints plus grant_types_supported: ["password","refresh_token","authorization_code"], response_types_supported: ["code"], code_challenge_methods_supported: ["S256"], id_token_signing_alg_values_supported: ["EdDSA"], token_endpoint_auth_methods_supported: ["none"]
GET /.well-known/jwks.jsonThe pool’s public keys (OKP / Ed25519), Cache-Control: max-age=300
GET /brandingPre-auth branding document for custom login UIs (includes the pool’s real password_min_length)

Clients are public (token_endpoint_auth_methods_supported: ["none"]) — there are no client secrets; PKCE is the code-flow proof.

Authorization-code flow (PKCE)

GET /authorize

{issuer}/authorize?response_type=code &client_id=my-web &redirect_uri=https://app.example.com/callback &state=<random> &code_challenge=<S256(verifier)> &code_challenge_method=S256 [&scope=openid] [&nonce=<random>] [&tenant=<slug>]
  • PKCE S256 is mandatory; the challenge must be 43–128 chars.
  • redirect_uri must exactly match one of the pool’s settings.redirect_uris — an unlisted URI gets an error page and never a redirect. Other errors redirect back with RFC 6749 error codes plus the RFC 9207 iss parameter.
  • The user lands on the hosted login page; the login transaction is valid for 30 minutes. An expired transaction sends the user back to your app with error=access_denied&error_description=login_session_expired — just start a fresh authorize.
  • scope=openid additionally mints an ID token at exchange (with aud = client_id, nonce echoed).
  • tenant=<slug> pins the resulting tokens to one tenant.

POST /token — code exchange

{ "grant_type": "authorization_code", "code": "…", "redirect_uri": "https://app.example.com/callback", "code_verifier": "…", "client_id": "my-web" }

Codes are single-use with a 60-second TTL and bound to the client_id that started the flow. Replaying a code revokes the session family it minted.

Direct grants

POST /token — password

{ "grant_type": "password", "email": "[email protected]", "password": "…", "tenant": "acme-uk" }

POST /token — refresh

{ "grant_type": "refresh_token", "refresh_token": "…" }

Successful grants return:

{ "access_token": "<EdDSA JWT>", "token_type": "bearer", "expires_in": 900, "refresh_token": "<opaque — new every time>", "id_token": "<code flow + openid only>" }

Refresh tokens rotate on every use; reusing an old one burns the whole family (with a small grace window for double-submits). Store only the newest.

Accounts

Method + pathBodyNotes
POST /signup{ "email", "password" }403 when signup is disabled; sends a verification link; returns a session unless verification is required first
POST /recover{ "email" }Always 200 — no user enumeration. Emails a reset link
POST /reset{ "token", "password" }Single-use token (30 min TTL); marks the email verified and revokes all sessions
POST /verify{ "token" }Email confirmation (link TTL 3 days)
POST /logout{ "refresh_token" }Revokes the session family
GET /user— (Bearer: pool access token){ "claims": {…}, "user": {…} }

The hosted pages live on the same base: GET /login, /reset, /verify render the branded UI that drives these endpoints.

Errors

Non-token endpoints: { "error": "<message>" }. The token endpoint speaks RFC 6749 §5.2:

{ "error": "invalid_grant", "error_description": "…" }
StatusMeaning
400Malformed request / invalid_request
401Bad credentials, bad or expired token
403Signup disabled, connection disabled, email not verified
404Unknown pool or user
409Conflict (e.g. email already registered)
429Rate limit exceeded — see budgets
503Pool user-store migration in its locked phase — retry shortly

CORS

Browser calls are allowed only from origins in the pool’s policies.cors_allowed_origins (exact scheme+host+port match). Allowed requests get Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS, Access-Control-Allow-Headers: authorization,content-type, and a 600-second preflight cache. There is no Allow-Credentials — auth is bearer-token, never cookies, so your frontend sends Authorization explicitly.