# Webhooks

Configure HTTPS destinations, verify raw-body signatures, and deduplicate repeated deliveries.

## Add a destination

Open Webhooks in the workspace and add a public HTTPS URL. Up to three active destinations are allowed per account. Copy the signing secret when it is displayed; it cannot be retrieved later.

Destinations must resolve exclusively to public IPv4 addresses on port 443, without URL credentials or fragments. Private, loopback, link-local and IPv6 destinations are refused. Redirects are not followed, and DNS is revalidated before each delivery.













## Event envelope

Events include an event ID, type, data, and created_at timestamp. Dispatch notifications can contain the local message ID. Forwarded provider message events instead put provider data in data.message; do not assume the two message ID namespaces are interchangeable.

Forwarded events include message.received/sent/ack/failed/receipt/revoked/reaction/edited, session.status/disconnected, group.join/leave/update/join_request, presence.update, poll.vote, status.received and call.received/accepted/rejected/missed. Group, presence, poll, status and call provider data is under data.event. Pairing QR and authentication material are never forwarded. Inspect documented top-level fields and code defensively around provider-specific payloads.







```json
{
  "id": "fce4e0cb-9ee9-4f74-a5f4-a8851facf045",
  "type": "message.sent",
  "data": {
    "id": "b4829d81-353f-4821-ae99-77adeab896ce",
    "channel_id": "1d848fa6-b0c2-4d28-b870-09f44d750d93"
  },
  "created_at": "2026-11-14T10:00:00.000Z"
}
```





## Verify each delivery

The signing input is the timestamp, a literal period, and the exact raw request body. Verify before JSON parsing. Reject stale timestamps and use a timing-safe comparison.





Header | Value
--- | ---
X-TurbineWhats-Event | Stable event ID for deduplication.
X-TurbineWhats-Timestamp | Unix timestamp in seconds for this delivery attempt.
X-TurbineWhats-Signature | sha256= followed by a hexadecimal HMAC-SHA256 signature.

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

export function verifyEvent(rawBody, headers, secret, now = Date.now()) {
  const timestamp = headers.get('x-turbinewhats-timestamp') || '';
  const received = headers.get('x-turbinewhats-signature') || '';
  if (!/^\d+$/.test(timestamp)) return false;
  const seconds = Number(timestamp);
  if (!Number.isSafeInteger(seconds) || Math.abs(now / 1000 - seconds) > 300) {
    return false;
  }
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(timestamp + '.').update(rawBody).digest('hex');
  const left = Buffer.from(received), right = Buffer.from(expected);
  return left.length === right.length && timingSafeEqual(left, right);
}
// Pass the exact raw body bytes/string, before JSON parsing.
// After verification, deduplicate X-TurbineWhats-Event in your database.
// Return 2xx once you have durably recorded the event.
```





## Retries and processing

A 2xx response completes delivery. Other statuses and network failures receive bounded exponential backoff, up to five attempts. The event ID and body remain stable; the attempt timestamp and signature are recomputed.

Persist the event before returning success. Enforce a unique event ID in your database so redelivery cannot repeat a business action. Process slower work asynchronously.

Removing a webhook destination disables new and queued deliveries to that destination; no public replay API is currently provided. Preserve your own application records when you need a durable event history.











