Skip to content
VerathYour digital identity. Reimagined.

Guide

Webhooks

Pending checks, verification.completed, verification.failed, wallet.low_balance and signatures.

Add an HTTPS endpoint in the console’s Developers page. Verath posts events to it as JSON. Reply with any 2xx status to acknowledge; failed deliveries are retried. Webhooks carry match results only, never record fields.

Events

EventWhen
verification.completedA pending check finished, or a person finished a link (with link_id and link_kind: match or face). data is the check, or the link with its result.
verification.failedA pending check gave up after 72 hours without an answer. Never billed.
wallet.low_balanceThe available balance dropped below your alert level. At most once a day. data is the wallet.
verification.completed
{
  "id": "whk_01J9X…",
  "type": "verification.completed",
  "created_at": "2026-11-11T15:34:10.000Z",
  "data": {
    "reference_id": "chk_3c5e1f0a9b2d4c7e8f6a1b2c3d4e5f60",
    "object": "check",
    "check": "nin_basic",
    "status": "completed",
    "found": true,
    "match_result": { "first_name": "match", "last_name": "match", "dob": "match" },
    "cost": { "billed": false, "amount_kobo": 0, "currency": "NGN" },
    "mode": "test"
  }
}

Pending checks

If no source answers within a few seconds, the call returns 424 upstream_unavailable with the check’s reference_id and status: "pending". Verath keeps the request encrypted and retries on its own for up to 72 hours, then sends verification.completed or verification.failed. You can also poll Fetch a check. Pending and failed checks are never billed.

Checking the signature

Every delivery has a Verath-Signature header: t=<unix time>,v1=<hex HMAC-SHA256>, computed over t. followed by the raw body, with your endpoint’s secret. Check it against the raw body before you parse it, and reject anything older than five minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

// Verify a Verath webhook before trusting it.
export function verifyVerathWebhook(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; // replay protection
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}