Integration Guide
Xident is a backend-first integration. Your server creates a verification session with its secret key, redirects the user to the Xident widget, and reads the result back. This guide covers the full flow and the options at each step. For a minimal walkthrough, start with the Quick Start.
The flow
- Create a session — your backend calls
POST /verify/v1/init(secret key) →verify_url. - Redirect the user's browser to
verify_url. - Callback — Xident redirects the user back to your
callback_urlwith?status&token&user_id. - Read the result — your backend calls
GET /verify/v1/result/{token}(secret key).
Security: Keep your secret key (sk_live_…) server-side only. Never trust the callback URL's status for authorization — always read the result server-side with GET /verify/v1/result/{token}.
Prerequisites
- A secret key (
sk_live_…) from your dashboard - A callback URL on your domain (HTTPS in production;
http://localhostallowed in development) - A backend route to create the session and another to handle the callback
Step 1: Create a verification session
Call POST /verify/v1/init from your backend:
curl https://api.xident.io/verify/v1/init \
-X POST \
-H "X-API-Key: sk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://yoursite.com/verified",
"min_age": 18,
"purpose": "age_verification",
"user_id": "your-user-123",
"theme": "system",
"locale": "en"
}'
The response gives you a one-time init token and the URL to send the user to:
{
"success": true,
"data": {
"token": "xit_9f8e7d6c5b4a...",
"verify_url": "https://verify.xident.io?t=xit_9f8e7d6c5b4a..."
}
}
Init options
| Field | Required | Description |
|---|---|---|
callback_url | Yes | Where Xident redirects the user afterward. HTTPS, or http://localhost in dev. |
min_age | Yes* | Age threshold to check: 1–99 (0–99 when purpose is id_verification). *Required for age verification. |
verification_mode | No | auto (default) · document · facial — see Verification mode. Composes with min_age. |
purpose | No | age_verification (default) or id_verification. Identity proofing; sets no age threshold. For age-gating with a forced document use verification_mode instead. |
expected | No | Identity data you already hold about the user, to be checked against the document: first_name, last_name, date_of_birth (YYYY-MM-DD), document_number, nationality (ISO alpha-2). Any subset. Needs a document: purpose: id_verification or verification_mode: document. Only verdicts come back — see Data match. |
mismatch_policy | No | report (default): mismatches are reported in the result, the outcome is unchanged. review: any mismatch sends the session to your review queue with reason data_mismatch. Only with expected. |
user_id | No | Your user identifier — echoed back on the callback as user_id |
theme | No | light, dark, or system |
locale | No | en, es, fr, de, pt, ar, zh, ja, hi, nl |
Verification mode
By default Xident chooses the verification path from the visitor's country
and your content category. verification_mode overrides that for a
single session, so you can apply a stricter path to some products without
changing anything account-wide.
| Mode | What runs | When to use it |
|---|---|---|
auto (default) | Whatever the rule engine resolves for the visitor's country and your content category | Almost all traffic. Cheapest path that still satisfies local law. |
document | Liveness → document capture → face match. No on-device age estimation. Requires a plan that includes document verification. | Age-restricted goods where you need the highest-assurance path regardless of how the on-device model would classify a borderline visitor. |
facial | Liveness → on-device age estimation. Never escalates to a document. | When you would rather fail a borderline visitor than ask for ID. |
It composes with min_age. Mode decides
how you verify; min_age decides what threshold
must be met. A store selling age-restricted products in the US might send
min_age: 21 with verification_mode: "document" for
those SKUs, and plain min_age: 18 on everything else — two calls
to the same endpoint, one API key.
This is the difference from purpose.
purpose: "id_verification" also forces a document, but it is
identity proofing and applies no age threshold. If you need both a
document and an age check, use verification_mode.
curl https://api.xident.io/verify/v1/init \
-X POST \
-H "X-API-Key: sk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://yoursite.com/verified",
"min_age": 21,
"verification_mode": "document",
"user_id": "your-user-123"
}'
facial and document both bypass the rule engine,
so you take on responsibility for the path being adequate in the visitor's
jurisdiction. auto keeps that with us.
An unrecognised value is rejected with
400 INVALID_VERIFICATION_MODE rather than silently falling back
to auto — a typo should fail your build, not quietly downgrade a
restricted checkout.
Step 2: Redirect the user
Redirect the user's browser to the verify_url from Step 1 (for example, an HTTP 302 from your backend route). The widget runs on verify.xident.io and handles liveness, age estimation, and document capture as required. The one-time init token (xit_…) is carried in the URL as ?t= and expires in 10 minutes.
Step 3: Handle the callback
When verification finishes, Xident redirects the user to your callback_url:
https://yoursite.com/verified?status=success&token=xtk_abc123&user_id=your-user-123
| Parameter | Description |
|---|---|
status | success, failed, or canceled |
token | Result token (xtk_…) — pass to the result endpoint |
user_id | Your user_id if you supplied one at init |
The callback only ever reports a terminal verdict (one of the three values above); GET /verify/v1/result/{token}'s own status field has a wider set covering the whole session lifecycle — see Core Concepts → Result Status Values.
Step 4: Read the result (server-side)
Fetch the authoritative result with your secret key:
curl https://api.xident.io/verify/v1/result/xtk_abc123 \
-H "X-API-Key: sk_live_your_secret_key"
data.verified is the one field to branch on for access control — a boolean, derived server-side from data.status, never trusted from the client. Right after a callback fires, data.status itself will normally read success, failed, or canceled, but see Core Concepts → Result Status Values for its full vocabulary and the API Reference for the full response schema.
Language SDKs
Server SDKs wrap these two calls (plus optional webhook verification). Use the one for your stack:
- Node.js
- Python
- Go
- PHP
- Mobile apps (Android / iOS) — the app opens
verify_url; init and result stay on your backend
Retries and idempotency
Send an Idempotency-Key header on
POST /verify/v1/verification-tokens/verify and a retry of the
same call will not be billed twice.
Idempotency-Key: 8f14e45f-ea6b-4f2c-b6d3-9c1a2b3c4d5e
Generate one value per logical call and reuse it across every retry of that call — a fresh value on each attempt defeats the purpose. Any string up to a reasonable length works; a UUID per request is the usual choice.
Why this is yours to send. Verifying a token is a read: it does not consume the token, so calling twice is two billable Checks. That is correct when you genuinely check a user twice, and wrong when your HTTP client retried after a timeout. Those are identical from our side. You know which one happened, so you tell us.
Without the header, billing is unchanged — one event per call. The header is optional and costs nothing to ignore.
When billing refuses a session (402)
POST /verify/v1/init can be refused for a billing reason
before any verification work starts. All of these answer HTTP
402, and the code field is what you branch on — the
status alone does not tell you what to do, because the three remedies are
different and two of them are not yours to apply.
| Code | What happened | How it clears |
|---|---|---|
ALLOWANCE_EXHAUSTED | The included monthly volume is used up on a plan that stops at its allowance. | Buy a top-up pack in the dashboard. Costs money. |
BUDGET_EXCEEDED | Spend reached the monthly budget the account owner set and asked us to enforce. Never happens unless someone switched it on. | Raise or remove the budget in billing settings, or wait for the next billing month. Costs nothing. |
PAYMENT_REQUIRED | The account is suspended for non-payment. | Update the payment method. |
Do not collapse these into one "payment problem" message. Telling a customer to buy a pack when their own budget cap is what stopped them makes them pay for a remedy that does not unblock them.
Which plans can hit ALLOWANCE_EXHAUSTED.
Starter, Growth and Scale are post-paid: they do not stop at the
allowance, they simply meter the extra usage at the plan's own rate and bill
it monthly. Basic and the free sandbox do stop. On a paid plan the only
ceiling is the account's own budget, and only if it was switched on.
A refusal never interrupts work already in progress. The stop refuses to start a new session; a session already running always finishes and returns a result. By the time usage is metered the document has already been processed and paid for, so abandoning it would burn the cost and deliver nothing.
The language SDKs below do not yet raise a dedicated exception type for
402 — it surfaces as the generic API error, with the code
available on the error object. Branch on the code.
Security Best Practices
- Use HTTPS — callback URLs must be HTTPS (except
http://localhostin development). - Read results server-side — never trust the callback
statusalone; always callGET /verify/v1/result/{token}with your secret key. - Keep the secret key server-side — never ship
sk_live_…to a browser or mobile app. A publishablepk_live_…exists for optional client-side widget embedding only. - Use
X-API-Key— Xident authenticates with theX-API-Keyheader, notAuthorization: Bearer. - Token expiration — the init token (
xit_…) expires in 10 minutes and is single-use.
Testing
- There is no sandbox mode and no test key — the same
sk_live_…secret key is used for testing and production, and never changes at go-live. http://localhost:*callback URLs are allowed (no HTTPS required).- Use a second project for staging: separate key pair, separate webhook endpoint and secret.
- To exercise a failure branch, send a high
min_age(e.g.99) — any real person then fails withage_below_threshold. Full matrix in Core Concepts.
Advanced: client-side embedding
If you prefer to trigger verification from the browser instead of redirecting from your backend, a publishable key (pk_live_…) can call POST /verify/v1/init client-side. This is optional and not required for the standard backend flow; results must still be read server-side with your secret key.
Next Steps
- Quick Start — minimal end-to-end example
- Core Concepts — the two-token model and verification paths
- API Reference — full generated API documentation