Create a webhook endpoint
Webhooks notify your service when something happens: a verification completes, a manual review is requested, a verification expires without a verdict. You register an HTTPS endpoint, pick the events, and the platform POSTs a signed payload.
Before you start
Section titled “Before you start”You need Owner or Admin access. Webhook endpoints are configured per mode — endpoints registered in test mode receive only test-mode events. Switch mode in the sidebar before registering. See Modes and regions.
Register an endpoint
Section titled “Register an endpoint”Open Developers → Webhooks and click New subscription. Enter the receiver URL. The URL must start with https://. Plain HTTP is rejected so your signing secret cannot leak. Pick the events you want.
On submit, we generate a 32-byte signing secret and return it once. Copy it into a secrets manager — it is never retrievable again. If you lose the secret, rotate it (see Verify webhook signatures).
Choose event types
Section titled “Choose event types”These events are delivered today:
kyc.session.completed— verification finished, terminal outcome assigned.kyc.session.cancelled— verification cancelled by the operator.kyc.session.manual_review_requested— the verification was parked for human review.kyc.session.form_flagged- a custom form step triggered a flag-for-review rule. This one is an occurrence event sent mid-journey, not a terminal state, and it carries no answer payload: retrieve submitted answers from the console or the session detail endpoint.kyc.session.expired- the verification ended without a verdict because the applicant did not finish in time, or because a verification left awaiting review lapsed.statusisexpiredandoutcomeisnull.kyc.session.document_signed- an applicant signed an e-signature step and the sealed document is ready to verify. Occurrence only: the body carries the envelope and document identifiers and each document’s final hash, never the document content. Silence is not a decline. A signature we recorded but could not finish sealing produces no event at all, on purpose: an event asserting a sealed document that does not exist is worse than none. If you need to know an e-signature step finished either way, read the session outcome rather than waiting on this event.kyc.session.document_declined- an applicant declined to sign. A normal outcome, not an error. Occurrence only, and narrower than the signed event: no final hash, because nothing was sealed, and no decline reason, because the reason is applicant-written text we do not put on an outbound event. The session lands completed with adeclinedoutcome, and akyc.session.completeddelivery carries that outcome alongside this one.
The event type below, and only that one, is reserved: it is accepted on a subscription but produces no delivery. Everything listed above this paragraph is delivered today. Subscribing to a reserved type is harmless and forward-compatible; build your receiver to handle it when deliveries begin, without assuming it currently arrives:
kyc.session.skipped_by_applicant— the applicant skipped a step, ending the verification.
Naming follows kyc.<domain>.<state> so you can pattern-match on the prefix. Your receiver should handle unknown event.type values defensively (log and ignore, do not error) — new event types can ship without a code change on your side.
The event payload
Section titled “The event payload”Most kyc.session.* deliveries carry a compact JSON body with reference fields only, no applicant personal data. The two e-signature events carry their own shape instead; both are described under E-signature event payloads below.
{ "sessionId": "vs_01J9X2Y3Z4A5B6C7D8E9F0G1H2", "status": "completed", "outcome": "approved", "occurredAt": "2026-06-16T12:34:56.000Z", "mode": "test", "type": "kyc.session.completed", "decisionSource": "automatic"}sessionId— the verification id. Use it to fetch full detail from the REST API.status— the terminal status at emit time.outcome— the assigned verification outcome, ornullfor a transition that carried no verdict.occurredAt— UTC ISO-8601 timestamp of the transition.mode-liveortest, the data plane the event was produced on. Present only on events emitted after this release.type- the event type, one of thekyc.session.*types listed above. Present only on events emitted after this release.decisionSource- what produced the decision:automaticwhen our verification rules assigned the outcome with no human review, orreviewerwhen a person recorded it. Present only on events that carry a decision, and only on events emitted after this release.
mode, type and decisionSource are additive fields, not a new envelope. Retry and replay resend the exact body that was stored at emit time, so an event emitted before a field existed, or replayed from one that was, keeps its original shape permanently - there is no backfill. Treat all three as optional in your receiver.
decisionSource is absent whenever the transition carried no decision at all, for example a cancelled or expired session or a session parked for review, and on any event emitted before it existed. It is also absent in one case that IS a completed decision: the applicant ended the session themselves, which arrives as status: "completed" with outcome: "declined" and no decisionSource. So an absent decisionSource does not mean no decision was made. Read outcome for the verdict and decisionSource only for who or what produced it. Do not read an absent value as reviewer, and do not infer one from outcome. The field never carries a reviewer’s name, email or id, and never says why the outcome was assigned.
You do not have to infer any of that from a missing key. Every delivery of a registered event type also carries an X-Webhook-Event-Version header holding the payload contract version for that event type, which is 1 for all kyc.session.* events today. Payloads are additive within a version and a breaking change ships as a new version, so this header is what your receiver branches on to pick a parser.
Adding mode and type was additive within version 1, so the header does not by itself tell you whether a given body carries them. What it does tell you is which contract the delivery belongs to, and that resolves the three cases:
| Header | mode in body |
What it means |
|---|---|---|
1 |
present | An event emitted after this release. Read mode and type directly. |
1 |
absent | An event emitted before this release, or a replay of one. Handle it exactly as you did before. |
| absent | absent | Not a registered event type. Today that is only the console test ping, which also sends X-Webhook-Origin: test_ping. |
X-Webhook-Event-Version is advisory metadata and is not covered by the signature. The signature is computed over the request body alone, so anyone who can reach your endpoint can send any value in this header. Verify the signature first, and never make a trust or authorization decision based on this header.
Do not treat an absent mode as live: a receiver that defaults it to live would process a synthetic session-completed event as though it were a real identity verification.
mode reports which data plane produced the event, not whether the event is a real verification. A console test ping sent on a live subscription carries mode: "live" and test: true in the body. Recognize a test ping by its X-Webhook-Origin: test_ping header (see Verify webhook signatures), not by mode.
The payload is additive within a version: a new optional field may be added without breaking your receiver, and a breaking change ships as a new event version. Parse defensively and ignore fields you do not recognize. The webhook is an integration trigger, not a data export — fetch authoritative detail by sessionId from the API.
E-signature event payloads
Section titled “E-signature event payloads”The two e-signature events carry document identifiers instead of a session status, so their bodies differ from the shape above. Both still carry mode and type, and both are occurrence only.
kyc.session.document_signed:
{ "sessionId": "vs_01J9X2Y3Z4A5B6C7D8E9F0G1H2", "envelopeId": "9a1d0c2e-3b4f-4a6d-8e2f-1c5b7a9d0e33", "documents": [ { "documentId": "5f2b8c14-7d3e-4a91-b60c-2e8f4a7d1b05", "documentOrder": 0, "finalHash": "3b1f8c9a2d47e05b6f13c8a9d0e2b47f5a6c8d19e3f204b7c6a5d8e9f0b1c2d3" } ], "signedAt": "2026-06-16T12:34:56.000Z", "mode": "test", "type": "kyc.session.document_signed"}kyc.session.document_declined is the same shape without finalHash, and with declinedAt in place of signedAt:
{ "sessionId": "vs_01J9X2Y3Z4A5B6C7D8E9F0G1H2", "envelopeId": "9a1d0c2e-3b4f-4a6d-8e2f-1c5b7a9d0e33", "documents": [{ "documentId": "5f2b8c14-7d3e-4a91-b60c-2e8f4a7d1b05", "documentOrder": 0 }], "declinedAt": "2026-06-16T12:34:56.000Z", "mode": "test", "type": "kyc.session.document_declined"}envelopeId- the signing envelope. Use it to fetch detail from the API.documents- always an array, one entry per document in the envelope, ordered bydocumentOrderstarting at0. Iterate it; do not readdocuments[0]and assume it is the only one.finalHash- the SHA-256 of the sealed document, lowercase hex. Each entry carries its own, so you always know which document a hash belongs to. Pass it to the public verification endpoint to confirm a copy you hold.signedAt/declinedAt- UTC ISO-8601 timestamp of the act.
Neither body carries signer names, contact details, the document title, the values filled into the document, or the decline reason. Fetch those from the console or the session detail endpoint, where they are access controlled.
Test the delivery
Section titled “Test the delivery”After saving the subscription, send a test request from your client to confirm signature verification. The Verify webhook signatures page shows the exact algorithm, header format, and tolerance window. Real events fire when the underlying state transition happens.
Inspect deliveries in the console
Section titled “Inspect deliveries in the console”The subscription list at Developers → Webhooks shows each endpoint, the events it receives, and the last successful and unsuccessful delivery times. Open a subscription to see its full delivery history, replay individual deliveries, and rotate the signing secret.
Inspect deliveries programmatically
Section titled “Inspect deliveries programmatically”The same delivery log is available over the REST API at GET /v1/webhook-events. It is cursor-paginated and returns one row per delivery attempt, with the status, HTTP code, latency, attempt count, and a bounded error code. Filter by session_id to trace one verification’s deliveries, or by subscription_id to inspect one endpoint. The response never includes the signing secret or the event payload. See the API reference for the full schema.
Next steps
Section titled “Next steps”- Verify every delivery: Verify webhook signatures.
- Recover from failures: Replay and troubleshoot webhook deliveries.