Guides Handling results

Handling Responses & Error Cases

Your callback code has two decision points, and it helps to think about them separately. First, the browser arrives back at your redirect_uri — either with an authorization code or with an error. Only if you get a code do you exchange it and call /userinfo, which is where the age result appears.

Errors can surface at either point, and they mean different things.

Before you exchange anything

Once state checks out, look for an error parameter. If one is present there is no code to exchange, and the flow ends here.

OutcomeRedirect containsAction
User cancellederror=access_deniedDo not grant access. Show something like “Verification was cancelled.”
Verification unsuccessfulerror=access_deniedDo not grant access. You may offer a retry via a new authorization request.
No error parametercode=… and state=…Continue to the token exchange.

Verification is unsuccessful when the user attempted it but it did not complete — a rejected document, a failed liveness check, or an expired session.

https://yourapp.com/callback?error=access_denied&error_description=The+user+denied+the+request&state=YOUR_ORIGINAL_STATE

Both outcomes return the same error value. For most integrations that is fine — the correct action is identical, so handle them together and deny access. If you need to distinguish them, see the note at the end of this page.

After you call /userinfo

You have a code, you exchanged it successfully, and you have called /userinfo. The response tells you the result.

Verified

{
  "sub": 89,
  "age_verified": true,
  "expires_at": 1765035995,
  "metadata": "order:XYZ-42"
}

Not verified

The user completed the flow and did not pass.

{
  "sub": 110,
  "age_verified": false
}

Do not grant access. Treat them as an unverified user. Note that expires_at and metadata are absent here — code that reads them unconditionally will break, so guard your field access.

Exempt region

Users in a Safezone or otherwise exempt region return the same shape as a verified user.

{
  "sub": 88,
  "age_verified": true,
  "expires_at": 1765035995
}

Treat this as a success and grant access exactly as you would for a verified user.

Everything in one table

WhereSignalGrant access?
CallbackAn error parameter is presentNo
Token exchangeHTTP 400 with an error fieldNo — see Error reference
/userinfoage_verified: trueYes
/userinfoage_verified: falseNo
/userinfoHTTP 401 or 5xxNo — and treat as an integration fault, not a failed user

Distinguishing cancellation from failed verification

Last updated July 28, 2026

Was this page helpful?