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

# Webhook API reference

> Endpoints for creating, managing, testing, and inspecting beta webhooks.

<Note>
  This is the beta webhook API, available in open beta. For the existing webhook system, see [Legacy webhooks](/mdx/guides/webhooks).
</Note>

Use these endpoints to create, manage, test, and inspect webhooks created with the beta API. Endpoints are grouped into CRUD (create/read/update/delete) and Testing & debugging.

<Note>
  Webhooks created through these endpoints are managed through these endpoints and do not appear in the Quo app webhook settings during the beta.
</Note>

Every request requires your Public API key and the API version header:

```bash theme={null}
Authorization: YOUR_API_KEY
x-quo-api-version: 2026-03-30
```

## Endpoint index

CRUD:

| Method   | Path                   | Purpose                                                  |
| -------- | ---------------------- | -------------------------------------------------------- |
| `GET`    | `/webhooks`            | [List webhooks](#list-webhooks) for the workspace.       |
| `POST`   | `/webhooks`            | [Create a webhook](#create-a-webhook).                   |
| `GET`    | `/webhooks/:id`        | [Get a webhook](#get-a-webhook).                         |
| `PATCH`  | `/webhooks/:id`        | [Update a webhook](#update-a-webhook).                   |
| `DELETE` | `/webhooks/:id`        | Delete a webhook.                                        |
| `POST`   | `/webhooks/:id/rotate` | [Rotate the signing secret](#rotate-the-signing-secret). |

Testing & debugging:

| Method | Path                                  | Purpose                                                             |
| ------ | ------------------------------------- | ------------------------------------------------------------------- |
| `POST` | `/webhooks/:id/events/test`           | [Send a test event](#send-a-test-event) to the webhook URL.         |
| `GET`  | `/webhooks/:id/events`                | [List deliveries](#list-deliveries).                                |
| `GET`  | `/webhooks/:id/events/:eventId`       | [Get delivery detail](#get-delivery-detail) including all attempts. |
| `POST` | `/webhooks/:id/events/:eventId/retry` | [Retry a delivery](#retry-a-delivery).                              |

## Supported event types

| Event type                                                                                 | When it fires                                                                                                                                                         |
| ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`message.received`](/mdx/beta/webhooks-event-payloads#message-received)                   | An inbound SMS, MMS, or message was received by a Quo number.                                                                                                         |
| [`message.delivered`](/mdx/beta/webhooks-event-payloads#message-delivered)                 | An outbound message was delivered.                                                                                                                                    |
| [`call.ringing`](/mdx/beta/webhooks-event-payloads#call-ringing)                           | A call started ringing. Fires for incoming and outgoing.                                                                                                              |
| [`call.answered`](/mdx/beta/webhooks-event-payloads#call-answered)                         | A call was connected. `answeredByUserId` identifies the Quo-side user associated with the answer when known. Outgoing calls fire this event for voicemail pickup too. |
| [`call.completed`](/mdx/beta/webhooks-event-payloads#call-completed)                       | A call ended. Terminal lifecycle event with final status and duration.                                                                                                |
| [`call.forwarded`](/mdx/beta/webhooks-event-payloads#call-forwarded)                       | An incoming call was forwarded. Includes the forwarding phone numbers.                                                                                                |
| [`call.missed`](/mdx/beta/webhooks-event-payloads#call-missed)                             | An incoming call ended without being answered.                                                                                                                        |
| [`call.recording.completed`](/mdx/beta/webhooks-event-payloads#call-recording-completed)   | A call recording finished processing.                                                                                                                                 |
| [`call.summary.completed`](/mdx/beta/webhooks-event-payloads#call-summary-completed)       | A call summary finished generating.                                                                                                                                   |
| [`call.transcript.completed`](/mdx/beta/webhooks-event-payloads#call-transcript-completed) | A call transcript finished processing.                                                                                                                                |
| [`call.voicemail.completed`](/mdx/beta/webhooks-event-payloads#call-voicemail-completed)   | A voicemail was left and finished processing.                                                                                                                         |
| [`contact.updated`](/mdx/beta/webhooks-event-payloads#contact-updated)                     | A contact was created or its fields changed.                                                                                                                          |
| [`contact.deleted`](/mdx/beta/webhooks-event-payloads#contact-deleted)                     | A contact was deleted.                                                                                                                                                |

For payload shapes, see [Webhook event payloads](/mdx/beta/webhooks-event-payloads).

## List webhooks

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks \
    -H "Authorization: YOUR_API_KEY" \
    -H "x-quo-api-version: 2026-03-30"
  ```

  ```ts Node theme={null}
  const res = await fetch("https://api.openphone.com/webhooks", {
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "x-quo-api-version": "2026-03-30",
    },
  })
  const { data } = await res.json()
  ```
</CodeGroup>

Returns all webhooks created through the beta webhook endpoints for the workspace. This endpoint is not paginated.

```json theme={null}
{
  "data": [
    {
      "id": "123",
      "orgId": "OR123",
      "label": "Production webhook",
      "status": "enabled",
      "url": "https://example.com/webhooks/quo",
      "createdAt": "2026-04-13T12:00:00.000Z",
      "updatedAt": "2026-04-13T12:00:00.000Z",
      "events": ["call.summary.completed", "contact.updated"],
      "resourceIds": ["PNabc123"],
      "apiVersion": "2026-03-30"
    }
  ]
}
```

## Get a webhook

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks/123 \
    -H "Authorization: YOUR_API_KEY" \
    -H "x-quo-api-version: 2026-03-30"
  ```

  ```ts Node theme={null}
  const res = await fetch(`https://api.openphone.com/webhooks/${id}`, {
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "x-quo-api-version": "2026-03-30",
    },
  })
  const { data } = await res.json()
  ```
</CodeGroup>

```json theme={null}
{
  "data": {
    "id": "123",
    "orgId": "OR123",
    "label": "Production webhook",
    "status": "enabled",
    "url": "https://example.com/webhooks/quo",
    "createdAt": "2026-04-13T12:00:00.000Z",
    "updatedAt": "2026-04-13T12:00:00.000Z",
    "events": ["call.summary.completed", "contact.updated"],
    "resourceIds": ["PNabc123"],
    "apiVersion": "2026-03-30"
  }
}
```

## Create a webhook

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks \
    -X POST \
    -H "Authorization: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -H "x-quo-api-version: 2026-03-30" \
    -d '{
      "url": "https://example.com/webhooks/quo",
      "events": ["call.completed", "message.received", "contact.updated"],
      "resourceIds": ["PNabc123"],
      "label": "Production webhook",
      "status": "enabled"
    }'
  ```

  ```ts Node theme={null}
  const res = await fetch("https://api.openphone.com/webhooks", {
    method: "POST",
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "Content-Type": "application/json",
      "x-quo-api-version": "2026-03-30",
    },
    body: JSON.stringify({
      url: "https://example.com/webhooks/quo",
      events: ["call.completed", "message.received", "contact.updated"],
      resourceIds: ["PNabc123"],
      label: "Production webhook",
      status: "enabled",
    }),
  })
  const { data } = await res.json()
  ```
</CodeGroup>

| Field         | Type                      | Required? | Description                                                                                                                                |
| ------------- | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `url`         | string                    | **Yes**   | Public HTTPS endpoint that receives webhook deliveries.                                                                                    |
| `events`      | string\[]                 | **Yes**   | One or more [supported event types](#supported-event-types). Message, call, and contact events can be mixed in a single subscription.      |
| `resourceIds` | string\[]                 | No        | Phone number ids for filtering message and call events, or `["*"]` for all. Defaults to `["*"]`. Contact events are always workspace-wide. |
| `label`       | string                    | No        | Human-readable label.                                                                                                                      |
| `status`      | `"enabled" \| "disabled"` | No        | Defaults to `enabled`.                                                                                                                     |

The response includes the `whsec_…` signing secret. Save it as an environment variable — see [Validate webhook signatures](/mdx/beta/webhooks-signature-validation#webhook-key-format).

```json theme={null}
{
  "data": {
    "id": "123",
    "orgId": "OR123",
    "label": "Production webhook",
    "status": "enabled",
    "url": "https://example.com/webhooks/quo",
    "key": "whsec_...",
    "createdAt": "2026-04-13T12:00:00.000Z",
    "updatedAt": "2026-04-13T12:00:00.000Z",
    "events": ["call.completed", "message.received", "contact.updated"],
    "resourceIds": ["PNabc123"],
    "apiVersion": "2026-03-30"
  }
}
```

## Update a webhook

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks/123 \
    -X PATCH \
    -H "Authorization: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -H "x-quo-api-version: 2026-03-30" \
    -d '{
      "events": ["call.summary.completed", "contact.updated"],
      "resourceIds": ["PNabc123"],
      "label": "Updated webhook"
    }'
  ```

  ```ts Node theme={null}
  const res = await fetch(`https://api.openphone.com/webhooks/${id}`, {
    method: "PATCH",
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "Content-Type": "application/json",
      "x-quo-api-version": "2026-03-30",
    },
    body: JSON.stringify({
      events: ["call.summary.completed", "contact.updated"],
      resourceIds: ["PNabc123"],
      label: "Updated webhook",
    }),
  })
  const { data } = await res.json()
  ```
</CodeGroup>

All fields are optional. Provide only the fields you want to change.

| Field         | Type                      | Description                                                                                                      |
| ------------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `url`         | string                    | Replaces the webhook URL.                                                                                        |
| `events`      | string\[]                 | Replaces the subscribed event types.                                                                             |
| `resourceIds` | string\[] \| null         | Replaces the phone number filters for message and call events. Send `null`, `[]`, or `["*"]` to clear filtering. |
| `label`       | string \| null            | Replaces the label. Send `null` to clear it.                                                                     |
| `status`      | `"enabled" \| "disabled"` | Enables or disables delivery.                                                                                    |

## Delete a webhook

```bash theme={null}
curl https://api.openphone.com/webhooks/123 \
  -X DELETE \
  -H "Authorization: YOUR_API_KEY" \
  -H "x-quo-api-version: 2026-03-30"
```

Returns `204 No Content` on success.

## Rotate the signing secret

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks/123/rotate \
    -X POST \
    -H "Authorization: YOUR_API_KEY" \
    -H "x-quo-api-version: 2026-03-30"
  ```

  ```ts Node theme={null}
  const res = await fetch(`https://api.openphone.com/webhooks/${id}/rotate`, {
    method: "POST",
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "x-quo-api-version": "2026-03-30",
    },
  })
  const { data } = await res.json()  // { key: "whsec_..." }
  ```
</CodeGroup>

```json theme={null}
{
  "data": { "key": "whsec_..." }
}
```

Store the new key and use it for future signature verification. See [Validate webhook signatures](/mdx/beta/webhooks-signature-validation).

## Send a test event

Sends a real, signed delivery to your webhook URL — the same as a production event — and returns the sample payload inline so you can inspect it without waiting for the delivery to land.

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks/123/events/test \
    -X POST \
    -H "Authorization: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -H "x-quo-api-version: 2026-03-30" \
    -d '{ "eventType": "message.received" }'
  ```

  ```ts Node theme={null}
  const res = await fetch(`https://api.openphone.com/webhooks/${id}/events/test`, {
    method: "POST",
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "Content-Type": "application/json",
      "x-quo-api-version": "2026-03-30",
    },
    body: JSON.stringify({ eventType: "message.received" }),
  })
  const sample = await res.json()
  ```
</CodeGroup>

`eventType` must be one of the [supported event types](#supported-event-types). The response is the sample payload sent to your webhook URL. Delivery is asynchronous; use [`GET /webhooks/:id/events`](#list-deliveries) to find the delivery id and inspect what your endpoint returned.

```json theme={null}
{
  "id": "EV-test-message-received",
  "apiVersion": "2026-03-30",
  "createdAt": "2026-03-30T18:00:00.000Z",
  "type": "message.received",
  "data": {
    "resource": {
      "id": "ACsampleactivity0000000000000000",
      "direction": "incoming",
      "text": "Hello from Quo! This is a sample message.",
      "status": "received",
      "createdAt": "2026-03-30T18:00:00.000Z"
    },
    "context": {
      "phoneNumberId": "PNsamplephonenumber000000000000",
      "conversationId": "CNsampleconversation000000000000",
      "userId": "USsampleus",
      "contacts": { "ids": ["CTsampleContact01234"], "lookupStatus": "matched" },
      "senderIdentifier": "+15555551234",
      "recipientIdentifiers": ["+15555555678"]
    },
    "links": { "quo": "https://my.quo.com/..." }
  }
}
```

## List deliveries

<CodeGroup>
  ```bash curl theme={null}
  curl "https://api.openphone.com/webhooks/123/events?limit=10" \
    -H "Authorization: YOUR_API_KEY" \
    -H "x-quo-api-version: 2026-03-30"
  ```

  ```ts Node theme={null}
  const params = new URLSearchParams({ limit: "10" })
  const res = await fetch(`https://api.openphone.com/webhooks/${id}/events?${params}`, {
    headers: {
      "Authorization": process.env.QUO_API_KEY!,
      "x-quo-api-version": "2026-03-30",
    },
  })
  const { data, nextCursor } = await res.json()
  ```
</CodeGroup>

| Parameter       | Type                                              | Description                                       |
| --------------- | ------------------------------------------------- | ------------------------------------------------- |
| `limit`         | number                                            | Page size.                                        |
| `after`         | string                                            | Cursor from the previous response's `nextCursor`. |
| `status`        | `"success" \| "pending" \| "sending" \| "failed"` | Filter by delivery status.                        |
| `eventTypes`    | string\[]                                         | Restrict results to specific event types.         |
| `createdBefore` | ISO-8601 string                                   | Only include deliveries created before this time. |
| `createdAfter`  | ISO-8601 string                                   | Only include deliveries created after this time.  |

Delivery statuses:

| Status    | Meaning                                                          |
| --------- | ---------------------------------------------------------------- |
| `success` | At least one delivery attempt returned a `2xx` response.         |
| `pending` | Delivery is queued and has not attempted yet.                    |
| `sending` | Delivery is in progress or waiting for a retry attempt.          |
| `failed`  | All retry attempts have been exhausted without a `2xx` response. |

```json theme={null}
{
  "data": [
    {
      "id": "msg_2abcDEFghiJKLmnoPQRstu",
      "eventType": "message.received",
      "status": "success",
      "nextAttemptAt": null,
      "createdAt": "2026-04-13T12:00:00.000Z"
    }
  ],
  "nextCursor": "eyJsYXN0SWQiOiJtc2dfMmFiY0RFRmdoaUpLTG1ub1BRUnN0dSJ9"
}
```

## Get delivery detail

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks/123/events/msg_2abcDEFghiJKLmnoPQRstu \
    -H "Authorization: YOUR_API_KEY" \
    -H "x-quo-api-version: 2026-03-30"
  ```

  ```ts Node theme={null}
  const res = await fetch(
    `https://api.openphone.com/webhooks/${id}/events/${eventId}`,
    {
      headers: {
        "Authorization": process.env.QUO_API_KEY!,
        "x-quo-api-version": "2026-03-30",
      },
    }
  )
  const { data } = await res.json()
  ```
</CodeGroup>

Delivery detail includes the request body and all attempts, ordered most-recent first.

```json theme={null}
{
  "data": {
    "id": "msg_2abcDEFghiJKLmnoPQRstu",
    "eventType": "message.received",
    "createdAt": "2026-04-13T12:00:00.000Z",
    "requestBody": {
      "id": "EV123",
      "apiVersion": "2026-03-30",
      "createdAt": "2026-04-13T12:00:00.000Z",
      "type": "message.received",
      "data": {
        "resource": {
          "id": "AC-message",
          "direction": "incoming",
          "text": "hello",
          "status": "received",
          "createdAt": "2026-04-13T12:00:00.000Z"
        },
        "context": {
          "phoneNumberId": "PN123",
          "conversationId": "CN123",
          "userId": "US123",
          "contacts": { "ids": ["CT123"], "lookupStatus": "matched" },
          "senderIdentifier": "+15550001111",
          "recipientIdentifiers": ["+15550002222"]
        },
        "links": { "quo": "https://my.quo.com/inbox/..." }
      }
    },
    "attempts": [
      {
        "id": "atmpt_2abcDEFghiJKLmnoPQRstu",
        "timestamp": "2026-04-13T12:00:01.000Z",
        "status": "success",
        "responseStatusCode": 200,
        "responseBody": "{\"ok\":true}",
        "responseDurationMs": 123,
        "triggerType": "scheduled",
        "url": "https://example.com/webhooks/quo"
      }
    ]
  }
}
```

## Retry a delivery

<CodeGroup>
  ```bash curl theme={null}
  curl https://api.openphone.com/webhooks/123/events/msg_2abcDEFghiJKLmnoPQRstu/retry \
    -X POST \
    -H "Authorization: YOUR_API_KEY" \
    -H "x-quo-api-version: 2026-03-30"
  ```

  ```ts Node theme={null}
  const res = await fetch(
    `https://api.openphone.com/webhooks/${id}/events/${eventId}/retry`,
    {
      method: "POST",
      headers: {
        "Authorization": process.env.QUO_API_KEY!,
        "x-quo-api-version": "2026-03-30",
      },
    }
  )
  ```
</CodeGroup>

The retry is queued asynchronously and returns `202 Accepted`. Use [`GET /webhooks/:id/events/:eventId`](#get-delivery-detail) to inspect the new attempt.

## See also

* [Quickstart](/mdx/beta/webhooks-quickstart)
* [Overview](/mdx/beta/webhooks-overview)
* [Webhook event payloads](/mdx/beta/webhooks-event-payloads)
* [Validate webhook signatures](/mdx/beta/webhooks-signature-validation)
* [Migrating from legacy](/mdx/beta/webhooks-differences-from-current)
