Skip to main content

Overview

This page covers setting up the endpoint and handling the request. For the payload contract see Event types; for delivery guarantees see Retries.

Why webhooks over polling

ConcernPolling GET /v1/verify/{id}Webhooks
Latency from verdict to your codeYour poll intervalOne request, as soon as the verdict exists
API calls per verification3–301 inbound
Key requiredSecret key, server-side onlyNone — you verify a signature instead
Human review decisionsYou must keep polling indefinitelyDelivered as a second event
If your service is downYou miss nothing, but you must keep pollingRetried for ~12.6 minutes, then re-queueable

The last row is the one that matters most. A review verdict is final until a person decides it, which may be hours later. Polling for it means either polling forever or giving up and stranding the user. The webhook for the reviewer's decision arrives whenever it arrives.

Setup

There is one webhook per tenant, configured in the dashboard under Settings → Webhook. There is no endpoint list, no per-event subscription, and no test/live split for webhooks.

The form has two fields:

FieldRules
URLMust start with https://. Rejected before saving otherwise. Also subject to an SSRF guard.
SecretYou choose it. Minimum 24 characters. Leave the field blank to keep the current secret.

The secret is not generated for you and is never displayed back — the field is write-only. Generate one with real entropy and store it where your application reads it:

openssl rand -hex 32

Once saved, the next verification that reaches a verdict POSTs to your URL.

Changing the secret takes effect immediately

Saving a new secret replaces the old one at once. There is no dual-secret grace window. Deploy the new secret to your application first, then save it in the dashboard — see rotation.

The request

POST /webhooks/veridia HTTP/1.1
Host: yourapp.com
Content-Type: application/json
Veridia-Signature: t=1753142348,v1=4f8a3b9c01ee5d2f...
Veridia-Event: verification.approved
Veridia-Event-Id: evt_9f2c41ab7d8e4c05b6a3e17f2d904c8b

{"id":"evt_9f2c41ab7d8e4c05b6a3e17f2d904c8b","type":"verification.approved", ...}
HeaderUse
Veridia-Signaturet=<unix seconds>,v1=<hex hmac-sha256>. Verify this before trusting anything.
Veridia-EventThe event type, mirroring the body's type. Convenient for routing/metrics; not authenticated on its own — the body is what the HMAC covers.
Veridia-Event-IdThe same value as the body's id. Lets you check your dedup store before parsing the body.

The signature covers the raw bytes of the body. Do not re-serialize the JSON before computing the HMAC — key order and whitespace are part of what was signed. See Signature verification.

Respond 2xx within 10 seconds

The delivery timeout is 10 seconds (5 seconds to connect). Anything slower counts as a failed attempt and is retried, which means your slow-but-successful handler will be asked to do the same work again.

Acknowledge first, work afterwards:

app.post('/webhooks/veridia', express.raw({ type: 'application/json' }), (req, res) => {
// 1. Verify the signature — fast, and the only thing that must happen inline.
if (!verifyVeridiaSignature(req.header('Veridia-Signature'), req.body, SECRET)) {
return res.status(401).send('invalid signature');
}

// 2. Acknowledge. Everything past this point is on your own time.
res.status(200).send('ok');

// 3. Do the work. Failures here are yours to retry — the delivery is
// already acknowledged and will not be redelivered.
const payload = JSON.parse(req.body.toString('utf8'));
enqueue(payload).catch(err => logger.error({ err, eventId: payload.id }));
});

That last comment is the trade-off you are accepting. Acknowledging early means a crash between step 2 and step 3 loses the event, so persist the payload durably (a row, a queue message) as the acknowledgement, and do the real processing from there. The examples all follow that pattern.

Status codes we act on

Your responseWhat happens
2xxDelivered. Done.
5xx, timeout, connection errorRetried on the backoff schedule
408, 429Retried
Any other 4xxPermanent failure. Not retried — a 401 or 404 means retrying the identical request cannot succeed.

A permanent failure still lands in the dashboard and can be re-queued manually once you have fixed the cause.

status is not verdict

This applies to the polling API rather than to webhooks, but it is the most expensive mistake this product's API makes available, and webhook handlers inherit it whenever they cross-check against GET /v1/verify/{id}:

  • status is the pipeline state: queued, processing, completed, failed.
  • verdict is the outcome: approved, review, rejected.

status === "completed" means the pipeline ran, not that the person passed. Branching on it to grant access admits every rejected applicant. Webhook payloads have no status field at all — they carry type and verdict, both of which are outcomes. Branch on those.

Testing locally

Webhooks cannot reach localhost. This is not a configuration you can relax: the URL must be https://, and the SSRF guard rejects loopback and private addresses at send time. A tunnel is the only way to receive real deliveries in development.

ToolNotes
ngrokFree tier; the usual choice
localtunnelFree, open source
Cloudflare TunnelFree; better for a long-lived dev environment
ngrok http 3000
# Forwarding https://abc123.ngrok.io -> http://localhost:3000

# Put https://abc123.ngrok.io/webhooks/veridia in Settings → Webhook

To exercise the handler without running a verification, replay a body with a fresh signature — see the replay script. Sign the exact bytes you send.

Checklist

  • Verify Veridia-Signature before trusting the body. Always.
  • Reject if |now - t| > 300 seconds.
  • Respond 2xx within 10 seconds; persist first, process after.
  • Deduplicate by id (or the Veridia-Event-Id header). Delivery is at least once.
  • Switch on type. Not event — there is no event field.
  • Read scores with snake_case keys, and null-check scores.liveness.
  • Treat fieldsExtracted as personal data, including in your logs.
  • Log id and verificationId on every delivery — they are what support will ask for.

What's next