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

  1. Create a session — your backend calls POST /verify/v1/init (secret key) → verify_url.
  2. Redirect the user's browser to verify_url.
  3. Callback — Xident redirects the user back to your callback_url with ?status&token&user_id.
  4. 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://localhost allowed 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

FieldRequiredDescription
callback_urlYesWhere Xident redirects the user afterward. HTTPS, or http://localhost in dev.
min_ageYes*Age threshold to check: 1–99 (0–99 when purpose is id_verification). *Required for age verification.
verification_modeNoauto (default) · document · facial — see Verification mode. Composes with min_age.
purposeNoage_verification (default) or id_verification. Identity proofing; sets no age threshold. For age-gating with a forced document use verification_mode instead.
expectedNoIdentity 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_policyNoreport (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_idNoYour user identifier — echoed back on the callback as user_id
themeNolight, dark, or system
localeNoen, 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.

ModeWhat runsWhen 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
ParameterDescription
statussuccess, failed, or canceled
tokenResult token (xtk_…) — pass to the result endpoint
user_idYour 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:

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

  1. Use HTTPS — callback URLs must be HTTPS (except http://localhost in development).
  2. Read results server-side — never trust the callback status alone; always call GET /verify/v1/result/{token} with your secret key.
  3. Keep the secret key server-side — never ship sk_live_… to a browser or mobile app. A publishable pk_live_… exists for optional client-side widget embedding only.
  4. Use X-API-Key — Xident authenticates with the X-API-Key header, not Authorization: Bearer.
  5. 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 with age_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