> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vouchmark.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Register an endpoint per app, environment, and service type, then verify signed deliveries.

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](/api-reference/developers/create-webhook) and [rotate](/api-reference/developers/rotate-webhook-secret). Store it securely the moment you receive it.
* In the [list](/api-reference/developers/list-webhooks) 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](/api-reference/developers/rotate-webhook-secret). 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:

```json theme={null}
{
  "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](#reliability-and-retries). |
| `event`     | The event type. See [Event types](#event-types).                                                    |
| `createdAt` | ISO 8601 timestamp of when the event was created.                                                   |
| `data`      | The event-specific payload.                                                                         |

## Headers

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:

1. Read `t` and `v1` from the `X-Vouchmark-Signature` header.

2. 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}`
   ```

3. Compute `HMAC-SHA256(signedPayload, signingSecret)` and encode it as lowercase hex.

4. Compare your computed value against `v1` in **constant time**.

5. Reject deliveries whose `t` is too far in the past to protect against replay attacks.

<Warning>
  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.
</Warning>

### Node.js example

```js theme={null}
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

```bash theme={null}
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"
  }'
```

```json Response theme={null}
{
  "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

```bash theme={null}
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

```bash theme={null}
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](/api-reference/developers/rotate-webhook-secret).

## Delete an endpoint

```bash theme={null}
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](/rate-limits).
