U-verify

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

There's no self-service "add webhook URL" button in the dashboard yet, 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"
}
FieldTypeNotes
outcomestringapproved, rejected, manual_review, or inconclusive.
confidencenumber | nullOverall confidence score, 0–100.
checksobjectOne 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 200 as 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.