This guide takes you from zero to a verified, production-shaped webhook delivery in five minutes. Read Overview for the underlying delivery semantics, payload anatomy, and versioning policy.
Before you start
You’ll need:
- A Quo 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 for framework-specific notes.
You can send a real, signed test event before any code is written using POST /webhooks/:id/events/test. See step 4 below.
1. Create the webhook
Pin the subscription to the current API version with Quo-Api-Version. The version is recorded once when the webhook is created and used for every subsequent delivery.
curl https://api.quo.com/webhooks \
-X POST \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "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.
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()
}
)
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
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.
curl https://api.quo.com/webhooks/{id}/events/test \
-X POST \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "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.
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