Verify← Dashboard

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_HERE

Test 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.

POST/api/v1/verification-sessions

Request body

FieldTypeRequiredDescription
clientRefstringNoYour opaque identifier for this user (e.g. your DB user ID). Returned in the webhook.
ageThresholdintegerNoMinimum age to pass. Default: 18. Range: 13–25.
jurisdictionstringNouk | eu | us | global. Affects compliance rules. Default: global.
redirectUrlstring (URL)NoWhere 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.

GET/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)

GET/api/verify/:id/status
x-session-token: <token from the URL fragment>

Record consent

POST/api/verify/:id/consent
{ "agreed": true }

Must be called before submitting. Records an explicit Art. 9 GDPR consent receipt.

Submit verification

POST/api/verify/:id/submit

Submits 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.

POST/api/v1/data-requests
typeEffect
accessReturns all minimal records held for this clientRef (result, timestamp — no biometrics).
erasureDeletes 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:

reasonMeaning
under_ageThe document DOB shows the user is below the age threshold.
face_mismatchThe live face did not match the document photo with sufficient confidence.
liveness_failedThe anti-liveness score was too low (possible spoof or poor lighting).
spoof_detectedPresentation attack detected — photo, mask, or replay attempt.
sequence_failedHead-pose sequence did not match the challenge (wrong order or direction).
document_invalidMRZ check-digit validation failed or document could not be parsed.
document_expiredThe document's expiry date has passed.
timeoutThe session was not completed within 30 minutes.
user_abandonedUser closed the flow before completing.
quota_exceededMonthly verification quota reached — upgrade your plan.
errorUnexpected internal error — retry the session.

Error responses

StatuscodeMeaning
400invalid_requestMissing or invalid request body.
401unauthorizedAPI key missing, invalid, or revoked.
402quota_exceededMonthly scan quota reached.
404not_foundSession ID not found or belongs to another org.
409invalid_stateSession is in the wrong state for this action.
429rate_limitedToo many requests — back off and retry.
{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key."
  }
}