Webhooks
Set it up
In the dashboard, go to Settings → Developers → Webhooks, add your endpoint URL and subscribe it to the VERIFICATION_COMPLETED event (under Verify). Copy the signing secret shown there.
This event covers hosted sessions only. Direct KYC checks return their result in the response, so they don't send webhooks.
VERIFICATION_COMPLETED
Sent once per session, when it ends as COMPLETED or FAILED. A session held at NEEDS_REVIEW sends it only after someone approves or rejects it. Sessions that simply expire don't send it; check for EXPIRED by polling if you need to.
{"eventId": "cm2x7k1q80001abcd1234efgh","event": "VERIFICATION_COMPLETED","businessId": "BU-A6CBSDSQ","timestamp": "2026-10-12T10:04:31.000Z","data": {"sessionId": "q8V3nX0bTzK2mR7wYp1cHg","contact": "+233241234567","contactType": "PHONE","purpose": "opening your account","status": "COMPLETED","passed": true,"challenges": ["BLINK", "LOOK_LEFT", "SMILE"],"completedChallenges": ["BLINK", "LOOK_LEFT", "SMILE"],"idDocument": "GHANA_CARD","idVerified": true,"identityCheckId": "IDN-5f0c2a9e-7c1b-4f7e-9d2a-0b6c1e8f3a21","failReason": null,"imageKinds": ["ID_FRONT", "ID_BACK", "SELFIE"],"riskFlags": [],"reviewedAt": null,"reviewNote": null,"completedAt": "2026-10-12T10:04:31.000Z"}}
| Field | Type | Description |
|---|---|---|
data.passed | boolean | true only when status is COMPLETED. This is the field to act on. |
data.idVerified | boolean | null | Result of the ID step; null if the session had none or the customer never reached it. |
data.failReason | string | null | Why it failed. See the fail reasons on the hosted sessions page. |
data.riskFlags | string[] | Why it was held for review, if it was. With reviewedAt and reviewNote when decided in review. |
eventId | string | Unique per delivery. Use it to ignore duplicates. |
Verify the signature
Each request carries an X-Sendexa-Signature header: the hex HMAC-SHA256 of the raw request body, keyed with your webhook's signing secret. Reject requests where it doesn't match. The x-sendexa-event header names the event.
import crypto from "node:crypto";// Use the raw request body, before any JSON parsing.function isFromSendexa(rawBody, signatureHeader, secret) {const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");const a = Buffer.from(expected);const b = Buffer.from(signatureHeader ?? "");return a.length === b.length && crypto.timingSafeEqual(a, b);}
Respond with a 2xx quickly and do any slow work afterwards. Failed deliveries are retried if retries are enabled on the webhook.