Docs · Reference

Webhooks

Server-to-server delivery of a session’s outcome — signed, and the record of truth for your integration. Register an endpoint from your dashboard or via POST /v1/webhook-endpoints.

Event types

FieldTypeDescription
verification_session.completedeventA session reached a terminal outcome — data.status is passed or failed.
verification_session.expiredeventA session's 30-minute window elapsed before it reached a terminal outcome.

A new endpoint subscribes to both by default; pass events at creation to narrow it.

Payload shape

json · verification_session.completed
{
  "id": "evt_5f3a2b1c9d8e7f6a5b4c3d2e",
  "type": "verification_session.completed",
  "created": "2026-07-12T04:00:31.000Z",
  "data": {
    "id": "vs_01ARZ3NDEKTSV4RRFFQ69G5FAV",
    "status": "passed",
    "required_age": 18,
    "method": "face_estimation",
    "confidence": "high",
    "mode": "test",
    "metadata": {}
  }
}
FieldTypeDescription
idstringDeterministic per (session, event type, endpoint) — a retried or duplicated delivery of the same logical event always carries the same id. Dedupe on it if your handler isn’t naturally idempotent.
typestringOne of the event types above.
createdstringISO 8601 timestamp of this delivery attempt.
data.idstringThe verification session id (vs_...).
data.statusstringThe session's status at delivery time — passed, failed, or expired.
data.required_age16 | 18As set at session creation.
data.methodstring | nullWhich method produced the outcome. null for an expired session.
data.confidencestring | nullhigh, medium, or low. null for an expired session.
data.mode"test" | "live"Which key created the session.
data.metadataobjectThe metadata you passed at session creation, echoed back verbatim.

The AgeAssure-Signature header

Every delivery carries:

headers
AgeAssure-Signature: t=1752300031,v1=5b3f6c9e2a1d... (hex hmac-sha256)
AgeAssure-Event: verification_session.completed
Content-Type: application/json

t is the delivery’s unix timestamp in seconds. v1 is the hex-encoded HMAC-SHA256 of {t}.{raw request body} — the timestamp, a literal dot, then the exact raw JSON body, signed with the whsec_... secret you were shown once when you created the endpoint.

Recompute the signature yourself and compare with a constant-time equality check; reject anything where t is further than five minutes from “now” (replay protection). This is exactly the check AgeAssure’s own API runs on inbound provider webhooks — same construction, same tolerance.

javascript · node
import crypto from "node:crypto";

// header: "t=<unix seconds>,v1=<hex hmac-sha256>"
export function verifyAgeAssureSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const match = /^t=(\d+),v1=([0-9a-f]+)$/.exec(header ?? "");
  if (!match) return false;

  const [, timestamp, signature] = match;
  if (Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp)) > toleranceSeconds) {
    return false; // too old — possible replay
  }

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(signature, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
javascript · node (usage)
// In your route handler, verify against the RAW request body (before any
// JSON.parse/body-parser reformatting) — re-serialising the parsed object
// will not byte-for-byte match what was signed.
const ok = verifyAgeAssureSignature(rawBody, req.headers["ageassure-signature"], endpointSecret);
python
import hashlib
import hmac
import re
import time


def verify_ageassure_signature(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    match = re.match(r"^t=(\d+),v1=([0-9a-f]+)$", header or "")
    if not match:
        return False

    timestamp, signature = match.group(1), match.group(2)
    if abs(int(time.time()) - int(timestamp)) > tolerance_seconds:
        return False  # too old — possible replay

    expected = hmac.new(
        secret.encode("utf-8"),
        f"{timestamp}.{raw_body.decode('utf-8')}".encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

Retries and failure

A delivery counts as successful on any 2xx response within a 10-second timeout — anything else (non-2xx, timeout, DNS/network failure) is retried automatically, up to 5 attempts. After that it lands in a dead-letter queue and stops retrying; re-deliver manually from the dashboard’s Webhooks page.

Retries resend to every endpoint subscribed to that event, including ones that already succeeded on an earlier attempt for the same batch — the deterministic id above is what makes that safe to dedupe on. Return a 2xx quickly (queue the heavy work) rather than doing slow processing inline in the handler.