Quo (formerly OpenPhone) — REST API & Webhooks Integration
Add programmatic phone communications to a site or app: send and receive
SMS/text messages, pull call history with AI summaries / transcripts /
recordings / voicemails, manage contacts (and custom fields),
conversations, tasks, users, and phone numbers, and react to
events in real time via webhooks. Quo is the rebrand of OpenPhone; the two
share one API.
Naming: the product is now Quo (quo.com), but the REST API, the
OpenAPI spec, and most existing integrations are unchanged from OpenPhone. The
canonical host is https://api.quo.com/v1; https://api.openphone.com/v1 is a
live, byte-identical legacy alias (both verified). Pick one and keep it in a
single configurable constant.
Facts that prevent broken work
Read this table first — each row is a real failure integrators hit.
| Fact |
Consequence |
Auth is the RAW API key in the Authorization header. The docs say verbatim "The Quo API does not use a Bearer token." |
Authorization: Bearer <key> → 401 Unauthorized. Send the key value alone: Authorization: <key>. |
Base URL is https://api.quo.com/v1 (OpenAPI servers); https://api.openphone.com/v1 is an identical alias |
Hardcoding one host is fine, but don't mix /v1 REST with the un-prefixed beta webhook host (https://api.quo.com/webhooks, no /v1). |
| Sending SMS to US numbers requires A2P 10DLC carrier registration |
Without it, POST /v1/messages returns 400 code 0206400 "A2P Registration Not Approved" — a 400 here is NOT a malformed body. |
Send is async and returns 202, not 200 |
Treat 202 as queued. to must be exactly one E.164 recipient (maxItems: 1 — no fan-out per call); content is 1–1600 non-whitespace chars; MMS is not supported. |
from is an E.164 number OR a PN… phoneNumberId |
The legacy top-level phoneNumberId body field is deprecated — use from. GET /v1/phone-numbers gives you valid senders. |
Rate limit: 10 requests/second per API key → 429 |
The docs document no Retry-After / RateLimit-* headers — implement your own exponential backoff (the bundled client already does). |
Pagination: maxResults (required, max 100) + pageToken → response nextPageToken |
totalItems is documented as inaccurate — loop until nextPageToken is null; never page on totalItems. |
| List messages/calls are scoped, not global |
GET /v1/messages and /v1/calls require phoneNumberId and participants and maxResults together. There is no "list everything" call. |
| Beta webhooks are signed; legacy v1 webhooks are not |
For verifiable authenticity use the beta webhook API: HMAC-SHA256 (base64) over {webhook-id}.{webhook-timestamp}.{raw-body} with a whsec_… secret. Verify the raw body before parsing. |
Webhook idempotency key is the webhook-id HEADER |
Not the envelope id. Deliveries retry (8 attempts over ~27h) and arrive out of order — dedupe on webhook-id, retain ≥28h, make handlers order-independent. |
| AI features are plan-gated |
Call summaries and transcripts (and their webhooks) require a business or scale plan; lower plans get 403/absent. They're also async — poll processingStatus until completed. |
| Error envelope is inconsistent |
The live gateway returns { "error": { "message", "key", "trace" } }; the OpenAPI documents { "message", "code", "status", "errors": [] }. Parse defensively: body.error?.message ?? body.message. |
| ID prefixes are load-bearing |
AC… activity/message/call · PN… phone number · US… user · CN… conversation · CT… contact · VM… voicemail. Endpoints validate them by regex. |
Preflight
Confirm tooling + key before writing code, so the first failure points at config:
bash scripts/quo-preflight.sh # node/curl + QUO_API_KEY + Bearer-mistake checks
bash scripts/quo-preflight.sh --probe # also calls GET /v1/phone-numbers live (real 401 vs 200)
It accepts QUO_API_KEY (or the legacy OPENPHONE_API_KEY), flags a stray
Bearer prefix, and validates key presence — a live 200/401 from
--probe is what proves the key actually works.
Build an integration in one command (scaffold)
The fastest path to a working integration is to generate it, then customize.
scripts/scaffold-quo.mjs writes a complete, runnable full-stack app — an
Express backend (API client + send-SMS route + a signature-verified beta
webhook receiver) and a frontend — with the client + webhook verifier inlined
(the generated project has zero dependency on this skill).
node scripts/scaffold-quo.mjs --out ./quo-app # vanilla JS frontend
node scripts/scaffold-quo.mjs --out ./quo-app --frontend react
node scripts/scaffold-quo.mjs --help # flags; --dry-run previews
Then: cd quo-app && npm install && cp .env.example .env (fill in QUO_API_KEY),
npm start. Customize the emitted code with the references below — the scaffold
is a correct skeleton, not a black box.
Quick start by hand — send an SMS
The shortest path to "text from my app": one server-side call with the raw-key
header. Use the bundled client so the auth/202/E.164 rules are handled for you.
import { createQuoClient } from "./scripts/quo-client.mjs";
const quo = createQuoClient({ apiKey: process.env.QUO_API_KEY }); // raw key, no Bearer
const msg = await quo.sendMessage({
from: "+15555550100", // one of YOUR Quo numbers (E.164) or its PN… id
to: "+15555550111", // exactly ONE E.164 recipient
content: "Hello from Quo 👋", // 1–1600 chars
});
// 202 Accepted → queued. Final delivery arrives as a `message.delivered` webhook,
// or poll GET /v1/messages/{id} (status: queued|sent|delivered|undelivered).
Raw HTTP equivalent (note the header — no Bearer):
curl -X POST https://api.quo.com/v1/messages \
-H "Authorization: $QUO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from":"+15555550100","to":["+15555550111"],"content":"Hello from Quo"}'
For the CLI: node scripts/quo-client.mjs send --from +1… --to +1… --text "Hi".
Quick start by hand — receive a verified webhook
Use the beta webhook API (it's signed). Create a subscription, save the
whsec_… secret, and verify every delivery against the raw body:
import express from "express";
import { verifyQuoWebhook } from "./scripts/verify-webhook.js";
// express.raw — the signature is over the raw bytes; a JSON parser first breaks it.
app.post("/webhooks/quo", express.raw({ type: "*/*" }), (req, res) => {
if (!verifyQuoWebhook(req.headers, req.body, process.env.QUO_WEBHOOK_KEY)) {
return res.status(400).send("bad signature");
}
const id = req.headers["webhook-id"]; // idempotency key (NOT envelope.id)
// ...dedupe on `id` (retain ≥28h), then handle:
const event = JSON.parse(req.body.toString());
if (event.type === "message.received") {
console.log("inbound:", event.data.resource.text);
}
res.status(200).json({ ok: true }); // ack fast; do slow work async
});
Create the subscription (un-prefixed host, version header required):
curl -X POST https://api.quo.com/webhooks \
-H "Authorization: $QUO_API_KEY" \
-H "Content-Type: application/json" \
-H "x-quo-api-version: 2026-03-30" \
-d '{"url":"https://YOUR_HOST/webhooks/quo","events":["message.received","message.delivered","call.completed"]}'
# Save data.key (whsec_…) into QUO_WEBHOOK_KEY — it is shown only once.
Prove your verifier works without any traffic: node scripts/verify-webhook.js --selftest.
API surface
All REST paths are under https://api.quo.com/v1. Beta webhooks live at
https://api.quo.com/webhooks (no /v1).
| Resource |
Endpoints |
Reference |
| Messages |
POST /messages · GET /messages · GET /messages/{id} |
references/messages-and-numbers.md |
| Phone numbers |
GET /phone-numbers · GET /phone-numbers/{id} |
references/messages-and-numbers.md |
| Calls |
GET /calls · /calls/{id} · /call-recordings/{id} · /call-summaries/{id} · /call-transcripts/{id} · /call-voicemails/{id} |
references/calls.md |
| Contacts |
POST/GET/PATCH/DELETE /contacts · GET /contact-custom-fields |
references/contacts.md |
| Conversations |
GET /conversations · POST /conversations/{id}/mark-as-read |
references/conversations-and-tasks.md |
| Tasks |
GET/POST /tasks · /tasks/{id} (+ complete, reopen, assign, due-date, link actions) |
references/conversations-and-tasks.md |
| Users |
GET /users · GET /users/{id} |
references/webhooks-v1-and-users.md |
| Webhooks (beta, signed) |
POST/GET/PATCH/DELETE /webhooks (+ rotate, test, deliveries) |
references/webhooks.md |
| Webhooks (legacy v1) |
POST /v1/webhooks/{messages,calls,call-summaries,call-transcripts} |
references/webhooks-v1-and-users.md |
Where to go next
Each reference is self-contained and source-cited (https://www.quo.com/docs/mdx/...):
| Task |
Reference |
| Auth, base URL, versioning, error codes, rate limits, pagination |
references/api-basics.md |
| Send/list/get messages, phone numbers, the 202 + E.164 + scoping rules |
references/messages-and-numbers.md |
| Calls + recordings, summaries, transcripts, voicemails, plan gating, async AI |
references/calls.md |
Contacts CRUD, custom fields, the defaultFields/customFields shape, sync-by-externalId |
references/contacts.md |
Conversations + the full Tasks lifecycle (no status enum — completed/isDeleted) |
references/conversations-and-tasks.md |
| Beta webhooks: signature validation, event payload catalog, retries, migration |
references/webhooks.md |
| Legacy v1 webhooks (unsigned) + Users |
references/webhooks-v1-and-users.md |
| Building with AI/LLMs, A2P 10DLC registration, pricing & cost-minimization |
references/ai-cost-and-registration.md |
Scripts
| Script |
Purpose |
scripts/scaffold-quo.mjs |
Generate a complete, runnable integration (Express client + send route + verified beta webhook receiver + frontend + env + README). --frontend vanilla|react. |
scripts/quo-client.mjs |
Importable + CLI REST client: raw-key auth, 429 backoff, sendMessage (202/E.164 guards), paginate (cursor-correct). |
scripts/verify-webhook.js / verify_webhook.py |
Verify a beta webhook signature (HMAC-SHA256 base64 over {id}.{ts}.{body}, whsec_ secret, replay window, rotation-aware). --selftest round-trips offline. Node↔Python parity verified. |
scripts/quo-preflight.sh |
Check node/curl, the key, the Bearer mistake, and (optional --probe) live reachability. |
Common mistakes
| # |
Mistake |
Fix |
| 1 |
Authorization: Bearer <key> |
Send the raw key: Authorization: <key>. Quo uses no Bearer token. |
| 2 |
Treating 202 as failure / assuming sync delivery |
202 = queued. Confirm via GET /v1/messages/{id} or a message.delivered webhook. |
| 3 |
Sending US SMS without A2P registration |
Complete US Carrier (A2P 10DLC) registration first, or every send returns 400 0206400. |
| 4 |
Multiple recipients in one send |
to is maxItems: 1. Fan out client-side, under 10 req/s. |
| 5 |
Verifying a webhook against parsed JSON |
Verify the raw body (express.raw / Flask get_data()); re-serialized JSON won't match the HMAC. |
| 6 |
Deduping webhooks on event.id |
Use the webhook-id header; retain ≥28h to cover the retry window. |
| 7 |
Paginating on totalItems |
It's documented as inaccurate — loop until nextPageToken is null. |
| 8 |
Expecting summaries/transcripts on any plan |
They need a business/scale plan and are async — gate on processingStatus. |
| 9 |
Using the legacy OpenPhone-Signature verifier on beta deliveries |
Beta uses webhook-* headers + whsec_ — the schemes are not interchangeable. |
| 10 |
Polling lists in a loop |
Prefer webhooks over polling to conserve the 10 req/s budget. |
Resources
1---2name: quo3description: Build, scaffold, and integrate Quo — formerly OpenPhone — telephony into a full-stack app: send & receive SMS/text messages, list and analyze calls (recordings, AI summaries, transcripts, voicemails), manage contacts, conversations, tasks, users, and phone numbers via the Quo REST API (https://api.quo.com/v1), plus signature-verified webhooks. Ships a one-command scaffolder that emits a runnable integration (Express + API client + send-SMS route + a signature-verifying webhook receiver), a preflight checker, a tested webhook verifier (Node + Python), and an importable client. Use whenever the user wants to add, build, or scaffold SMS/text messaging, programmatic calls, an OpenPhone or Quo integration, two-way SMS, call transcripts/summaries, contact sync, or Quo webhooks — even if they only say 'OpenPhone', 'Quo', or 'send a text from my app'. Bakes in the raw-API-key auth gotcha (no Bearer token), A2P 10DLC registration, the 202/one-recipient/E.164 send rules, and the whsec_ webhook signing scheme.4license: MIT5---67# Quo (formerly OpenPhone) — REST API & Webhooks Integration89Add programmatic phone communications to a site or app: **send and receive10SMS/text messages**, pull **call** history with AI **summaries / transcripts /11recordings / voicemails**, manage **contacts** (and custom fields),12**conversations**, **tasks**, **users**, and **phone numbers**, and react to13events in real time via **webhooks**. Quo is the rebrand of OpenPhone; the two14share one API.1516> **Naming:** the product is now **Quo** (`quo.com`), but the REST API, the17> OpenAPI spec, and most existing integrations are unchanged from OpenPhone. The18> canonical host is `https://api.quo.com/v1`; `https://api.openphone.com/v1` is a19> live, byte-identical legacy alias (both verified). Pick one and keep it in a20> single configurable constant.2122## Facts that prevent broken work2324Read this table first — each row is a real failure integrators hit.2526| Fact | Consequence |27|---|---|28| **Auth is the RAW API key** in the `Authorization` header. The docs say verbatim *"The Quo API does not use a Bearer token."* | `Authorization: Bearer <key>` → `401 Unauthorized`. Send the key value alone: `Authorization: <key>`. |29| **Base URL** is `https://api.quo.com/v1` (OpenAPI `servers`); `https://api.openphone.com/v1` is an identical alias | Hardcoding one host is fine, but don't mix `/v1` REST with the **un-prefixed** beta webhook host (`https://api.quo.com/webhooks`, no `/v1`). |30| **Sending SMS to US numbers requires A2P 10DLC carrier registration** | Without it, `POST /v1/messages` returns **`400` code `0206400` "A2P Registration Not Approved"** — a 400 here is NOT a malformed body. |31| **Send is async and returns `202`**, not `200` | Treat `202` as *queued*. `to` must be **exactly one** E.164 recipient (`maxItems: 1` — no fan-out per call); `content` is 1–1600 non-whitespace chars; **MMS is not supported**. |32| `from` is an **E.164 number OR a `PN…` phoneNumberId** | The legacy top-level `phoneNumberId` body field is deprecated — use `from`. `GET /v1/phone-numbers` gives you valid senders. |33| **Rate limit: 10 requests/second per API key** → `429` | The docs document **no** `Retry-After` / `RateLimit-*` headers — implement your own exponential backoff (the bundled client already does). |34| **Pagination:** `maxResults` (required, max 100) + `pageToken` → response `nextPageToken` | `totalItems` is **documented as inaccurate** — loop until `nextPageToken` is `null`; never page on `totalItems`. |35| **List messages/calls are scoped, not global** | `GET /v1/messages` and `/v1/calls` require `phoneNumberId` **and** `participants` **and** `maxResults` together. There is no "list everything" call. |36| **Beta webhooks are signed; legacy v1 webhooks are not** | For verifiable authenticity use the **beta** webhook API: HMAC-SHA256 (base64) over `{webhook-id}.{webhook-timestamp}.{raw-body}` with a `whsec_…` secret. Verify the **raw** body before parsing. |37| **Webhook idempotency key is the `webhook-id` HEADER** | Not the envelope `id`. Deliveries retry (8 attempts over ~27h) and arrive out of order — dedupe on `webhook-id`, retain ≥28h, make handlers order-independent. |38| **AI features are plan-gated** | Call **summaries** and **transcripts** (and their webhooks) require a **business or scale** plan; lower plans get `403`/absent. They're also async — poll `processingStatus` until `completed`. |39| **Error envelope is inconsistent** | The live gateway returns `{ "error": { "message", "key", "trace" } }`; the OpenAPI documents `{ "message", "code", "status", "errors": [] }`. Parse defensively: `body.error?.message ?? body.message`. |40| **ID prefixes are load-bearing** | `AC…` activity/message/call · `PN…` phone number · `US…` user · `CN…` conversation · `CT…` contact · `VM…` voicemail. Endpoints validate them by regex. |4142## Preflight4344Confirm tooling + key before writing code, so the first failure points at config:4546```bash47bash scripts/quo-preflight.sh # node/curl + QUO_API_KEY + Bearer-mistake checks48bash scripts/quo-preflight.sh --probe # also calls GET /v1/phone-numbers live (real 401 vs 200)49```5051It accepts `QUO_API_KEY` (or the legacy `OPENPHONE_API_KEY`), flags a stray52`Bearer ` prefix, and validates key **presence** — a live `200`/`401` from53`--probe` is what proves the key actually works.5455## Build an integration in one command (scaffold)5657The fastest path to a working integration is to **generate** it, then customize.58`scripts/scaffold-quo.mjs` writes a complete, runnable full-stack app — an59Express backend (API client + send-SMS route + a **signature-verified** beta60webhook receiver) and a frontend — with the client + webhook verifier **inlined**61(the generated project has zero dependency on this skill).6263```bash64node scripts/scaffold-quo.mjs --out ./quo-app # vanilla JS frontend65node scripts/scaffold-quo.mjs --out ./quo-app --frontend react66node scripts/scaffold-quo.mjs --help # flags; --dry-run previews67```6869Then: `cd quo-app && npm install && cp .env.example .env` (fill in `QUO_API_KEY`),70`npm start`. Customize the emitted code with the references below — the scaffold71is a correct skeleton, not a black box.7273## Quick start by hand — send an SMS7475The shortest path to "text from my app": one server-side call with the raw-key76header. Use the bundled client so the auth/202/E.164 rules are handled for you.7778```ts79import { createQuoClient } from "./scripts/quo-client.mjs";8081const quo = createQuoClient({ apiKey: process.env.QUO_API_KEY }); // raw key, no Bearer8283const msg = await quo.sendMessage({84 from: "+15555550100", // one of YOUR Quo numbers (E.164) or its PN… id85 to: "+15555550111", // exactly ONE E.164 recipient86 content: "Hello from Quo 👋", // 1–1600 chars87});88// 202 Accepted → queued. Final delivery arrives as a `message.delivered` webhook,89// or poll GET /v1/messages/{id} (status: queued|sent|delivered|undelivered).90```9192Raw HTTP equivalent (note the header — **no `Bearer`**):9394```bash95curl -X POST https://api.quo.com/v1/messages \96 -H "Authorization: $QUO_API_KEY" \97 -H "Content-Type: application/json" \98 -d '{"from":"+15555550100","to":["+15555550111"],"content":"Hello from Quo"}'99```100101For the CLI: `node scripts/quo-client.mjs send --from +1… --to +1… --text "Hi"`.102103## Quick start by hand — receive a verified webhook104105Use the **beta** webhook API (it's signed). Create a subscription, save the106`whsec_…` secret, and verify every delivery against the **raw** body:107108```ts109import express from "express";110import { verifyQuoWebhook } from "./scripts/verify-webhook.js";111112// express.raw — the signature is over the raw bytes; a JSON parser first breaks it.113app.post("/webhooks/quo", express.raw({ type: "*/*" }), (req, res) => {114 if (!verifyQuoWebhook(req.headers, req.body, process.env.QUO_WEBHOOK_KEY)) {115 return res.status(400).send("bad signature");116 }117 const id = req.headers["webhook-id"]; // idempotency key (NOT envelope.id)118 // ...dedupe on `id` (retain ≥28h), then handle:119 const event = JSON.parse(req.body.toString());120 if (event.type === "message.received") {121 console.log("inbound:", event.data.resource.text);122 }123 res.status(200).json({ ok: true }); // ack fast; do slow work async124});125```126127Create the subscription (un-prefixed host, version header required):128129```bash130curl -X POST https://api.quo.com/webhooks \131 -H "Authorization: $QUO_API_KEY" \132 -H "Content-Type: application/json" \133 -H "x-quo-api-version: 2026-03-30" \134 -d '{"url":"https://YOUR_HOST/webhooks/quo","events":["message.received","message.delivered","call.completed"]}'135# Save data.key (whsec_…) into QUO_WEBHOOK_KEY — it is shown only once.136```137138Prove your verifier works without any traffic: `node scripts/verify-webhook.js --selftest`.139140## API surface141142All REST paths are under `https://api.quo.com/v1`. Beta webhooks live at143`https://api.quo.com/webhooks` (no `/v1`).144145| Resource | Endpoints | Reference |146|---|---|---|147| **Messages** | `POST /messages` · `GET /messages` · `GET /messages/{id}` | `references/messages-and-numbers.md` |148| **Phone numbers** | `GET /phone-numbers` · `GET /phone-numbers/{id}` | `references/messages-and-numbers.md` |149| **Calls** | `GET /calls` · `/calls/{id}` · `/call-recordings/{id}` · `/call-summaries/{id}` · `/call-transcripts/{id}` · `/call-voicemails/{id}` | `references/calls.md` |150| **Contacts** | `POST/GET/PATCH/DELETE /contacts` · `GET /contact-custom-fields` | `references/contacts.md` |151| **Conversations** | `GET /conversations` · `POST /conversations/{id}/mark-as-read` | `references/conversations-and-tasks.md` |152| **Tasks** | `GET/POST /tasks` · `/tasks/{id}` (+ complete, reopen, assign, due-date, link actions) | `references/conversations-and-tasks.md` |153| **Users** | `GET /users` · `GET /users/{id}` | `references/webhooks-v1-and-users.md` |154| **Webhooks (beta, signed)** | `POST/GET/PATCH/DELETE /webhooks` (+ rotate, test, deliveries) | `references/webhooks.md` |155| **Webhooks (legacy v1)** | `POST /v1/webhooks/{messages,calls,call-summaries,call-transcripts}` | `references/webhooks-v1-and-users.md` |156157## Where to go next158159Each reference is self-contained and source-cited (`https://www.quo.com/docs/mdx/...`):160161| Task | Reference |162|---|---|163| Auth, base URL, versioning, **error codes**, rate limits, pagination | `references/api-basics.md` |164| Send/list/get messages, phone numbers, the **202 + E.164 + scoping** rules | `references/messages-and-numbers.md` |165| Calls + recordings, summaries, transcripts, voicemails, plan gating, async AI | `references/calls.md` |166| Contacts CRUD, custom fields, the `defaultFields`/`customFields` shape, sync-by-externalId | `references/contacts.md` |167| Conversations + the full **Tasks** lifecycle (no status enum — `completed`/`isDeleted`) | `references/conversations-and-tasks.md` |168| **Beta webhooks**: signature validation, event payload catalog, retries, migration | `references/webhooks.md` |169| Legacy v1 webhooks (unsigned) + Users | `references/webhooks-v1-and-users.md` |170| Building with **AI/LLMs**, A2P 10DLC registration, pricing & cost-minimization | `references/ai-cost-and-registration.md` |171172## Scripts173174| Script | Purpose |175|---|---|176| `scripts/scaffold-quo.mjs` | **Generate a complete, runnable integration** (Express client + send route + verified beta webhook receiver + frontend + env + README). `--frontend vanilla\|react`. |177| `scripts/quo-client.mjs` | Importable + CLI REST client: raw-key auth, 429 backoff, `sendMessage` (202/E.164 guards), `paginate` (cursor-correct). |178| `scripts/verify-webhook.js` / `verify_webhook.py` | Verify a beta webhook signature (HMAC-SHA256 base64 over `{id}.{ts}.{body}`, `whsec_` secret, replay window, rotation-aware). `--selftest` round-trips offline. Node↔Python parity verified. |179| `scripts/quo-preflight.sh` | Check node/curl, the key, the `Bearer` mistake, and (optional `--probe`) live reachability. |180181## Common mistakes182183| # | Mistake | Fix |184|---|---|---|185| 1 | `Authorization: Bearer <key>` | Send the **raw** key: `Authorization: <key>`. Quo uses no Bearer token. |186| 2 | Treating `202` as failure / assuming sync delivery | `202` = queued. Confirm via `GET /v1/messages/{id}` or a `message.delivered` webhook. |187| 3 | Sending US SMS without A2P registration | Complete US Carrier (A2P 10DLC) registration first, or every send returns `400 0206400`. |188| 4 | Multiple recipients in one send | `to` is `maxItems: 1`. Fan out client-side, under 10 req/s. |189| 5 | Verifying a webhook against parsed JSON | Verify the **raw** body (`express.raw` / Flask `get_data()`); re-serialized JSON won't match the HMAC. |190| 6 | Deduping webhooks on `event.id` | Use the **`webhook-id` header**; retain ≥28h to cover the retry window. |191| 7 | Paginating on `totalItems` | It's documented as inaccurate — loop until `nextPageToken` is `null`. |192| 8 | Expecting summaries/transcripts on any plan | They need a **business/scale** plan and are async — gate on `processingStatus`. |193| 9 | Using the legacy `OpenPhone-Signature` verifier on beta deliveries | Beta uses `webhook-*` headers + `whsec_` — the schemes are not interchangeable. |194| 10 | Polling lists in a loop | Prefer webhooks over polling to conserve the 10 req/s budget. |195196## Resources197198- API reference: https://www.quo.com/docs/mdx/api-reference/introduction199- Doc index (machine-readable): https://www.quo.com/docs/llms.txt200- OpenAPI spec: https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-v1-prod.json201- LLM-ready docs bundle: https://openphone-public-api-prod.s3.us-west-2.amazonaws.com/public/openphone-public-api-llm-ready-docs-prod.zip202- Building with AI/LLMs guide: https://www.quo.com/docs/mdx/guides/building-with-ai-llms