Skip to main content

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

PropertyValue
MethodPOST
Content typeapplication/json
Signature headerX-Signature when a webhook secret is configured
Successful acknowledgementAny 2xx for a V2 endpoint; strictly 200 OK for a LEGACY_V1 endpoint
Delivery attemptsUp to four total attempts, with exponential backoff from 1s to 8s
Request timeout30 seconds by default
RedirectsNot 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:

  1. Capture the unmodified request body.
  2. Verify X-Signature before trusting the body.
  3. Parse the JSON only after verification.
  4. Store or enqueue the event using idempotent application logic.
  5. Return 200 OK promptly.

See Webhook Signature for the exact HMAC input and header format.

Which shape you receive

The endpoint's payloadVersion decides:

  • LEGACY_V1 delivers the two envelopes described below, exactly as Zyphe sent them before organization-level endpoints existed.
  • V2 carries the same facts in a different, versioned envelope: a canonical type, an event id, the source and recipient of the delivery, a flow block for the run, and the result under its own name in data.

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"
}
FieldMeaning
resultIdIdentifier of the overall flow result.
eventThe event for this delivery. Common verification values include COMPLETED, FAILED, REVIEW, and REJECTED. AML updates use PRODUCED, REFRESHED, or MODERATED.
dataThe typed result object. The primary key identifies the result type.
customFlow-level custom data used to reconcile the result with your system.
flowStatusCurrent 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_EXPIRED
  • KYC_RESULT_EXPIRED
  • FLOW_RISK_REEVALUATED
  • TRANSACTION_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.