- 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).
Common envelope
Every webhook delivery uses the same top-level shape. Thetype field determines the schema of data.
| 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. |
Versioning policy
Webhooks follow the same dated versioning as the rest of the API, with one addition: a subscription pins its payload version at creation.- Pinning. Set
Quo-Api-Versionwhen callingPOST /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.*andcall.*events accept aresourceIdsfilter (phone number ids, or["*"]).contact.*events are workspace-wide. For contact-only webhooks, omitresourceIds.- Mixed subscriptions are supported.
resourceIdsfilters 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.
Retries
Failed deliveries are retried automatically. Any2xx 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 |
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, acall.transcript.completed may arrive before the corresponding call.summary.completed.
Design handlers to be order-independent:
- Compare
data.resource.updatedAt(orcreatedAtfor 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.
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}. |