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

# Overview

> Concepts, delivery semantics, and versioning for the beta webhook API.

<Note>
  This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/mdx/guides/webhooks).
</Note>

The beta webhook API delivers real-time events to an HTTPS endpoint you control. Use it to:

* Update your CRM when an inbound SMS arrives (`message.received`).
* Log calls in your system as soon as they end (`call.completed`), with AI summaries and transcripts following (`call.summary.completed`, `call.transcript.completed`).
* Sync contact changes between Quo and your system (`contact.updated`).

If you want to ship a working integration first and read concepts second, jump to the [Quickstart](/mdx/beta/webhooks-quickstart).

<Tip>
  You can send a real, signed test delivery without writing any handler code using `POST /webhooks/:id/events/test`. See [Send a test event](/mdx/beta/webhooks-api-reference#send-a-test-event).
</Tip>

## Common envelope

Every webhook delivery uses the same top-level shape. The `type` field determines the schema of `data`.

```json theme={null}
{
  "id": "EV123",
  "apiVersion": "2026-03-30",
  "createdAt": "2026-04-13T12:00:00.000Z",
  "type": "call.summary.completed",
  "data": {
    "resource": {},
    "context": {},
    "links": { "quo": "https://my.quo.com/..." }
  }
}
```

| Field            | Meaning                                                                                                                                                  |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`             | Stable identifier for the event across retries and payload versions. Not a delivery identifier — for delivery-level dedupe, use the `webhook-id` header. |
| `apiVersion`     | Payload version recorded when the webhook was created.                                                                                                   |
| `createdAt`      | When the underlying event occurred. The send time is in the `webhook-timestamp` header.                                                                  |
| `type`           | The event name. Discriminates the schema of `data`.                                                                                                      |
| `data.resource`  | Primary business object for the event.                                                                                                                   |
| `data.context`   | Surrounding metadata (phone number, conversation, participants, contact lookup, sharing).                                                                |
| `data.links.quo` | Quo app deep link, or `null` when none is available.                                                                                                     |

Treat all event, delivery, and resource ids as opaque strings.

## Versioning policy

* **Format.** Versions are date strings (`YYYY-MM-DD`). The current version is `2026-03-30`.
* **Stability.** A webhook's version is recorded at creation and never changes for that subscription. Existing deliveries continue using the version they were created with.
* **Pinning.** Set `x-quo-api-version` when calling `POST /webhooks`. Future versions may add event types or make breaking payload changes; new deliveries use the older shape until you create a new subscription on the newer version.

See [Migrating from legacy](/mdx/beta/webhooks-differences-from-current) for moving existing integrations.

## Subscription rules

* `message.*` and `call.*` events accept a `resourceIds` filter (phone number ids, or `["*"]`).
* `contact.*` events are workspace-wide. For contact-only webhooks, omit `resourceIds`.
* Mixed subscriptions are supported. `resourceIds` filters the activity events above; contact events are always delivered workspace-wide.
* A workspace can have at most 50 webhooks created with the beta API.

## Delivery semantics

### Idempotency

Quo will retry deliveries on any non-`2xx` response, so your endpoint must tolerate seeing the same delivery more than once. Use the `webhook-id` header as an idempotency key — it is stable across retries and distinct from the envelope `id`.

```ts theme={null}
if (await store.has(req.header("webhook-id"))) return res.status(200).end()
await store.add(req.header("webhook-id"), { ttlSeconds: 60 * 60 * 28 })
```

Store processed ids for at least 28 hours - long enough to cover the full retry window.

### Retries

Failed deliveries are retried automatically. Any `2xx` response marks a delivery accepted; any non-`2xx` response triggers the next retry. After all retries fail, the delivery is marked failed and not retried further.

| Attempt | Delay from previous | Cumulative |
| ------- | ------------------- | ---------- |
| 1       | Immediate           | 0          |
| 2       | 5 seconds           | 5s         |
| 3       | 5 minutes           | 5m 5s      |
| 4       | 30 minutes          | 35m 5s     |
| 5       | 2 hours             | 2h 35m 5s  |
| 6       | 5 hours             | 7h 35m 5s  |
| 7       | 10 hours            | 17h 35m 5s |
| 8       | 10 hours            | 27h 35m 5s |

If every attempt fails, the delivery is marked failed after roughly 27 hours and 35 minutes from the initial attempt.

You can also trigger a manual retry with `POST /webhooks/:id/events/:eventId/retry`.

### Ordering

Delivery order is not guaranteed. Events can arrive out of order across event families and, occasionally, within a single resource — for example, a `call.transcript.completed` may arrive before the corresponding `call.summary.completed`.

Design handlers to be order-independent:

* Compare `data.resource.updatedAt` (or `createdAt` for terminal events) against your stored state and ignore stale events.
* Don't drive state machines from arrival order. Treat each event as a freshness check on the resource it references.

```ts theme={null}
const incoming = event.data.resource
const stored = await db.contacts.get(incoming.id)
if (stored && stored.updatedAt >= incoming.updatedAt) return  // stale, ignore
await db.contacts.upsert(incoming)
```

## Headers on every delivery

| Header              | Format                                  | Purpose                                                         |
| ------------------- | --------------------------------------- | --------------------------------------------------------------- |
| `webhook-id`        | string                                  | Idempotency key. Stable across retries.                         |
| `webhook-timestamp` | unix seconds                            | When Quo signed the request.                                    |
| `webhook-signature` | `v1,<base64>` (space-separated entries) | HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw-body}`. |

Always verify the signature before trusting the body. See [Validate webhook signatures](/mdx/beta/webhooks-signature-validation).

## See also

* [Quickstart](/mdx/beta/webhooks-quickstart)
* [Webhook event payloads](/mdx/beta/webhooks-event-payloads)
* [Validate webhook signatures](/mdx/beta/webhooks-signature-validation)
* [Webhook API reference](/mdx/beta/webhooks-api-reference)
* [Migrating from legacy](/mdx/beta/webhooks-differences-from-current)
