Overview
The Verify API lets you check that a real, live person matches their identity document and is over a specified age — without storing images or biometrics. You create a session on your server, send the user to the hosted URL, and receive the result via webhook.
Base URL
https://your-app.vercel.app
Auth
Bearer token (API key)
Format
JSON request + response
Authentication
All customer API endpoints require an API key in the Authorization header. Create and manage keys from the API keys tab.
Authorization: Bearer idv_test_YOUR_KEY_HERETest keys (idv_test_…) work exactly like live keys but don't charge usage. Switch to a live key (idv_live_…) before going to production.
Create a verification session
Creates a session and returns a hosted URL for the end user to complete the check. Call this from your server — never from the browser.
/api/v1/verification-sessionsRequest body
| Field | Type | Required | Description |
|---|---|---|---|
| clientRef | string | No | Your opaque identifier for this user (e.g. your DB user ID). Returned in the webhook. |
| ageThreshold | integer | No | Minimum age to pass. Default: 18. Range: 13–25. |
| jurisdiction | string | No | uk | eu | us | global. Affects compliance rules. Default: global. |
| redirectUrl | string (URL) | No | Where to send the user after completing the flow. |
Response
{
"id": "vs_abc123",
"status": "pending",
"sessionToken": "XYBF...", // internal — the client uses it via the hostedUrl fragment
"hostedUrl": "https://…/verify/vs_abc123#XYBF...", // send the user HERE
"ageThreshold": 18,
"expiresAt": "2024-01-01T12:30:00Z" // session expires after 30 minutes
}Get a session
Look up the status and result of any session. Use this to poll for a result if you're not using webhooks.
/api/v1/verification-sessions/:id{
"id": "vs_abc123",
"status": "completed", // pending | consented | processing | completed | failed | expired
"result": "approved", // "approved" | "declined" | null (not yet complete)
"ageOverThreshold": true, // true if user passed the age check
"ageThreshold": 18,
"failureReason": null, // see Failure reasons below
"clientRef": "user_12345",
"createdAt": "2024-01-01T12:00:00Z",
"completedAt": "2024-01-01T12:05:00Z"
}End-user flow API
These endpoints are called by the hosted verification page on behalf of the end user using a short-lived session token. You don't need to call them directly — the hosted flow handles this automatically.
Get session status (end-user)
/api/verify/:id/statusx-session-token: <token from the URL fragment>Record consent
/api/verify/:id/consent{ "agreed": true }Must be called before submitting. Records an explicit Art. 9 GDPR consent receipt.
Submit verification
/api/verify/:id/submitSubmits the face embeddings, liveness signals, and document signals collected by the in-browser biometric engine. All biometric data is processed in memory and discarded — only the coded outcome is persisted.
Webhooks
We POST a verification.completed event to each of your configured endpoints when a session finishes. Set up endpoints from the Webhooks tab.
Payload
{
"type": "verification.completed",
"data": {
"id": "vs_abc123",
"clientRef": "user_12345",
"result": "approved",
"ageOverThreshold": true,
"ageThreshold": 18,
"failureReason": null,
"completedAt": "2024-01-01T12:05:00Z"
},
"createdAt": "2024-01-01T12:05:00Z"
}Verifying signatures
Every delivery includes an X-IdVerif-Signature header. Always verify it before trusting the payload.
import { createHmac } from "crypto";
function verifyWebhookSignature(rawBody, signature, secret) {
const [tPart, v1Part] = signature.split(",");
const timestamp = tPart.replace("t=", "");
const received = v1Part.replace("v1=", "");
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
if (expected !== received) {
throw new Error("Webhook signature mismatch — reject this request");
}
return JSON.parse(rawBody);
}
// Express example:
app.post("/webhooks/verify", express.raw({ type: "*/*" }), (req, res) => {
const event = verifyWebhookSignature(
req.body.toString(),
req.headers["x-idverif-signature"],
process.env.VERIFY_WEBHOOK_SECRET, // the whsec_... from the dashboard
);
if (event.data.result === "approved") {
await db.users.update({ where: { id: event.data.clientRef }, data: { ageVerified: true } });
}
res.sendStatus(200);
});Data-subject requests (GDPR)
Because we store almost nothing, erasure removes the complete record we hold for a subject. Identify subjects by the clientRef you provided when creating the session.
/api/v1/data-requests| type | Effect |
|---|---|
| access | Returns all minimal records held for this clientRef (result, timestamp — no biometrics). |
| erasure | Deletes all records for this clientRef immediately. |
// Access
{ "type": "access", "subjectRef": "user_12345" }
→ { "subjectRef": "user_12345", "records": [{ "id": "vs_...", "result": "approved", ... }] }
// Erasure
{ "type": "erasure", "subjectRef": "user_12345" }
→ { "subjectRef": "user_12345", "erased": 1 }Failure reasons
When result is "declined", failureReason contains one of:
| reason | Meaning |
|---|---|
| under_age | The document DOB shows the user is below the age threshold. |
| face_mismatch | The live face did not match the document photo with sufficient confidence. |
| liveness_failed | The anti-liveness score was too low (possible spoof or poor lighting). |
| spoof_detected | Presentation attack detected — photo, mask, or replay attempt. |
| sequence_failed | Head-pose sequence did not match the challenge (wrong order or direction). |
| document_invalid | MRZ check-digit validation failed or document could not be parsed. |
| document_expired | The document's expiry date has passed. |
| timeout | The session was not completed within 30 minutes. |
| user_abandoned | User closed the flow before completing. |
| quota_exceeded | Monthly verification quota reached — upgrade your plan. |
| error | Unexpected internal error — retry the session. |
Error responses
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or invalid request body. |
| 401 | unauthorized | API key missing, invalid, or revoked. |
| 402 | quota_exceeded | Monthly scan quota reached. |
| 404 | not_found | Session ID not found or belongs to another org. |
| 409 | invalid_state | Session is in the wrong state for this action. |
| 429 | rate_limited | Too many requests — back off and retry. |
{
"error": {
"code": "unauthorized",
"message": "Invalid or missing API key."
}
}