The beta webhook API is not a drop-in replacement for the legacy webhook system. Management, signing, and payload shapes all change. This page summarizes the differences and walks through a no-downtime migration.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.
Quo currently has separate webhook systems for app-managed webhooks and API-managed webhooks. Webhooks created in the Quo app are managed in the app, and webhooks created through the current API are managed through the API. During open beta, beta webhooks are managed separately through the beta webhook endpoints and do not appear in Quo app settings. We plan to unify API and app webhook management.
What changes
| Area | Legacy | Beta |
|---|---|---|
| Management | App settings (for app-managed webhooks) and the existing API (for API-managed webhooks). | Beta webhooks are created and managed through the beta webhook endpoints. We plan to unify API and app webhook management. |
| Create endpoints | Four near-duplicate create endpoints split by event family (/v1/webhooks/messages, /v1/webhooks/calls, /v1/webhooks/call-summaries, /v1/webhooks/call-transcripts). | One POST /webhooks with an events array. Mix message, call, and contact subscriptions in a single webhook. |
| Event coverage | Calls, messages, contacts, transcripts. | Messages, call summaries, call transcripts, contacts. Call lifecycle planned later. |
| Payload shape | data.object with object-specific fields. | data.resource + data.context + data.links envelope. |
| Filtering | Per-webhook event subscription, per-phone-number for some event types. | resourceIds filter for message and call events; contact events workspace-wide. |
| Signature header | OpenPhone-Signature. | webhook-id + webhook-timestamp + webhook-signature (Standard-Webhooks-compatible). |
| Signing secret | OpenPhone-format secret. | whsec_… base64 secret, compatible with the Svix SDK. |
| Versioning | Single static API version. | Date-versioned via x-quo-api-version. Pinned per subscription at creation. |
| Delivery inspection | None. | Test events, delivery history, per-attempt detail, manual retry. |
Field remap
The biggest payload change is the split between the primary record and surrounding context.| Legacy field | Beta field |
|---|---|
data.object.id | data.resource.id |
data.object.text | data.resource.text |
data.object.phoneNumberId | data.context.phoneNumberId |
data.object.userId | data.context.userId |
data.object.contactIds | data.context.contacts.ids |
data.deepLink | data.links.quo |
Side-by-side: message.received
Before (legacy)
After (beta)
- Resource identifiers (id, text, status, createdAt) live under
resource. - Routing context (phoneNumberId, userId, contacts) lives under
context. - Deep links live under
links.quo— and are explicitly nullable.
Rollout procedure
The recommended migration runs both webhook systems side by side, compares deliveries, and cuts over once you’ve verified parity. Each step lists what’s running where.Update verification first
Update your endpoint to accept the new
webhook-id / webhook-timestamp / webhook-signature headers and a whsec_… secret. See Validate webhook signatures. Your endpoint still verifies legacy OpenPhone-Signature deliveries normally — this step adds a second verifier, doesn’t replace the first.Running: legacy webhooks only.Create the beta subscription, disabled
Create a beta webhook with
status: "disabled" and the same target URL. This pins the subscription to 2026-03-30 and gives you a whsec_… secret without firing any traffic yet.Running: legacy webhooks only.Update your handler for beta idempotency
Add idempotency keyed on the
webhook-id header for beta delivery retries (see Idempotency). If legacy and beta both send the same business event, use business-field correlation to avoid duplicate side effects across systems.Running: legacy webhooks only.Enable the beta subscription
Flip the beta subscription to
status: "enabled". Both systems now deliver events; your handler verifies both signature schemes and dedupes beta retries.Running: legacy + beta in parallel.Compare for parity
Use
GET /webhooks/:id/events to inspect beta deliveries and confirm they match what you expected. Common things to verify: data.context.contacts.ids matches your stored contactIds; data.links.quo is non-null where you previously used deepLink; senderIdentifier and recipientIdentifiers look right.Running: legacy + beta in parallel.If something goes wrong
- Verification failures on every beta delivery. Most likely a body re-serialization issue. See Framework gotchas.
- Duplicate processing observed. For beta retries, confirm your idempotency store TTL covers the full retry window and that you keyed on the
webhook-idheader. For duplicate processing across legacy and beta, correlate on stable business fields such as message id, call id, contact id, event type, and event timestamp. - Need to roll back. Set the beta subscription
status: "disabled". Legacy continues firing because it was never disabled until step 6.
Dual-running both systems
While both systems are active, the same business event may arrive through both systems. Two patterns work:- Single endpoint, single handler. Your handler verifies whichever signature header is present. Use
webhook-idto dedupe beta retries, and use business-field correlation if you need to suppress duplicate processing across legacy and beta deliveries. - Separate endpoints. Point the beta subscription at a distinct URL like
/webhooks/quo-beta. This keeps verification logic isolated and makes traffic patterns easy to compare during cutover.
2xx response as accepting the delivery. Returning a non-2xx from the wrong handler will trigger retries you don’t want.