Overview
The Verisoar 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://www.verisoar.com
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.
/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 — any age from 1 to 120. Defaults to your organisation's configured threshold (18 unless changed). |
| jurisdiction | string | No | uk | eu | us | global. Affects compliance rules. Defaults to your organisation's data-residency setting. |
| redirectUrl | string (URL) | No | Where to send the user after completing the flow. |
| identifyMethods | string[] | No | Identification methods to offer: age_estimation and/or document_liveness. Multiple → the user picks one; single → it's used directly. Defaults to your organisation's configured methods. |
| fallbackIdentifyMethod | string | Conditional | Required when identifyMethods is supplied. The method an inconclusive method (e.g. age estimation) escalates to — it doesn't have to be one of identifyMethods, so you can offer age_estimation only and escalate to document_liveness. Defaults to your organisation's configured fallback. |
Methods & fallback. A quick age_estimation check (a camera-only estimate) verifies most users without an ID. If it can't confirm the user clears the age threshold, the session escalates to the fallbackIdentifyMethod (e.g. document_liveness). Each method run is billed as one usage, so an escalated verification counts as two. If the user is already on the fallback method, an inconclusive result fails the session and prompts a retry rather than looping.
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}
Embeddable widget
Load widget.js on your page to open the hosted flow in a pop-up overlay instead of a full-page redirect. Obtain hostedUrl from your server first — never call the API with your secret key in the browser.
<script src="https://www.verisoar.com/widget.js"></script><script>document.getElementById("verify-btn").addEventListener("click", async () => {const { hostedUrl } = await fetch("/your-api/start-verification", {method: "POST",}).then((r) => r.json());IdVerify.open({hostedUrl,onComplete: (result) => {// result.result: "approved" | "declined"// result.ageOverThreshold: booleanif (result.result === "approved") grantAccess();},onClose: () => {// User closed via ×, IdVerify.close(), or after the result screen},});});</script>
API
| Method | Description |
|---|---|
| IdVerify.open(opts) | Opens the pop-up. Returns { close } for the same session. Opening again closes any existing pop-up first. |
| IdVerify.close() | Closes the active pop-up, if any. Fires onClose. |
The iframe posts postMessage events with source: "idverify" — completed or failed on finish (followed by a brief result screen, then auto-close). For an inline embed without the pop-up, use the React IdVerifyWidget component.
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,"method": "age_estimation", // the method the user attempted first"escalated": false, // true if an inconclusive method escalated to the fallback"escalatedTo": null, // the method it escalated to, if any (e.g. "document_liveness")"failureReason": null, // see Failure reasons below"clientRef": "user_12345","createdAt": "2024-01-01T12:00:00Z","completedAt": "2024-01-01T12:05:00Z"}
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."}}