> ## 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 Quo webhook API.

The Quo 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](/docs/2026-03-30/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](/docs/2026-03-30/webhook-events/send-a-test-event-to-a-webhook).
</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": { "orgId": "OR123" },
    "links": { "quo": "https://my.quo.com/..." }
  }
}
```

| Field | Meaning |
| - | - |
| `id` | Identifies the event, not the delivery. Every endpoint subscribed to this event receives the same `id`. To deduplicate, use the `webhook-id` header, which differs for each delivery. |
| `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, including the authorizing workspace in `orgId` plus phone number, conversation, participants, contact lookup, and sharing details. |
| `data.links.quo` | Quo app deep link, or `null` when none is available. |

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

## Event, resource, and delivery identifiers

Webhook payloads and delivery records use three identifiers:

| Identifier | Where to find it | Use |
| - | - | - |
| Event ID | `id` in the webhook payload and `eventId` in delivery API responses | Match a webhook payload to its delivery record. There is no `eventId` filter. |
| Resource ID | `data.resource.id` in most payloads, `data.resource.callId` for call summaries, transcripts, and voicemails, and `resourceId` in delivery API responses | Find deliveries related to the same business resource. |
| Delivery ID | `webhook-id` header and `id` in delivery API responses | Deduplicate deliveries, get delivery details, or retry a delivery. |

Quo records correlation data from September 2, 2026. Deliveries created before that date return `null` for `eventId` and `resourceId`, and the `resourceId` filter does not match them. Test deliveries also return `null`.

To investigate a business object, list deliveries with its resource ID. Then use the returned delivery ID for detail or retry requests.

## Versioning policy

Webhooks follow the same [dated versioning](/docs/2026-03-30/versioning) as the rest of the API, with one addition: a subscription pins its payload version at creation.

* **Pinning.** Set `Quo-Api-Version` when calling `POST /webhooks`. That version is recorded for the subscription and used for every delivery it makes.
* **Stability.** A webhook's version never changes after creation. Existing deliveries keep the shape they were created with, even after newer versions ship.
* **Adopting a new version.** Create a new subscription on the newer version. Deliveries to the existing subscription are unaffected.

## 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.

## 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/:deliveryId/retry`. Use the delivery `id` from the list response.

### 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. Unique per delivery, and 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](/docs/2026-03-30/webhooks-signature-validation).

## See also

* [Quickstart](/docs/2026-03-30/webhooks-quickstart)
* [Webhook event payloads](/docs/2026-03-30/webhooks-event-payloads)
* [Validate webhook signatures](/docs/2026-03-30/webhooks-signature-validation)
* [Webhook API reference](/docs/2026-03-30/webhooks/create-a-new-webhook)
