Webhooks let you register an HTTPS endpoint that Vouchmark associates with one of your apps (KYB widgets). A webhook is keyed by the combination of app, environment, and service type. Each registration points at a single endpoint URL, and only receives events for its subscribed serviceType running in its environment.
When a matching event occurs, Vouchmark POSTs a signed JSON payload to your endpoint. Verify every delivery with the webhook’s signing secret before trusting it.
The registration model
A webhook registration has these fields:
| Field | Values | Notes |
|---|
appId | the ID of one of your KYB widgets | Must be a widget owned by your account. |
environment | sandbox or live | Separate endpoints per environment. |
serviceType | kyc_widget, verification, trust_monitor, disputes | One registration per service. |
endpoint | an http/https URL (max 2048 chars) | Where deliveries are sent. |
The signing secret
Each webhook has its own signing secret in the form whsec_....
- The full secret is returned only once, in the response to create and rotate. Store it securely the moment you receive it.
- In the list response and the dashboard, the secret is masked (for example
whsec_…ab12, last 4 characters visible) so you can identify it without exposing it.
- If a secret leaks, rotate it. The old secret stops verifying deliveries immediately, so update your integration in lockstep.
The event envelope
Every delivery is a JSON body with this shape:
{
"id": "whd_...",
"event": "verification.cac.completed",
"createdAt": "2026-05-12T09:14:00.000Z",
"data": { }
}
| Field | Description |
|---|
id | Unique delivery ID (prefix whd_). Use it to dedupe — see Reliability. |
event | The event type. See Event types. |
createdAt | ISO 8601 timestamp of when the event was created. |
data | The event-specific payload. |
Each delivery carries these headers:
| Header | Example | Description |
|---|
Content-Type | application/json | Body is always JSON. |
X-Vouchmark-Event | verification.cac.completed | The event type, mirroring event. |
X-Vouchmark-Delivery | whd_... | The delivery ID, mirroring id. |
X-Vouchmark-Signature | t=1715505240,v1=4f1d... | The signature — see below. |
X-Vouchmark-Timestamp | 1715505240 | Unix seconds the signature was computed at. |
Verifying the signature
The X-Vouchmark-Signature header has the form t=<unixSeconds>,v1=<hmacHex>, where t matches X-Vouchmark-Timestamp.
To verify a delivery:
-
Read
t and v1 from the X-Vouchmark-Signature header.
-
Build the signed payload by concatenating the timestamp, a
., and the exact raw request body (the bytes you received, before any JSON re-serialization):
signedPayload = `${t}.${rawRequestBody}`
-
Compute
HMAC-SHA256(signedPayload, signingSecret) and encode it as lowercase hex.
-
Compare your computed value against
v1 in constant time.
-
Reject deliveries whose
t is too far in the past to protect against replay attacks.
Verify against the raw request body. If your framework parses and re-serializes JSON
before you read it, the bytes change and the signature will not match. Capture the raw
body string first.
Node.js example
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
function verifyVouchmarkWebhook(rawBody, signatureHeader, signingSecret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("=")),
);
const timestamp = Number(parts.t);
const provided = parts.v1;
if (!timestamp || !provided) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) return false;
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(signedPayload)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(provided);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
In Express, capture the raw body with express.raw({ type: "application/json" }) (or express.json({ verify })) so rawBody is the unparsed string.
Event types
A webhook only receives events for its subscribed serviceType and environment. The event types currently emitted are:
| Event | Service type |
|---|
kyb.applicant.decision | kyc_widget |
verification.cac.completed | verification |
verification.tin.completed | verification |
Service types
| Service type | Description |
|---|
kyc_widget | Applicant decisions from your embedded KYB widget. |
verification | Standalone verification results (CAC, TIN). |
trust_monitor | Ongoing trust monitoring events. |
disputes | Dispute lifecycle events. |
Sandbox vs live
sandbox and live are fully separate. Register a webhook per environment, and remember that each environment has its own signing secret. Test against sandbox before pointing live traffic at your production endpoint.
Reliability and retries
- Deliveries are at-least-once. The same event may arrive more than once, so dedupe on the delivery
id (the whd_... value, also in X-Vouchmark-Delivery).
- Vouchmark attempts each delivery up to 5 times with exponential backoff (base 5 seconds).
- Each attempt has a 10 second request timeout.
- Respond with a
2xx status quickly and process the event asynchronously. Any non-2xx response or a timeout triggers a retry.
Register an endpoint
curl -X POST https://api.vouchmark.com/v1/developers/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"appId": "wgt_aBcD...",
"environment": "live",
"serviceType": "verification",
"endpoint": "https://your-app.com/hooks/vouchmark"
}'
{
"success": true,
"data": {
"id": "whk_aBcD...",
"appId": "wgt_aBcD...",
"environment": "live",
"serviceType": "verification",
"endpoint": "https://your-app.com/hooks/vouchmark",
"isActive": true,
"signingSecret": "whsec_aBcD...",
"createdAt": "2026-05-12T09:14:00Z"
}
}
The signingSecret is returned in full here only — store it now.
List your endpoints
curl https://api.vouchmark.com/v1/developers/webhooks \
-H "Authorization: Bearer $TOKEN"
List responses return the secret masked as signingSecretMasked, never in full.
Rotate a signing secret
curl -X POST https://api.vouchmark.com/v1/developers/webhooks/whk_aBcD.../rotate-secret \
-H "Authorization: Bearer $TOKEN"
Returns a new full signingSecret. The previous secret stops verifying deliveries immediately. See Rotate webhook secret.
Delete an endpoint
curl -X DELETE https://api.vouchmark.com/v1/developers/webhooks/whk_aBcD... \
-H "Authorization: Bearer $TOKEN"
Registrations are soft-deleted, so a deleted endpoint stops being listed immediately.
Rate limits
Webhook create/delete is limited to 100 changes per hour per IP. See Rate limits.