Skip to main content

Webhooks

A webhook is how you learn the outcome of a verification. When a verification reaches a verdict — automatically, or because a human reviewer decided it — Veridia POSTs a signed JSON body to the URL you configured.

This is the only push-based way to get a verdict. The widget's veridia:complete event tells you the user finished submitting; it does not carry the verdict. GET /v1/verify/{id} carries it but requires a secret key and requires you to poll.

The three events

There are exactly three event types. There is no verification.created, no verification.expired, and no way to subscribe to a subset — a tenant receives all three or none.

typeverdictMeaning
verification.approvedapprovedCleared. Safe to onboard.
verification.rejectedrejectedFailed. Do not onboard.
verification.review_requiredreviewA human has to look at it. Not a transient state — no further event arrives until a reviewer decides.

When a reviewer later decides a review case, you receive a second event — verification.approved or verification.rejected — for the same verificationId, with a new id. That second event is the one that unblocks the user. Handlers that collapse both events into one dedup key silently discard the human decision; see Idempotency.

A complete payload

The body is flat. There is no data envelope.

{
"id": "evt_9f2c41ab7d8e4c05b6a3e17f2d904c8b",
"type": "verification.approved",
"createdAt": 1753142348,
"tenantId": "tn_default_demo",
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"verdict": "approved",
"confidence": 93.1,
"userRef": "customer-12345",
"scores": {
"ocr_confidence": 78.0,
"face_match": 96.2,
"liveness": 91.5,
"doc_quality": 85.0,
"mrz_valid": 100.0,
"name_match": 88.0
},
"flags": [
{ "level": "ok", "text": "auto_approved_all_checks_passed" },
{ "level": "ok", "text": "mrz_checksums_valid" }
],
"fieldsExtracted": {
"full_name": "MARIA ELENA GONZALEZ",
"document_number": "4567890",
"date_of_birth": "1991-04-17",
"nationality": "PRY",
"document_type": "dni"
},
"latencyMs": 3184
}

Full field-by-field reference: Event types.

Three things that break integrations

These are worth reading before you write the handler, because each one fails silently.

1. The discriminator is type, not event

switch (payload.type) { /* ... */ } // correct
switch (payload.event) { /* ... */ } // always undefined → falls to default

There is no event key in the body. A switch on it matches nothing, your handler returns 200 OK, and no verdict is ever applied. Approved users stay pending forever and rejected users do too. Nothing appears in your error logs, because nothing errored.

2. Deduplicate by the event id

Delivery is at least once. A handler that processed an event successfully but was slow to respond will receive it again. You need a dedup key, and the correct one is id — the evt_* value, which is stable across every retry of the same event and unique across different events.

const dedupKey = payload.id; // correct
const dedupKey = `${payload.verificationId}:${payload.type}`; // WRONG

The second form looks reasonable and is the trap. A verification that goes to review_required and is later approved by a reviewer produces two events with different type values, so that key survives — but any key built from verificationId alone, or from a field that evaluates to undefined, collapses the human decision into the machine's earlier event and discards it. Use id. It is also available as the Veridia-Event-Id header, so you can dedupe before parsing the body.

3. scores keys are snake_case, and liveness can be null

payload.scores.face_match // 96.2
payload.scores.faceMatch // undefined

undefined < 70 is false in JavaScript, so a threshold written against the camelCase spelling never fires — it fails open, admitting everyone. And scores.liveness is null when there was no liveness signal or the model errored, so arithmetic on it without a null check throws in production.

fieldsExtracted is personal data

fieldsExtracted carries identity PII: full name, document number, and date of birth, plus nationality and document type. It is the transcribed content of a government ID.

Two consequences:

  • Your endpoint must be HTTPS. The panel refuses to save a URL that does not start with https://, precisely because this body would otherwise cross the network in the clear. There is no HTTP exception, not even for localhost — see local testing.
  • Your logs are now a personal-data store. If you log raw webhook bodies (a normal, sensible default for debugging), your log retention policy and your access controls now apply to identity documents. Redact fieldsExtracted before logging, or account for it deliberately.

Values may be null — the pipeline extracts what it can read. These are OCR/MRZ outputs, not values Veridia asserts as true.

Where to go next

  • Overview — configuring the endpoint, headers, and the acknowledge-then-process pattern
  • Signature verification — the HMAC algorithm, with working code for four stacks
  • Event types — every field, every score, every flag
  • Retries — the backoff schedule, delivery guarantees, and recovering a failed event
  • Examples — complete handlers for Express, FastAPI, Laravel, and Cloudflare Workers