verisoar-logoVerisoar← Dashboard

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.

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 — any age from 1 to 120. Defaults to your organisation's configured threshold (18 unless changed).
jurisdictionstringNouk | eu | us | global. Affects compliance rules. Defaults to your organisation's data-residency setting.
redirectUrlstring (URL)NoWhere to send the user after completing the flow.
identifyMethodsstring[]NoIdentification 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.
fallbackIdentifyMethodstringConditionalRequired 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: boolean
if (result.result === "approved") grantAccess();
},
onClose: () => {
// User closed via ×, IdVerify.close(), or after the result screen
},
});
});
</script>

API

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

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,
"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.

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."
}
}