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.
This is the beta webhook API, available in open beta. For the existing webhook system, see Legacy webhooks.
Overview
Quo signs every webhook delivery. Before you trust a payload, verify it by checking the signed headers against the raw request body bytes.
Each request includes three headers:
| Header | Meaning |
|---|
webhook-id | A stable identifier for this delivery. |
webhook-timestamp | Unix seconds when Quo signed the request. |
webhook-signature | A space-separated list of v1,<base64-signature> entries. |
The signature is HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw-body}, encoded as base64.
Verification must use the exact raw request body bytes Quo sent. If your middleware parses or rewrites the JSON body first, verification will fail.
When you create a webhook, Quo returns a key value prefixed with whsec_. Store it exactly as returned.
- The
whsec_ prefix is part of the canonical format; SDK-based verification accepts it as-is.
- If you verify manually, strip
whsec_ and base64-decode the remainder to get the HMAC key bytes.
SDK verification (recommended)
You can verify Quo webhook signatures with the svix library — it accepts Quo’s headers and whsec_... key format as-is.
Install the library:
Then verify:
import { Webhook } from 'svix'
const secret = process.env.QUO_WEBHOOK_KEY ?? '' // whsec_...
const getHeader = (value: string | string[] | undefined, name: string): string => {
if (typeof value === 'undefined') {
throw new Error(`Missing required header: ${name}`)
}
if (Array.isArray(value)) {
throw new Error(`Expected a single ${name} header value`)
}
return value
}
const headers = {
'webhook-id': getHeader(request.headers['webhook-id'], 'webhook-id'),
'webhook-timestamp': getHeader(request.headers['webhook-timestamp'], 'webhook-timestamp'),
'webhook-signature': getHeader(request.headers['webhook-signature'], 'webhook-signature'),
}
const webhook = new Webhook(secret)
// Throws on error, returns the verified content on success.
const verified = webhook.verify(rawBody, headers)
Manual verification
If you prefer not to add an SDK dependency, verify with Node’s built-in crypto:
import crypto from 'node:crypto'
const secret = process.env.QUO_WEBHOOK_KEY ?? '' // whsec_...
const MAX_AGE_SECONDS = 5 * 60
const secretBase64 = secret.startsWith('whsec_') ? secret.slice('whsec_'.length) : secret
const secretBytes = Buffer.from(secretBase64, 'base64')
const webhookId = request.headers['webhook-id']
const webhookTimestamp = request.headers['webhook-timestamp']
const webhookSignature = request.headers['webhook-signature']
if (!webhookId || !webhookTimestamp || !webhookSignature) {
throw new Error('Missing required webhook headers')
}
const timestamp = Number(webhookTimestamp)
const now = Math.floor(Date.now() / 1000)
if (!Number.isFinite(timestamp) || Math.abs(now - timestamp) > MAX_AGE_SECONDS) {
throw new Error('Invalid or stale webhook timestamp')
}
const signedContent = `${webhookId}.${webhookTimestamp}.${rawBody}`
const expectedSignature = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64')
const providedSignatures = webhookSignature
.split(' ')
.map((entry) => entry.trim())
.filter(Boolean)
.map((entry) => {
const [version, signature] = entry.split(',')
return version === 'v1' ? signature : undefined
})
.filter((signature): signature is string => Boolean(signature))
const isValid = providedSignatures.some((signature) => {
const left = Buffer.from(signature)
const right = Buffer.from(expectedSignature)
return left.length === right.length && crypto.timingSafeEqual(left, right)
})
if (!isValid) {
throw new Error('Invalid webhook signature')
}
Implementation notes
- Always verify using the raw request body, before parsing or transforming it.
- Reject deliveries whose
webhook-timestamp is more than a few minutes off from the current time to protect against replay.
- Store the webhook key (
whsec_...) exactly as Quo returns it; do not trim, rewrap, or lowercase it.
Other languages
Any library that supports Quo’s signing scheme — HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw-body} with the webhook-* header set and a whsec_... base64 secret — can verify Quo webhooks. The Svix SDKs are a convenient off-the-shelf option across several languages; you can also port the manual example above.