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

# Webhooks

> Subscribe to call, SMS, contact, number-lifecycle, and booking events delivered to your URL.

RevDesk POSTs an event to your URL whenever something happens in your workspace — a call ends, an SMS
is delivered, a number is provisioned. Subscribe once per event type; we deliver as the events occur.

## Subscribe

```bash theme={null}
curl -X POST https://api.revdesk.com/api/v1/webhook_subscriptions \
  -H "Authorization: Bearer $REVDESK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "webhook_url": "https://example.com/hooks/revdesk", "event_type": "call_completed" }'
# → { "id": "whk_…" }
```

| Field         | Required | Description                          |
| ------------- | -------- | ------------------------------------ |
| `webhook_url` | yes      | HTTPS URL that receives the `POST`.  |
| `event_type`  | yes      | One of the event types below.        |
| `workflow_id` | no       | Scope delivery to a single workflow. |
| `campaign_id` | no       | Scope delivery to a single campaign. |

## Unsubscribe

```bash theme={null}
curl -X DELETE https://api.revdesk.com/api/v1/webhook_subscriptions/whk_… \
  -H "Authorization: Bearer $REVDESK_API_KEY"
# → 204 No Content
```

## Event types

<Note>
  `call_completed` fires on every call end. The **disposition** events below fire *in addition*, only
  on the matching outcome — subscribe to them to branch (retry, SMS fallback) without parsing call
  records. Answered/successful calls emit no disposition event.
</Note>

| `event_type`                          | Fires when                                                                                                                                                                              |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `new_call`                            | A call starts.                                                                                                                                                                          |
| `call_completed`                      | A call ends (any outcome).                                                                                                                                                              |
| `call_no_answer`                      | A call ended unanswered (rang out).                                                                                                                                                     |
| `call_voicemail`                      | A call reached voicemail / an answering machine.                                                                                                                                        |
| `call_busy`                           | The destination was busy.                                                                                                                                                               |
| `call_failed`                         | A call failed (declined, unreachable, carrier rejection).                                                                                                                               |
| `contact_replied`                     | An inbound SMS was received.                                                                                                                                                            |
| `sms_sent`                            | An outbound SMS was accepted by the carrier.                                                                                                                                            |
| `sms_delivered`                       | An outbound SMS was delivered to the handset.                                                                                                                                           |
| `sms_failed`                          | An outbound SMS failed / was undelivered.                                                                                                                                               |
| `contact_created`                     | A new contact was created.                                                                                                                                                              |
| `number_purchased`                    | A phone number finished provisioning and went active.                                                                                                                                   |
| `number_released`                     | A phone number was released from your workspace.                                                                                                                                        |
| `texting_registration_status_changed` | Your A2P texting registration (brand or campaign) was approved or rejected by carriers. On campaign approval the payload lists `texting_enabled_numbers` — the moment to start sending. |
| `new_appointment`                     | A booking was created.                                                                                                                                                                  |
| `workflow_contact_completed`          | A contact finished a workflow.                                                                                                                                                          |

## Delivery

Each event is a `POST` with a JSON body to your `webhook_url`. Delivery is fire-and-forget; return a
`2xx` quickly and do any heavy work asynchronously. Subscribe to the same event type multiple times
to fan out to multiple URLs.

<Warning>
  **Two delivery shapes.** Call-disposition and SMS events are delivered **flat** (the event object
  *is* the request body) and are **not signed**. Number-lifecycle events are delivered **enveloped**
  (`triggerEvent` / `createdAt` / `payload`) and **HMAC-signed**. Match the shape to the event family
  you subscribed to — see the examples below.
</Warning>

### Signature verification (number-lifecycle only)

Number-lifecycle deliveries carry an `X-revdesk-Signature-256` header. It is the lowercase hex
`HMAC-SHA256` of the **raw request body**, keyed with your subscription's signing secret. Compute the
same HMAC over the bytes you receive and compare with a constant-time equality check; reject on
mismatch. If the subscription has no signing secret configured, the header carries the literal value
`no-secret-provided` instead of a signature.

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

function verify(rawBody, header, secret) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
```

Call-disposition and SMS events are **unsigned** — they carry no signature header. Restrict those
handlers to a secret path or IP allowlist if you need to authenticate the sender.

### Payload examples

Call disposition (`call_no_answer`, `call_voicemail`, `call_busy`, `call_failed`) — **flat, unsigned**:

```json theme={null}
{
  "id": "call_abc123",
  "from_number": "+15551234567",
  "to_number": "+15557654321",
  "direction": "outbound",
  "duration_seconds": 0,
  "status": "VOICEMAIL",
  "failure_code": "voicemail_reached",
  "failure_category": "voicemail",
  "disconnection_reason": null,
  "created_at": "2026-06-30T19:30:00.000Z"
}
```

Outbound SMS (`sms_sent`, `sms_delivered`, `sms_failed`) — **flat, unsigned**:

```json theme={null}
{
  "id": "msg_uuid",
  "phone_number": "+15557654321",
  "from_number": "+15551234567",
  "direction": "outbound",
  "status": "delivered",
  "contact_token": "…",
  "provider_id": "…",
  "error_message": null,
  "channel": "sms",
  "created_at": "2026-06-30T19:30:00.000Z"
}
```

Number lifecycle (`number_purchased`, `number_released`) — **enveloped + HMAC-signed**. The event
data is nested under `payload`, and `triggerEvent` is the internal event name (`number.purchased` /
`number.released`):

```json theme={null}
{
  "triggerEvent": "number.purchased",
  "createdAt": "2026-06-30T19:30:00.000Z",
  "payload": {
    "id": 4821,
    "phone_number": "+15551234567",
    "area_code": "555",
    "status": "ACTIVE",
    "organization_id": "…"
  }
}
```

Delivered with header `X-revdesk-Signature-256: <hex HMAC-SHA256 of the raw body>`.
