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.