Webhooks
Delivery, signature verification and retries, the source of truth for every result
Webhooks are how results reach you. Nothing else is authoritative: not the redirect the user lands on, not anything the client-side flow reports. Those are useful for driving your UI. The webhook is the statement of fact.
Registering an endpoint
Create an endpoint through /v1/webhooks or in the console. You give a URL and choose the events you
want. Each endpoint gets its own signing secret, prefixed whsec_, shown once at creation. Register
separate endpoints for sandbox and live so a test event can never be mistaken for a real one.
The payload
{
"id": "evt_9c2f1b7a",
"event": "verification.approved",
"createdAt": "2026-07-26T16:30:25.032Z",
"data": {
"sessionId": "vs_2f8a1c9b4e7d",
"verdict": "approve",
"status": "approved",
"reasonCodes": []
}
}id is the event id and your deduplication key. data.sessionId is the verification id and your
reconciliation key. The full event catalogue is in Webhook events.
Headers
| Header | Purpose |
|---|---|
Verifisere-Signature | t=<unix seconds>,v1=<hex hmac> over the timestamp and the raw body. |
Verifisere-Event | The event name, so you can route before parsing. |
Idempotency-Key | The event id, repeated for convenience. |
X-Payload-Digest | Hex HMAC-SHA256 of the raw body alone. |
X-Payload-Digest-Alg | Always HMAC_SHA256_HEX. |
Verifying the signature
Split the header on the comma to get t and v1. Build the signed payload by joining the timestamp
and the exact raw request body with a full stop. Compute HMAC-SHA256 with your endpoint secret, and
compare against v1 in constant time. Reject anything whose timestamp is more than five minutes
away from now, which stops replay of a captured request.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
// A request without the header is invalid, not an error worth a 500 and a retry.
if (typeof header !== "string") return false;
const parts = Object.fromEntries(
header.split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i), p.slice(i + 1)];
}),
);
const timestamp = Number.parseInt(parts.t, 10);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected, "hex");
const b = Buffer.from(parts.v1 ?? "", "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Verify against the raw bytes
Sign and compare the exact body we sent, before any JSON parsing and before any re-serialization.
Round-tripping through a JSON parser changes whitespace and key order, and the signature will
never match again. In Express, use express.raw() on the webhook route. In Next.js, read
await request.text().
Responding
Return a 2xx as soon as you have verified the signature and safely stored the event. Do the real
work afterwards, out of band. Anything other than a 2xx, or a response that takes too long, counts
as a failure and triggers a retry.
Retries
Failed deliveries are retried four times with backoff. Most events use a slow ladder measured in hours, since the state they carry is not time-critical. Review actions use a fast ladder measured in seconds and minutes, because a person is waiting on the other end. After the last attempt the delivery is marked failed and you can resend it from the console.
Idempotency
Assume every event can arrive more than once, both because of retries and because a verification's final state can legitimately change after a manual review concludes.
Deduplicate on the event id, and make the underlying state transition safe to repeat, keyed on the
verification id. Ordering is not guaranteed either, so treat the state in the payload as the current
truth rather than assuming events arrive in the order they were created.
Testing
Sandbox verifications fire the same events to the same endpoints with the same signature scheme. You can also resend any past delivery from the console, which is the quickest way to reproduce a bug against a real payload.