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

# Migrating from legacy

> What changes between legacy and beta webhooks, and how to roll over without downtime.

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.

<Note>
  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.
</Note>

## 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 lifecycle events, call recordings, call summaries, call transcripts, voicemails, and contacts.              |
| 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.                                                           |

<Warning>
  **The signature schemes are not interchangeable.** Code that verifies the legacy `OpenPhone-Signature` header will reject every beta delivery. You must update verification before pointing beta traffic at an existing endpoint — see [Validate webhook signatures](/mdx/beta/webhooks-signature-validation).
</Warning>

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

```json Before (legacy) theme={null}
{
  "id": "EV0ea54...",
  "apiVersion": "v4",
  "type": "message.received",
  "data": {
    "object": {
      "id": "AC123",
      "text": "hello",
      "direction": "incoming",
      "phoneNumberId": "PN123",
      "userId": "US123",
      "contactIds": ["CT123"]
    },
    "deepLink": "https://my.quo.com/inbox/..."
  }
}
```

```json After (beta) theme={null}
{
  "id": "EV-message-received",
  "apiVersion": "2026-03-30",
  "type": "message.received",
  "data": {
    "resource": {
      "id": "AC123",
      "text": "hello",
      "direction": "incoming",
      "status": "received",
      "createdAt": "2026-04-13T12:00:00.000Z"
    },
    "context": {
      "phoneNumberId": "PN123",
      "userId": "US123",
      "contacts": { "ids": ["CT123"], "lookupStatus": "matched" },
      "senderIdentifier": "+15550001111",
      "recipientIdentifiers": ["+15550002222"]
    },
    "links": { "quo": "https://my.quo.com/inbox/..." }
  }
}
```

Three structural changes to notice:

1. Resource identifiers (id, text, status, createdAt) live under `resource`.
2. Routing context (phoneNumberId, userId, contacts) lives under `context`.
3. 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.

<Steps>
  <Step title="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](/mdx/beta/webhooks-signature-validation). Your endpoint still verifies legacy `OpenPhone-Signature` deliveries normally — this step adds a second verifier, doesn't replace the first.

    *Running:* legacy webhooks only.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Update your handler for beta idempotency">
    Add idempotency keyed on the `webhook-id` header for beta delivery retries (see [Idempotency](/mdx/beta/webhooks-overview#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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Compare for parity">
    Use [`GET /webhooks/:id/events`](/mdx/beta/webhooks-api-reference#list-deliveries) 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.
  </Step>

  <Step title="Disable the legacy webhook">
    Once parity is verified, disable the legacy webhook in the Quo app or via the legacy API. Your endpoint now serves only beta traffic.

    *Running:* beta only.
  </Step>
</Steps>

### If something goes wrong

* **Verification failures on every beta delivery.** Most likely a body re-serialization issue. See [Framework gotchas](/mdx/beta/webhooks-signature-validation#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-id` header. 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-id` to 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.

In either case, treat any `2xx` response as accepting the delivery. Returning a non-`2xx` from the wrong handler will trigger retries you don't want.

## See also

* [Overview](/mdx/beta/webhooks-overview)
* [Quickstart](/mdx/beta/webhooks-quickstart)
* [Webhook API reference](/mdx/beta/webhooks-api-reference)
* [Webhook event payloads](/mdx/beta/webhooks-event-payloads)
* [Validate webhook signatures](/mdx/beta/webhooks-signature-validation)
