Webhooks
The /webhooks page is admin-only. It registers HTTPS endpoints under your tenant that the platform calls when verification events complete — useful for fire-and-forget integrations where your backend wants to react to a result without polling.
Events
| Event type | Fired when |
|---|---|
ocr.completed | A /v1/ocr/scan request returns successfully. Payload includes ocr_id, the extracted fields, the document type, and a signed URL to the portrait crop if storage policy allows. |
face.verify.completed | A /v1/face/verify request returns. Payload includes match (boolean), similarity (0-1), threshold, and the two image ids. |
liveness.check.completed | A /v1/liveness/check request returns. Payload includes lc_id, is_live, score, threshold, and the captured frame's storage ref (if storage policy retains it). |
There is one event per successful API call. Failures (4xx / 5xx responses) do not fire webhooks — by the time the API call resolved with an error, your client knows about it; a duplicate notification doesn't add value.
Registering an endpoint
Click Add endpoint and fill in:
- URL — HTTPS only.
http://URLs are rejected at registration time. The URL is hit with a POST request whose body is JSON. - Mode —
liveortest. The endpoint only receives events from that mode's traffic. To receive both, register the same URL twice (once per mode) — they're separate rows. - Events — pick one or more of the three types above. Empty selection is invalid.
On submit the dashboard creates the endpoint and shows the signing secret exactly once in a confirmation modal. The secret is what the platform uses to sign the HMAC of every webhook payload — you'll verify it on your end before trusting the body (see "Verifying signatures" below).
Copy the secret into your environment / secrets manager before dismissing the modal. There's no way to recover it later. If you lose it, click Rotate secret on the endpoint row to mint a fresh one — that invalidates the previous secret and any in-flight webhook deliveries signed with it.
Webhook delivery format
POST https://your-endpoint
Content-Type: application/json
Drukverify-Webhook-Id: wh_evt_01HF…
Drukverify-Webhook-Timestamp: 1778632003
Drukverify-Webhook-Signature: t=1778632003,v1=<hex-hmac>
{
"id": "wh_evt_01HF…",
"type": "ocr.completed",
"created_at": "2026-05-13T00:34:15Z",
"mode": "live",
"tenant_id": "019e18a6-58de-77e4-b46c-48ae74045f88",
"data": { …event-specific payload… }
}
The signature header follows the Stripe convention so existing webhook-verification libraries work with minor tweaks.
Verifying signatures
Compute the expected signature server-side and compare in constant time:
const crypto = require('crypto');
function verify(req, secret) {
const ts = req.headers['drukverify-webhook-timestamp'];
const sig = req.headers['drukverify-webhook-signature']
.split(',').find(s => s.startsWith('v1=')).slice(3);
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${req.rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(sig, 'hex'),
Buffer.from(expected, 'hex'),
);
}
Two important details:
- Use the raw body, not a parsed/re-serialized one. JSON.stringify will reorder keys and break the signature.
- Reject anything older than 5 minutes (
Math.abs(now - ts) < 300) to make replay attacks harder. The platform retries with exponential backoff for up to 24 hours — replays beyond that window are not legitimate.
Retries
The dispatcher retries on any non-2xx response (or timeout — 10 seconds per attempt) with exponential backoff: 30 s, 2 m, 10 m, 1 h, 6 h, 24 h. After the last retry fails, the event is moved to a dead-letter queue and surfaces on the endpoint's detail page (click into a row from the list).
A persistently-failing endpoint will be automatically disabled after a stretch of consecutive failures — the row stays in the list with a "Disabled (delivery failures)" badge, and an admin must click Re-enable to resume deliveries. The hold-down protects against firehose-ing a dead URL.
Deactivating an endpoint
Toggle Enabled off on a row. The platform stops sending events to it immediately; queued retries are dropped. The endpoint stays in the list — re-enabling resumes deliveries for future events (not the dropped ones).
Audit trail
Every dashboard mutation (create, rotate secret, enable/disable, delete) is recorded on the audit log. The endpoint detail page also has a per-delivery log: every attempt, with response status, latency, and the response body's first ~500 bytes for debugging.