Advanced integration
OAuth API
Construct the same secure authorization flow yourself when your application needs complete control over transaction creation and interface behavior.
Base URL and environments
Every API path in this guide is relative to the deployed Sandbox base URL. Sandbox and Production credentials are isolated and cannot be used across environments.
| Surface | Sandbox URL | Purpose |
|---|---|---|
| API | https://api.dev.unifyid.io | Authorization, token exchange, UserInfo, capabilities, and webhooks. |
| Identity experience | https://dev.unifyid.io | Hosted authentication, identity assurance, and consent. |
| Developer portal | https://developer.dev.unifyid.io | Applications, credentials, scopes, redirect URIs, and webhook configuration. |
| Production | Provided after approval | Production domains and credentials are not available until the application passes Production review. |
Create an application in the Developer portal, register every callback URI exactly, select the maximum scopes and document policy, and create a client secret only for a backend capable of protecting it. HTTPS is required outside localhost; wildcards and URL fragments are rejected.
Integration sequence
- 1Create a server-side OAuth transaction
- 2Generate state, nonce, and S256 PKCE
- 3Redirect to /v1/oauth/authorize
- 4Validate the callback state
- 5Exchange the one-time code at /v1/oauth/token
- 6Validate the ID token
- 7Call /v1/userinfo
- 8Listen for signed lifecycle webhooks
1. Create the transaction
Your backend should create and store a short-lived, single-use transaction before redirecting the browser. Bind it to the initiating browser session and store only the values required to validate the callback.
{
"state": "{32_or_more_random_bytes_base64url}",
"nonce": "{32_or_more_random_bytes_base64url}",
"codeVerifier": "{43_to_128_character_pkce_verifier}",
"redirectUri": "https://your-app.example/oauth/callback",
"createdAt": "2026-07-31T12:00:00.000Z",
"consumed": false
}2. Authorization request
/v1/oauth/authorizeStarts Hosted UnifyID authentication, account-bound face assurance, and consent. It returns to the registered callback with either code and state, or an OAuth error and the original state.
| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Public Client ID issued to the application environment. |
redirect_uri | Yes | Must exactly match a registered callback URI. |
scope | Yes | Space-separated subset of scopes enabled for the application. |
state | Yes | Unique unpredictable value bound to the initiating browser transaction. |
nonce | With openid | Unique unpredictable value that must match the ID token nonce. |
code_challenge | Public clients; recommended for all | Base64url SHA-256 digest of the PKCE verifier. |
code_challenge_method | With PKCE | Must be S256. |
purpose | Recommended | Plain-language reason shown during consent. |
display | No | Set to popup only when authorization is opened in a popup window. |
GET https://api.dev.unifyid.io/v1/oauth/authorize
?response_type=code
&client_id={client_id}
&redirect_uri={exact_registered_redirect_uri}
&scope=openid%20profile%20email%20identity_verified
&state={unpredictable_state}
&nonce={unpredictable_nonce}
&code_challenge={base64url_sha256_verifier}
&code_challenge_method=S256
&purpose={plain_language_purpose}3. Validate the callback
Load the transaction from the same browser session, compare state using a timing-safe comparison, reject missing, expired, consumed, or mismatched transactions, and mark it consumed before exchanging the code. Authorization codes expire after five minutes and can be used only once.
// Successful callback
GET https://your-app.example/oauth/callback?code={one_time_code}&state={original_state}
// Denied or failed callback
GET https://your-app.example/oauth/callback?error=access_denied&error_description={description}&state={original_state}4. Token exchange
/v1/oauth/tokenConsumes the authorization code and returns a 15-minute access token. An ID token is included only when openid was approved. Refresh tokens are not issued.
Confidential clients should authenticate with HTTP Basic. Encode the Client ID and Client Secret as form components before joining them with a colon and Base64 encoding the result. client_secret_post is supported for compatibility. Public browser and mobile clients send no secret and must use S256 PKCE.
POST https://api.dev.unifyid.io/v1/oauth/token
Authorization: Basic {base64(urlencode(client_id) + ":" + urlencode(client_secret))}
Accept: application/json
Content-Type: application/json
{
"grant_type": "authorization_code",
"code": "{one_time_authorization_code}",
"redirect_uri": "https://your-app.example/oauth/callback",
"client_id": "{client_id}",
"code_verifier": "{original_pkce_verifier}"
}{
"access_token": "{access_token}",
"token_type": "Bearer",
"expires_in": 900,
"id_token": "{id_token_when_openid_was_granted}",
"scope": "openid profile email identity_verified"
}5. Validate the ID token
/.well-known/openid-configurationReturns the issuer, endpoints, supported scopes, S256 requirement, token authentication methods, and the JWKS URI.
/.well-known/jwks.jsonReturns the active RS256 public signing key. Cache keys according to response headers and refetch once when an unfamiliar kid is encountered.
Use a maintained OpenID Connect library. Require RS256; select the JWK matching the header kid; verify the signature; require the discovery issuer; require aud to equal your Client ID; validate exp, iat, and auth_time; compare nonce with the stored transaction; and use sub as the application account key. Allow no more than 60 seconds of clock skew.
6. Request approved information
/v1/userinfoSend the OAuth access token as a Bearer token. The response contains the pairwise sub and only claims approved for this authorization. Missing claims are omitted and must not be interpreted as false.
GET https://api.dev.unifyid.io/v1/userinfo
Authorization: Bearer {access_token}
Accept: application/jsonSupported scopes
openidprofileemailemailsphonephoneslegal_nameageage_over_18date_of_birthnationalityidentity_verifiedidentity_assurance_levelliveness_verifiedface_match_verifiedprofile_photoprofile_photo_verifiedidentity_documentsdocument_typedocument_issuing_countrydocument_expiry_datedocument_validitydocument_verifieddocument_verified_atdocument_number_maskeddocument_number_fullSee the Scopes reference for disclosure semantics, dependencies, sensitivity, attestation support, and document policy.
7. Identity documents
/v1/userinfo/identity-documentsRequires identity_documents. Returns the data.documents collection of active, verified, share-enabled documents that match application policy and consent. The collection is not currently paginated.
{
"data": {
"documents": [{
"documentReference": "docref_application_specific_value",
"displayName": "NG Passport",
"documentType": "passport",
"issuingCountry": "NG",
"validityStatus": "active",
"verified": true
}]
}
}/v1/userinfo/identity-documents/:documentReferenceReturns the approved fields for one opaque reference. A reference that is unknown, no longer policy-eligible, disabled, or no longer shareable returns 404 IDENTITY_DOCUMENT_NOT_FOUND. Missing identity_documents returns 403 insufficient_scope.
{
"data": {
"documentReference": "docref_application_specific_value",
"displayName": "NG Passport",
"documentType": "passport",
"issuingCountry": "NG",
"expiryDate": "2031-05-17",
"validityStatus": "active",
"verified": true,
"maskedDocumentNumber": "•••• 7778",
"proof": {
"type": "UnifyIDAttestation",
"attestationId": "attestation_public_reference",
"schemaVersion": "1.0",
"status": "issued",
"proofHash": "privacy_safe_audit_hash"
}
}
}proof is currently an UnifyID audit and attestation reference. It is not a client-verifiable signature over the JSON response. Do not recompute or independently trust proofHash; use the API authorization, response, and lifecycle status as the authoritative boundary until a public proof-verification specification is published.
Popup transport
Open a blank window synchronously from the click before waiting for your backend to create the transaction. The transaction endpoint returns only an authorization URL and transaction identifier; secrets and tokens never enter the opener page.
// Application backend
POST /api/auth/unifyid/transactions
Cookie: application_session={session}
// Response after storing state, nonce, verifier, callback, expiry, and consumed=false
{
"transactionId": "oauth_txn_opaque",
"authorizationUrl": "https://api.dev.unifyid.io/v1/oauth/authorize?...&display=popup"
}// Opener page
const popup = window.open(
"",
"unifyid_" + crypto.randomUUID(),
"popup=yes,width=540,height=760,resizable=yes,scrollbars=yes"
);
if (!popup) {
window.location.assign((await createTransaction({ display: "page" })).authorizationUrl);
} else {
const transaction = await createTransaction({ display: "popup" });
popup.location.replace(transaction.authorizationUrl);
const listener = event => {
if (event.origin !== "https://your-app.example") return;
if (event.source !== popup) return;
if (event.data?.type !== "unifyid:oauth:complete") return;
if (event.data.transactionId !== transaction.transactionId) return;
window.removeEventListener("message", listener);
popup.close();
window.location.assign("/dashboard");
};
window.addEventListener("message", listener);
}// Callback page after the backend validates state, exchanges the code,
// validates the ID token, and creates an HTTP-only application session.
window.opener?.postMessage(
{
type: "unifyid:oauth:complete",
transactionId: "{opaque_transaction_id}"
},
"https://your-app.example"
);
window.close();Capability discovery
/v1/capabilities/scopes/v1/capabilities/countries/v1/capabilities/countries/:countryCode/v1/capabilities/countries/:countryCode/document-types/:documentTypeSigned lifecycle webhooks
Supported events are oauth.authorization.completed, claim_access.completed, consent.granted, consent.revoked, and webhook.test. Each body uses { id, type, createdAt, data }.
unifyid-event-id: {event_id}
unifyid-event-type: consent.revoked
unifyid-signature: t={unix_seconds},v1={hex_hmac_sha256}
signed_payload = timestamp + "." + raw_request_body
expected = HMAC_SHA256(webhook_secret, signed_payload)Verify the signature against the raw body using a timing-safe comparison, reject timestamps outside five minutes, persist the event ID before side effects, ignore duplicates, and return a successful response only after durable acceptance. Delivery attempts occur immediately and then after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours, and 24 hours. Requests time out after five seconds. See the Webhook reference.
Errors and rate limits
The token endpoint returns OAuth errors as { "error": "invalid_grant", "error_description": "..." }. Other API endpoints use { "error": { "code": "...", "message": "...", "requestId": "..." } }. Authorization callbacks carry error, optional error_description, and the original state.
| Condition | Result | Integrator action |
|---|---|---|
| Invalid request, redirect, scope, PKCE, or reused code | 400 with invalid_request, invalid_scope, or invalid_grant | Correct the request; never retry unchanged. |
| Missing or invalid client authentication | 401 invalid_client and WWW-Authenticate | Check the environment and rotate the secret if necessary. |
| User denial or missing consent | access_denied or consent_required | Return the person to the application without creating a session. |
| Missing document scope | 403 insufficient_scope | Start a new authorization for required access. |
| Document reference no longer resolves | 404 IDENTITY_DOCUMENT_NOT_FOUND | Remove cached availability and enumerate documents again only if still authorized. |
| Rate limit exceeded | 429 RATE_LIMIT_EXCEEDED | Honor Retry-After; do not busy-retry. |
OAuth endpoints currently allow 30 requests per IP within 10 minutes by default. Read X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on every response. Limits may be adjusted by environment and plan.
Token lifecycle and revocation
Authorization codes expire after five minutes and are single-use. Access and ID tokens expire after 15 minutes. Refresh tokens are not issued. UnifyID does not currently expose a token revocation endpoint. When consent is revoked, expired, or disconnected, UserInfo and document access reject the authorization even if the signed token has not reached its cryptographic expiry. Stop processing immediately when an API rejects the token or a signed consent.revoked event arrives.
Sandbox completion
Before requesting Production access, test approval, denial, callback-state mismatch, code reuse, wrong PKCE verifier, missing claims, expired consent, document removal, duplicate webhooks, signature failure, rate limiting, popup blocking, and full-page redirect fallback. Sandbox does not permit document_number_full.