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

# Quickstart

> Set up a beta webhook in five minutes.

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

This guide takes you from zero to a verified, production-shaped webhook delivery in five minutes. Read [Overview](/mdx/beta/webhooks-overview) for the underlying delivery semantics, payload anatomy, and versioning policy.

## Before you start

You'll need:

* A Quo Public API key with permission to manage webhooks.
* An HTTPS endpoint you control. For local development, use a tunnel such as `ngrok` or `cloudflared`.
* A runtime that gives you the **raw, unparsed request body**. This is required for signature verification — see [Validate webhook signatures](/mdx/beta/webhooks-signature-validation) for framework-specific notes.

<Tip>
  You can send a real, signed test event before any code is written using `POST /webhooks/:id/events/test`. See step 4 below.
</Tip>

## 1. Create the webhook

Pin the subscription to the current API version with `x-quo-api-version`. The version is recorded once when the webhook is created and used for every subsequent delivery.

```bash 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": ["message.received"],
    "label": "Quickstart webhook"
  }'
```

Save the `key` field from the response — that's your `whsec_…` signing secret. Treat it like a credential: store it as an environment variable, never in source.

## 2. Receive the delivery

Each delivery sets three headers and a JSON body. Verify the signature against the **raw bytes** of the body, then parse.

| Header              | Purpose                                                  |
| ------------------- | -------------------------------------------------------- |
| `webhook-id`        | Stable delivery identifier. Use as your idempotency key. |
| `webhook-timestamp` | Unix seconds when Quo signed the request.                |
| `webhook-signature` | Space-separated `v1,<base64-signature>` entries.         |

## 3. Verify and handle the event

The Svix SDK accepts Quo's headers and `whsec_…` key format unchanged. Reject any delivery whose timestamp is more than five minutes off from your server clock to defeat replay.

<CodeGroup>
  ```ts Node theme={null}
  import { Webhook } from "svix"
  import express from "express"

  const app = express()
  const secret = process.env.QUO_WEBHOOK_KEY ?? ""

  app.post(
    "/webhooks/quo",
    express.raw({ type: "application/json" }),
    (req, res) => {
      const deliveryId = req.header("webhook-id")!
      const headers = {
        "webhook-id": deliveryId,
        "webhook-timestamp": req.header("webhook-timestamp")!,
        "webhook-signature": req.header("webhook-signature")!,
      }

      const wh = new Webhook(secret)
      const event = wh.verify(req.body, headers) as { id: string; type: string }

      if (alreadyProcessed(deliveryId)) return res.status(200).end()
      markProcessed(deliveryId)

      handle(event)
      res.status(200).end()
    }
  )
  ```

  ```python Python theme={null}
  from svix.webhooks import Webhook
  from flask import Flask, request

  app = Flask(__name__)
  secret = os.environ["QUO_WEBHOOK_KEY"]

  @app.post("/webhooks/quo")
  def handle():
      payload = request.get_data()  # raw bytes — do not call request.get_json()
      delivery_id = request.headers["webhook-id"]
      headers = {
          "webhook-id": delivery_id,
          "webhook-timestamp": request.headers["webhook-timestamp"],
          "webhook-signature": request.headers["webhook-signature"],
      }

      event = Webhook(secret).verify(payload, headers)

      if already_processed(delivery_id):
          return "", 200
      mark_processed(delivery_id)

      process(event)
      return "", 200
  ```
</CodeGroup>

Return `200` to acknowledge. Any non-`2xx` response triggers a retry.

## 4. Send a test event

Trigger a real, signed delivery to your endpoint without waiting for a real call or message. The response also includes the sample payload inline so you can confirm what your endpoint received.

```bash theme={null}
curl https://api.openphone.com/webhooks/{id}/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" }'
```

If verification fails, the most common cause is a framework that parsed the JSON before your handler saw the bytes. See [Validate webhook signatures](/mdx/beta/webhooks-signature-validation#framework-gotchas).

## 5. Inspect deliveries

Every delivery is recorded. Use these endpoints to debug a missing or failing event:

* `GET /webhooks/:id/events` — recent deliveries with status.
* `GET /webhooks/:id/events/:eventId` — request body, all attempts, response codes.
* `POST /webhooks/:id/events/:eventId/retry` — manually retry a failed delivery.

## Next steps

* [Overview](/mdx/beta/webhooks-overview) — delivery semantics, idempotency, retries, ordering.
* [Webhook event payloads](/mdx/beta/webhooks-event-payloads) — schemas and examples for every event type.
* [Validate webhook signatures](/mdx/beta/webhooks-signature-validation) — manual HMAC, framework gotchas, test vector.
* [Migrating from legacy](/mdx/beta/webhooks-differences-from-current) — numbered rollout if you have existing legacy webhooks.
