Documentation
Webhooks
Document checks and facial liveness run asynchronously, they can take longer than a request is willing to wait on. Instead of polling a status endpoint in a loop, register a webhook URL and U-verify will call it the moment a verification session finishes.
Webhook setup currently goes through support
webhook_url and your separate webhook_secret are configured for you by the U-verify team. Message support with your client_key and the URL you want events sent to, and it'll be set up, usually same-day. This page documents what you'll receive once it is.The event
Currently one event type fires: verification.completed, sent once every check in a session has finished (whichever combination of NIN, document, and facial you requested).
POST https://your-webhook-url
Content-Type: application/json
X-VerifyNG-Event: verification.completed
X-Signature: sha256=8f434346648f6b96...
{
"event": "verification.completed",
"session_id": "01H8X...",
"subject_id": "01H8W...",
"outcome": "approved",
"confidence": 96,
"checks": {
"nin": { "status": "passed", "confidence": 96 },
"document": { "status": "passed", "confidence": 91 }
},
"timestamp": "2026-01-01T12:00:00.000000Z"
}| Field | Type | Notes |
|---|---|---|
outcome | string | approved, rejected, manual_review, or inconclusive. |
confidence | number | null | Overall confidence score, 0–100. |
checks | object | One entry per requested check type, status and confidence only. Fetch the full record via the API if you need more. |
Verifying the signature
X-Signature is sha256= followed by an HMAC-SHA256 of the exact raw request body, keyed with your webhook_secret; a different secret from your api_secret, and with no timestamp or path folded in (unlike request signing, this one only covers the body). Always verify before trusting a webhook; anyone who finds your URL can otherwise POST fake events to it.
import crypto from "node:crypto";
// req.rawBody must be the *unparsed* request body, verify before your
// framework's JSON body-parser touches it, or the bytes won't match.
function isValidWebhook(rawBody, headerSignature, webhookSecret) {
const expected = "sha256=" + crypto
.createHmac("sha256", webhookSecret)
.update(rawBody)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(headerSignature);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhooks/verifyng", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.header("X-Signature");
if (!isValidWebhook(req.body, sig, process.env.VERIFYNG_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
// ... handle event.event === "verification.completed"
res.sendStatus(200);
});Delivery behaviour
- Each attempt times out after 30 seconds, respond quickly, do slow work afterward.
- Retried up to 5 times if your endpoint doesn't return a 2xx status, with increasing delay between attempts (roughly 1 minute, then 2, then 3...).
- Return
200as soon as you've durably queued the event, don't make U-verify wait on your own downstream processing. - Treat a webhook as "something finished, go check" rather than the full source of truth, if you ever miss one,
GET /verifications/{ulid}/results(see the API reference) always reflects the current state.