Webhooks
Event envelope, signature verification, and delivery semantics.
Webhooks are the authoritative way to receive verification results. Polling
/v1/verifications/:id works, but webhooks are instant and cheaper.
Event envelope
This mirrors Stripe's event shape. id is globally unique; type identifies
the event; data.object is the full resource at the time the event fired.
Headers
Every delivery includes:
Content-Type: application/jsonKYC-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>KYC-Event-Id: evt_...(idempotency key for your receiver)User-Agent: KYC-Webhooks/1.0
Verifying the signature
- Read the
KYC-Signatureheader and parse outtandv1. - Compute
HMAC-SHA256(secret, t + "." + raw_body). - Compare the hex digest against
v1using a constant-time comparison. - Reject if
|now - t| > 300seconds.
Delivery guarantees
- At-least-once. Expect occasional duplicates; dedupe on
KYC-Event-Id. - Retries. Exponential backoff up to 24 hours.
- Expected response.
2xxwithin 10 seconds. Anything else is a retry.
Event types (MVP)
Only terminal-state events fire today. There is no verification.created
or verification.updated; intermediate progress is read from GET /v1/verifications/{id} or the iframe kyc.status_changed postMessage.
| Type | Fires when |
|---|---|
verification.completed | All checks pass and the session reaches a terminal verified state. |
verification.failed | Any check fails and the session reaches a terminal failed state. |
verification.requires_review | Session lands in requires_review (manual reviewer attention needed). |
verification.canceled | Tenant or admin canceled an in-flight session. |
verification.expired | Session aged out (~30 min) without reaching a terminal state. |
billing.topup.completed | Wallet credited (Stripe checkout or AIO USDT settled). |