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
| Concern | Polling GET /v1/verify/{id} | Webhooks |
|---|---|---|
| Latency from verdict to your code | Your poll interval | One request, as soon as the verdict exists |
| API calls per verification | 3–30 | 1 inbound |
| Key required | Secret key, server-side only | None — you verify a signature instead |
| Human review decisions | You must keep polling indefinitely | Delivered as a second event |
| If your service is down | You miss nothing, but you must keep polling | Retried 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:
| Field | Rules |
|---|---|
| URL | Must start with https://. Rejected before saving otherwise. Also subject to an SSRF guard. |
| Secret | You 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.
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", ...}
| Header | Use |
|---|---|
Veridia-Signature | t=<unix seconds>,v1=<hex hmac-sha256>. Verify this before trusting anything. |
Veridia-Event | The 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-Id | The 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 response | What happens |
|---|---|
2xx | Delivered. Done. |
5xx, timeout, connection error | Retried on the backoff schedule |
408, 429 | Retried |
Any other 4xx | Permanent 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}:
statusis the pipeline state:queued,processing,completed,failed.verdictis 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.
| Tool | Notes |
|---|---|
| ngrok | Free tier; the usual choice |
| localtunnel | Free, open source |
| Cloudflare Tunnel | Free; 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-Signaturebefore trusting the body. Always. - Reject if
|now - t| > 300seconds. - Respond
2xxwithin 10 seconds; persist first, process after. - Deduplicate by
id(or theVeridia-Event-Idheader). Delivery is at least once. - Switch on
type. Notevent— there is noeventfield. - Read
scoreswith snake_case keys, and null-checkscores.liveness. - Treat
fieldsExtractedas personal data, including in your logs. - Log
idandverificationIdon every delivery — they are what support will ask for.
What's next
- Signature verification — the algorithm, with code
- Event types — the full payload contract
- Retries — backoff, guarantees, recovery
- Examples — complete handler implementations