Receiving Webhooks
This page defines the HTTP delivery contract and the payload shapes your handler will see. Where deliveries come from is covered in Webhook Endpoints; step-specific fields are documented separately in Webhook Payload.
Delivery contract
| Property | Value |
|---|---|
| Method | POST |
| Content type | application/json |
| Signature header | X-Signature when a webhook secret is configured |
| Successful acknowledgement | Any 2xx for a V2 endpoint; strictly 200 OK for a LEGACY_V1 endpoint |
| Delivery attempts | Up to four total attempts, with exponential backoff from 1s to 8s |
| Request timeout | 30 seconds by default |
| Redirects | Not followed |
Each endpoint subscribed to an event gets its own delivery: they are signed, retried, and disabled independently, and one endpoint failing never affects another.
Your endpoint should:
- Capture the unmodified request body.
- Verify
X-Signaturebefore trusting the body. - Parse the JSON only after verification.
- Store or enqueue the event using idempotent application logic.
- Return
200 OKpromptly.
See Webhook Signature for the exact HMAC input and header format.
Which shape you receive
The endpoint's payloadVersion decides:
LEGACY_V1delivers the two envelopes described below, exactly as Zyphe sent them before organization-level endpoints existed.V2carries the same facts in a different, versioned envelope: a canonicaltype, an eventid, the source and recipient of the delivery, aflowblock for the run, and the result under its own name indata.
This page describes LEGACY_V1. The V2 envelope, and a field-by-field mapping between the two, are in Payload Versions. The step-specific objects in Webhook Payload are the same in both.
Standard result envelope
Verification-result webhooks use this envelope:
{
"resultId": "936f35a8-4921-430e-b016-15be48968886",
"event": "COMPLETED",
"data": {
"dv": {
"id": "d65fa9a0-596e-4d7e-afd1-59a23bfb5a74",
"status": "PASSED"
}
},
"custom": {},
"flowStatus": "COMPLETED"
}
| Field | Meaning |
|---|---|
resultId | Identifier of the overall flow result. |
event | The event for this delivery. Common verification values include COMPLETED, FAILED, REVIEW, and REJECTED. AML updates use PRODUCED, REFRESHED, or MODERATED. |
data | The typed result object. The primary key identifies the result type. |
custom | Flow-level custom data used to reconcile the result with your system. |
flowStatus | Current status of the overall flow, which may differ from the individual step status. |
The primary data keys are dv, poa, form, spid, phone, wallet, kyb, aml, and geolocation. Extended DV webhooks also include additionalData; organizations using legacy compatibility may also receive kyc beside dv.
Notification envelope
Lifecycle notifications have a different shape. They use top-level eventData and intentionally omit data and flowStatus:
{
"resultId": "936f35a8-4921-430e-b016-15be48968886",
"event": "NOTIFICATION",
"eventData": {
"version": 0,
"type": "DOCUMENT_EXPIRED",
"flowResultId": "936f35a8-4921-430e-b016-15be48968886"
},
"custom": {}
}
Route notifications by eventData.type. Current notification types are:
DOCUMENT_EXPIREDKYC_RESULT_EXPIREDFLOW_RISK_REEVALUATEDTRANSACTION_ALERT(Transaction Monitoring)
On a V2 endpoint you do not need to route on eventData.type: each of these is a distinct canonical event you subscribe to individually (verification.document.expired, verification.kyc.expired, flow.risk.reevaluated, transaction.alert.created).
Their fields are listed under Notification Events.
Dispatching events
On a V2 endpoint, branch on the envelope's canonical type — no envelope sniffing required:
if ('apiVersion' in body) {
// body.type is e.g. 'verification.dv.completed', 'flow.completed', 'transaction.alert.created'
// body.flow holds the run; body.data holds the result under its own name
handleCanonicalEvent(body.type, body.id, body.flow, body.data)
}
On a LEGACY_V1 endpoint, use the envelope before inspecting step-specific fields:
if (payload.event === 'NOTIFICATION') {
handleNotification(payload.eventData.type, payload.eventData)
} else if (payload.data.aml) {
handleAmlUpdate(payload.event, payload.data.aml)
} else {
const resultTypes = ['dv', 'poa', 'form', 'spid', 'phone', 'wallet', 'kyb', 'geolocation']
const resultType = resultTypes.find((key) => payload.data[key] != null)
handleVerificationResult(resultType, payload)
}
Do not assume that resultId uniquely identifies one delivery: a flow can produce multiple step results and later updates. Choose an idempotency key that includes the result type and its result identifier or update kind.
On V2 this is simpler: the envelope's id identifies the event and is stable across retries and across endpoints, so it is the natural idempotency key.