La documentación se publica en inglés.

DocumentationSign in

Handling the callback

state, nonce, PKCE — and the one rule that catches everybody: absent is not false.

Three checks before you trust anything

stateyou check
Compare against the value you stored server-side for this browser. At least 32 characters of cryptographic randomness, and it must match the authorization URL exactly.
nonceyou check
Compare against the ID token's nonce claim after verifying the signature. A token without your nonce is not yours.
code_verifierthe server checks
S256 only, and required of every client — a client secret does not stand in for it. Send the verifier that produced the challenge you sent to authorize. A code presented twice is refused, and the tokens it already bought are ended.

Also verify the ID token's iss against the discovery issuer, its aud against your client id, its signature against the JWKS at jwks_uri, and its expiry. claims_supported on the discovery document lists sub, aud, iss, exp, iat, nonce, auth_time, acr, amr, identity_verified, consent_id, first_sign_in and connected_at.

The ID token's own claims

auth_time
Unix seconds. The most recent moment the person proved themselves in their UnifyID session: the sign-in, a passkey re-check, or the confirmation the sign-in to your application asked for.
amr, acr
How they proved it (fido2, pwd, otp, recovery_code) and urn:unifyid:assurance:standard for a passkey, …:basic otherwise.
identity_verified
Always true: only a verified person can reach your callback.
first_sign_in
true when this sign-in created the person's connection to your application; false when they were already connected. Greet them accordingly.
connected_at
ISO 8601. When the connection was made — the person's first approval of your application, not the latest change to what they share.
A returning person's ID token payload
{
  "iss": "https://api.unifyid.example",
  "sub": "sub_7Xk…",
  "aud": "vid_live_example",
  "nonce": "…",
  "auth_time": 1788868800,
  "amr": ["fido2"],
  "acr": "urn:unifyid:assurance:standard",
  "identity_verified": true,
  "first_sign_in": false,
  "connected_at": "2026-08-01T09:00:00.000Z",
  "iat": 1788868810,
  "exp": 1788869710
}

A private sign-in application's ID token has no auth_time, amr or acr. See What the application receives.

The denial path

Cancel on the consent screen is RFC 6749 access_denied at your redirect URI. So is a sign-in attempt by a person whose identity verification failed, is under review, or whose account is restricted — which is exactly why three of the four test-identity scenarios refuse.

Your callback must therefore handle an error parameter as a first-class outcome, not as an exception. It is not rare and it does not mean anything is broken.

Absent is not false

This is the single most important sentence on the page. Userinfo builds a flat map, adding each claim only if the scope was granted and only if the underlying value exists. There is no null. There is no "declined" marker. There is no per-claim status.

So these two situations produce byte-identical payloads:

  • The person did not approve nationality.
  • The person approved nationality and there is no nationality record on their account.

And "nationality" in person === false tells you nothing about the person’s nationality. It tells you the platform did not release a value.

Reading userinfo without lying to yourself
# The absence of a key is the signal. There is nothing else to look for.
curl -s "$UNIFYID_API/v1/userinfo" \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  | jq 'has("age_over_18"), .age_over_18'

The claim names you will actually see

Userinfo uses OIDC names where OIDC has them, so the key is not always the scope code:

Scope grantedKey in the userinfo payload
alwayssub
profilename, given_name, family_name
legal_namelegal_name
date_of_birthdate_of_birth (YYYY-MM-DD)
ageage
age_over_18age_over_18
nationalitynationality
identity_verifiedidentity_verified
identity_assurance_levelidentity_assurance_level (level_<n>)
emailemail
phonephone_number
profile_photopicture (a URL that needs the same access token)

Contacts are verified contacts only, and one of each: email releases the primary verified address and phone the primary verified number. An unverified address on the account is not a candidate, and there is no scope that releases every address an account has.

What this does not do

  • There is no per-claim metadata. No verification timestamp, no method, no retention hint, no confidence. The only verification timestamp anywhere is document_verified_at, and only for documents, through Identity documents.
  • Every read is a disclosure. Each userinfo answer is recorded as a disclosure to your application in the same transaction that reads the data. Polling userinfo is not free and not invisible.
  • Do not cache a claim as settled fact. The consent behind it can end at any moment, and you will find out at the next 401 — or through a webhook, if you subscribed to one.

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.