Webhooks deliver real-time notifications to your system when verification events occur. Instead of polling for results, configure a webhook endpoint and Zenoo will POST event payloads as they happen.
Fires when a Person or Company Verification journey completes. The data field contains the full verification results, identical to the pull endpoint response.
Fires when a check fails after all automatic retry attempts are exhausted. This is a terminal state.
{ "event_type": "check.failed", "timestamp": "2026-01-15T16:00:00Z", "journey_id": "jrn-abc123", "callback_reference": "APP-2026-0123", "status": "failed", "data": { "check_type": "KYB_REGISTRATION", "error_category": "Permanent", "retry_count": 3, "failure_reason": "Company not found in registry" }}
Do not retry the same check after receiving this event. Investigate the cause (invalid data, provider outage, missing information) and either correct the issue or escalate.
How Zenoo responds to your endpoint’s status codes:
Your Response
Zenoo Behavior
200-299
Delivery confirmed, no retry
3xx
Follows redirect (up to 3 hops)
400-499
Retries with backoff (may be transient)
500-599
Retries with backoff
Timeout (>30s)
Retries with backoff
Connection refused
Retries with backoff
Return 200 OK immediately, then process the event in a background job. This is the single most important implementation detail for reliable webhook handling.
Configure your staging webhook endpoint during onboarding. All verification flows in the staging environment trigger the same webhook events as production, using mock provider data.
# Start your webhook server locallynode server.js # listening on port 3000# Expose with ngrokngrok http 3000# Configure your webhook URL as:# https://abc123.ngrok.io/webhooks/zenoo
Zenoo signs all outbound webhook payloads using HMAC-SHA256. Every webhook request includes a signature in the X-Zenoo-Signature header. Verify this signature before processing the payload.
Extract the X-Zenoo-Signature header value from the incoming request.
2
Isolate the hex digest
Remove the sha256= prefix to isolate the hex digest.
3
Compute the expected digest
Compute HMAC-SHA256 of the raw request body using your webhook secret.
4
Compare using constant-time comparison
Compare the computed digest with the received digest using a constant-time comparison function. If the digests do not match, reject the request with 401 Unauthorized. Do not process the payload.
import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.security.MessageDigest;public class WebhookVerifier { public static boolean verify(String rawBody, String signature, String secret) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256")); byte[] hash = mac.doFinal(rawBody.getBytes("UTF-8")); String expected = bytesToHex(hash); String received = signature.replace("sha256=", ""); return MessageDigest.isEqual( expected.getBytes("UTF-8"), received.getBytes("UTF-8") ); } private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); }}
Express does not provide req.rawBody by default. Use the body-parser middleware with verify option or express.raw() to capture the raw body before JSON parsing:
Always use a constant-time comparison function when verifying signatures. Standard string equality operators (==, ===, .equals()) are vulnerable to timing attacks. An attacker can measure response times to incrementally guess the correct signature byte by byte.
Log the failure with the request timestamp, source IP, and headers.
Do not process the payload.
Monitor for repeated failures, which may indicate an attack or a misconfigured secret.
If every webhook fails verification, check that your webhook secret matches the one Zenoo provided during onboarding. Secrets are environment-specific. A staging secret will not validate production webhooks.