Skip to main content

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.

This is the beta webhook API, available in open beta. For the existing webhook system, see Legacy webhooks.
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).
  • Trigger a workflow when a call summary is ready (call.summary.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.
You can send a real, signed test delivery without writing any handler code using POST /webhooks/:id/events/test. See Send a test event.

Common envelope

Every webhook delivery uses the same top-level shape. The type field determines the schema of data.
{
  "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/..." }
  }
}
FieldMeaning
idStable identifier for the event across retries and payload versions. Not a delivery identifier — for delivery-level dedupe, use the webhook-id header.
apiVersionPayload version recorded when the webhook was created.
createdAtWhen the underlying event occurred. The send time is in the webhook-timestamp header.
typeThe event name. Discriminates the schema of data.
data.resourcePrimary business object for the event.
data.contextSurrounding metadata (phone number, conversation, participants, contact lookup, sharing).
data.links.quoQuo 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 for moving existing integrations.

Subscription rules

  • message.*, call.summary.*, and call.transcript.* 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.
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.
AttemptDelay from previousCumulative
1Immediate0
25 seconds5s
35 minutes5m 5s
430 minutes35m 5s
52 hours2h 35m 5s
65 hours7h 35m 5s
710 hours17h 35m 5s
810 hours27h 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.
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

HeaderFormatPurpose
webhook-idstringIdempotency key. Stable across retries.
webhook-timestampunix secondsWhen Quo signed the request.
webhook-signaturev1,<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.

See also