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

# Webhooks

> Subscribe to Dialbird events and verify signed webhook deliveries.

Dialbird delivers events to your server using the [REST Hooks](https://resthooks.org/) model: **one subscription per event type per URL**. Manage subscriptions with the `api:webhooks` scope.

## Event types

| Event                        | Fires when                    |
| ---------------------------- | ----------------------------- |
| `message.incoming.received`  | An inbound SMS is received.   |
| `message.outgoing.delivered` | An outbound SMS is delivered. |
| `call.incoming.completed`    | An inbound call completes.    |
| `call.outgoing.completed`    | An outbound call completes.   |

## Subscribing

Create a subscription with a `target_url` (HTTPS) and a single `event_type`.

```bash theme={null}
curl -X POST https://app-staging.dialbird.io/api/v1/subscriptions \
  -H "Authorization: Bearer $DIALBIRD_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "message.incoming.received",
    "target_url": "https://example.com/hooks/dialbird"
  }'
```

<Warning>
  The response includes the plaintext `signing_secret` (prefixed `whsec_`) **once**. Store it immediately — it is never returned again. Listing subscriptions never includes it.
</Warning>

## Delivery payload

Every delivery is an event envelope:

```json theme={null}
{
  "id": "evt_4f9a2c",
  "type": "message.incoming.received",
  "api_version": "v1",
  "occurred_at": "2026-01-15T10:00:00Z",
  "business_id": "biz_123",
  "data": { }
}
```

* `id` — globally unique, prefixed `evt_`. Use it as a **dedup key**, since deliveries may be retried.
* `occurred_at` — when the event occurred, not when it was delivered.
* `data` — the event-specific payload (message/conversation/contact for message events; call/contact/user for call events).

Deliveries also carry `X-Dialbird-Event-Id` and `X-Dialbird-Event-Type` headers.

<Note>
  For `call.*.completed` events, `recording_url`, `voicemail_url`, and `transcript` are always `null` — recordings and voicemails are produced asynchronously after the call ends. They will be delivered via a dedicated `call.recording.completed` event (correlated by call `id`) in a future v1.x release.
</Note>

## Verifying signatures

Each delivery is signed with the `X-Dialbird-Signature` header:

```
X-Dialbird-Signature: t=1736937600,v1=<hmac_sha256_hex>
```

The `v1` value is an HMAC-SHA256, computed over `${t}.${raw_body}` using your subscription's `signing_secret`. Verify it against the **raw** request body before parsing JSON.

```javascript theme={null}
import crypto from "node:crypto";

function verify(rawBody, header, signingSecret) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("="))
  );
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}
```

Reject any delivery whose signature does not match.

## Acknowledging and retries

Respond with any `2xx` to acknowledge a delivery. Respond with `410 Gone` to permanently disable the subscription.

A subscription's `status` reflects its delivery health:

| Status     | Meaning                                                               |
| ---------- | --------------------------------------------------------------------- |
| `active`   | Delivering normally.                                                  |
| `failing`  | Consecutive failures crossed the warning threshold.                   |
| `disabled` | Failures crossed the hard limit, or the receiver returned `410 Gone`. |

`listSubscriptions` exposes `consecutive_failures`, `last_delivered_at`, and `last_failed_at` to help you monitor health.
