How the flow works
authorize → consent → code → token → userinfo, and what each step is actually responsible for.
Five steps
- 1
authorize
Send the person to GET /v1/oauth/authorize. It redirects to the hosted flow and forwards explicit prompt, max_age and login_hint values. With no prompt, a sufficient existing session and unchanged permissions continue automatically; otherwise authentication or consent is shown.
- 2
consent
UnifyID signs the person in. The first time, it shows them the concrete release — the claim names, each identity document by name, how many contacts leave — with your
purposeabove it, and approving is a single decision. Someone already connected sees *Continue as* instead, or only the permissions you added. Cancelling returnsaccess_denied. - 3
code
An authorization code arrives at your registered
redirect_uri, with thestateyou sent.response_types_supportedis["code"]: there is no other response type. - 4
token
POST /v1/oauth/tokenexchanges the code, with your PKCE verifier (required of every client) and your client authentication, for an access token and — whenopenidwas granted — an RS256 ID token that says whether this was the person's first sign-in to you. - 5
userinfo
GET /v1/userinforeturns a flat map of the claim values the person approved and the platform holds.
The discovery document is the contract
Do not hard-code endpoints from a page you read once. GET /.well-known/openid-configuration returns issuer, authorization_endpoint, token_endpoint, userinfo_endpoint and jwks_uri, and your application's quickstart returns the same set under endpoints.
{
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"subject_types_supported": ["pairwise"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": [
"client_secret_basic", "client_secret_post", "none"
],
"code_challenge_methods_supported": ["S256"],
"prompt_values_supported": ["none", "login", "consent", "select_account"]
}The JWKS answers with RSA keys carrying kid, use: "sig" and alg: "RS256". Select by kid, and re-fetch when you meet one you do not know.
Your one sentence
purpose is the only text you write that the person reads at the moment they decide. It is one string, at most 200 characters, stored on the consent record. If you send nothing, the screen says Sign in to <your application name>.
There is no per-claim reason field. You cannot annotate date_of_birth with why you want it on the screen. One request, one sentence.
First sign-in or returning
The first approval creates a connection. Later sign-ins reuse the active consent unless more permissions are requested. With a sufficient session and unchanged permissions, continuation is automatic. Authentication is requested when needed, and each completed sign-in is recorded.
What the person sees
first time- The whole consent: the concrete release with your
purposeabove it. returning, nothing new- Continue automatically when the session is sufficient. Explicit requests to authenticate, select an account or show consent are respected.
returning, more asked for- Only the permissions you added, with what they already share named beside them. Approving adds; it never removes a scope they approved before.
returning, less asked for- Treated as nothing new. The grant stays as it is; the token carries the scopes you asked for, intersected with it.
How you tell them apart
first_sign_inID tokentrueon the sign-in that created the connection,falseon every later one. A person who disconnected you and connects again istrueagain.consent_idID token- The consent backing this sign-in. Bind your application session to it so a delayed revocation of an older consent cannot end a newer connection.
connected_atID token- ISO 8601: when the connection was made, from the person's first approval, however many times what they share changed since.
sign_in.completedwebhook- Every completed sign-in, with
data.firstSignIn. See Revocation and webhooks. connection.createdwebhook- Sent when a connection is created, including reconnection after withdrawal.
prompt, max_age and login_hint
Three OpenID Connect parameters authorize reads. Anything else you send is forwarded and ignored.
no promptdefault- Reuse a sufficient session and active consent. Ask for authentication when needed and consent for new permissions.
prompt=login- Explicitly request fresh authentication even when a UnifyID session exists.
prompt=select_account- The sign-in page opens on *Use another account*, so somebody sharing a device can switch.
prompt=consent- The whole consent is shown again, even to somebody already connected.
prompt=none- No screen at all. A person who is signed in with a passkey, already connected with every scope you asked for and — if you set
max_age— recent enough gets a code. Everyone else is sent back to yourredirect_uriwitherror=login_required,consent_requiredorinteraction_requiredand yourstate.nonecannot be combined with another value. max_age- Seconds. With
prompt=none, the person's last proof (auth_time) must be newer than this or you getlogin_required.auth_timeis the most recent of their sign-in, a passkey re-check and the confirmation an application sign-in asks for. login_hint- The address you believe the person uses. It prefills the sign-in form; it decides nothing.
A private sign-in application cannot send max_age, which is refused with invalid_request, and its login_hint is dropped. See Private sign-in.
Subjects are pairwise
subject_types_supported is ["pairwise"]. The sub is derived per client id. Your staging application and your production application see different sub values for the same human being, and so do two products you own.
Key your user records on it anyway — just do not treat it as a UnifyID-wide person id, and do not try to join two applications on it.
A private sign-in application's sub is per client id too, and never equal to a pairwise one. It belongs to the person rather than to their UnifyID account: in production it stays the same after they delete their account and register again with the same identity document, where a standard application's sub would be new. See Private sign-in.
What this does not do
- Automatic continuation reuses approval. It requires an adequate session and existing permission for every requested scope. A first connection, withdrawn permission or additional scope requires consent. Explicit prompt values are respected.
- No refresh grant is advertised. The discovery document lists
authorization_codealone. - The token is bound to the consent, not just to the clock. Read Revocation and webhooks before you decide how long to cache a claim.
Verified against API commit 234dba22 on 2026-10-18. The routes, scopes, checklist, events and limits on these pages are read from that export, not typed.