Skip to content

API reference

Webhooks

Every status change is POSTed as JSON and signed. Reply 2xx within 10 seconds. Use the event id to ignore retries.

Add HTTPS endpoints in Developers, or pass webhookUrl on a single session. Endpoint events are signed with that endpoint’s secret (whsec_…), shown once when you add it. A per-session webhookUrl is signed with the account’s per-session secret, which you can reveal and rotate in Developers. A rotation applies to the next send, including retries.

The delivery is stored before it is queued. A lost queue message delays the event. It does not drop it.

Request

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: VerifyMe-Webhooks/1.0
X-VerifyMe-Signature: t=1710000000,v1=hex…
X-VerifyMe-Delivery: 01J…
X-VerifyMe-Attempt: 1

X-VerifyMe-Signature is t=<unix seconds>,v1=<hex hmac-sha256 of "t.body">. The body is the raw request bytes, not a re-serialised object. Compare the hex with a constant-time check, and reject timestamps more than 5 minutes from now.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Body

{
  "id": "01JEVENT…",
  "type": "verification.approved",
  "createdAt": "2026-10-07T16:04:11.000Z",
  "data": {}
}

data is the full verification payload from Sessions, including decision, identity, checks, and your metadata. The event id stays the same across retries. X-VerifyMe-Delivery is the delivery row and changes if you resend from the portal.

Events

TypeWhen
verification.createdThe session was created.
verification.startedThe guest opened the link.
verification.face_capturedThe face capture was uploaded.
verification.document_capturedThe document photos were uploaded.
verification.processingAutomated checks started.
verification.retry_startedThe guest started another attempt after a retryable decline.
verification.needs_reviewNothing declined, but a person needs to decide. Status stays needs_review until then.
verification.approvedApproved, by the checks or by a person. decision.source says which.
verification.declinedDeclined, by the checks or by a person. decision.retryableStep is set when the guest can try again.
verification.decision_changedA person changed the outcome, including after it was final. Sent as well as approved or declined. If you granted access on an earlier approval, revoke it when a later event declines.
verification.cancelledYou cancelled the session.
verification.expiredThe guest did not finish within 24 hours.

Retries

Reply with any 2xx within 10 seconds. Redirects are not followed. Anything else is retried after 1, 5, 10, 30, 60, 120, 240, 480, 720, then 1440 minutes (11 attempts, then dead). Each attempt has a new timestamp and signature. A 410 Gone stops retries for that delivery.

Stored bodies are removed after 30 days. The delivery row stays 90 days, and a wiped body cannot be sent again. Owners and admins are emailed if a delivery dies, at most once a day per endpoint. You can retry or cancel a delivery from Developers.

The endpoint must be public HTTPS. Private addresses, link-local hosts, and credentials in the URL are rejected.