---
title: "Signed webhooks"
description: "Receive call events, verify the raw body and deduplicate deliveries."
url: "https://documentation.verbatik.com/api/webhooks/"
---

# Signed webhooks

Receive call events, verify the raw body and deduplicate deliveries.

Configure endpoints in **Settings → Webhooks**. Endpoint management uses your signed-in workspace session; it is not part of the bearer-key public API.

## Add an endpoint

1. Select **Add endpoint** and enter a name and public HTTPS receiver URL.
2. Create a private signing secret of at least 32 characters and store the same value in your receiver’s secret configuration.
3. Subscribe only to needed events: `call.started`, `call.ended`, `call.failed`, `call.analyzed` and `alert.triggered`.
4. Keep caller details and transcripts excluded unless your receiver needs and is authorized to process them.
5. Enable deliveries. Inspect the **Deliveries** tab after an authorized test call or event.

## Headers and body

- `X-Verbatik-Id`: stable delivery identifier; also sent as `Idempotency-Key`.
- `X-Verbatik-Event-Id`: event identifier.
- `X-Verbatik-Event`: event type.
- `X-Verbatik-Timestamp`: Unix time in seconds for this delivery attempt.
- `X-Verbatik-Attempt`: attempt number.
- `X-Verbatik-Signature`: `v1=` followed by the HMAC-SHA256 hexadecimal digest.

The JSON body has `id`, `type`, `occurredAt` and `data`. An event and a delivery have different identifiers. Use the delivery header, not the body’s event ID, when verifying the signature.

## Verify before processing

Compute HMAC-SHA256 using your signing secret over the UTF-8 string:

```text
deliveryId.timestamp.rawBody
```

Use the original raw request body, before JSON parsing or reserialization. Reject missing or malformed headers, compare signatures with a constant-time comparison and reject timestamps more than five minutes from your server’s current time. Keep server clocks synchronized.

Node.js verifier:

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function validWebhook(rawBody, headers, secret) {
  const id = headers.get('x-verbatik-id');
  const timestamp = headers.get('x-verbatik-timestamp');
  const signature = headers.get('x-verbatik-signature');
  if (!id || !timestamp || !signature) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) ||
      Math.abs(Date.now() / 1000 - seconds) > 300) return false;
  const digest = createHmac('sha256', secret)
    .update(id + '.' + timestamp + '.' + rawBody).digest('hex');
  const expected = Buffer.from('v1=' + digest);
  const actual = Buffer.from(signature);
  return expected.length === actual.length &&
    timingSafeEqual(expected, actual);
}
```

After verification, atomically record the delivery ID and enqueue your own durable processing before returning a 2xx acknowledgement. If an ID was already accepted, acknowledge it without repeating side effects.

## Retries and inspection

Delivery has a ten-second receiver deadline. Temporary responses such as 408, 425, 429 and 5xx are retryable; redirects are not followed. Numeric Retry-After values are bounded to one hour. Automatic attempts are bounded, so inspect failed deliveries and use **Redeliver** only after fixing the receiver.

Editing endpoint settings cancels queued attempts tied to the prior settings. Redacting saved caller content cancels pending sensitive deliveries; copies already received by another system cannot be recalled.

Do not assume event ordering or process the same delivery twice. A call-ended notification does not guarantee that call analysis or final billing is already complete.
