Webhooks
Outbound webhook delivery
A Webhook template sends its rendered payload to a destination you supply on the send call itself, not to a URL fixed on the template. Each delivery is signed so you can verify it actually came from CommuniQueue.
Set it up
Webhook delivery is gated by the workspace's webhookDelivery entitlement. A workspace Owner or Admin generates the signing secret from Settings at /workspaces/{workspaceId}/settings/webhooks. The secret is shown once, at rotation - store it the same way you would an API key. Rotating it invalidates the previous secret immediately; there is no overlap period where both work. Go to your workspaces to open one and generate a secret there.
Send it
Create and publish a Webhook template, then call the normal send endpoint with a destination in webhookOptions:
curl -X POST https://api.communiqueue.com/api/v1/notifications/send \
-H "X-Api-Key: cq_live_..." \
-H "Content-Type: application/json" \
-d '{
"tenantId": "<your workspace id>",
"templateId": "<your webhook template id>",
"tags": [{ "key": "accountId", "value": "account-123" }],
"webhookOptions": {
"urlOverride": "https://hooks.yourapp.example/communiqueue",
"headers": { "X-Account": "account-123" }
}
}'Destination and header rules
- The destination must be an absolute https:// URL on the standard port 443 - no other scheme or port.
- Redirects are never followed, and every DNS answer for the host must be a public address. Loopback, private, link-local, and other non-routable ranges are rejected, for both IPv4 and IPv6.
- You may attach up to 16 custom headers: 64 characters per name, 1,024 characters per value, and 4,096 bytes total across all of them.
- Host, Authorization, Cookie, and any header name that looks like it carries a credential, token, or secret are rejected - use the signature below for authentication instead.
Verify the signature
Every delivery carries four headers:
- X-CommuniQueue-Delivery-Id - stable across retries of the same attempt.
- Idempotency-Key - the same value as the delivery id, for receivers that dedupe on that header by convention.
- X-CommuniQueue-Timestamp - Unix seconds when the request was signed.
- X-CommuniQueue-Signature - v1= followed by a lowercase hex HMAC-SHA256.
Compute the expected signature over the exact UTF-8 bytes of:
{timestamp}.{deliveryId}.{requestBody}Use your signing secret as the HMAC key, compare with a constant-time comparison rather than ===, reject timestamps outside a short replay window, and treat the delivery id as a dedupe key so a retried request isn't processed twice. Verify against the raw request bytes - don't parse and re-serialize the body first, or the bytes you sign over will drift from the bytes CommuniQueue signed.
Worked example - Node.js
const crypto = require('crypto')
function isValidDelivery(headers, rawBody, signingSecret) {
const timestamp = headers['x-communiqueue-timestamp']
const deliveryId = headers['x-communiqueue-delivery-id']
const signature = headers['x-communiqueue-signature'] // "v1=<hex>"
// Reject anything outside a short window - this is what stops a captured
// request from being replayed later.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp))
if (ageSeconds > 300) return false
const signedPayload = `${timestamp}.${deliveryId}.${rawBody}`
const expected = 'v1=' + crypto
.createHmac('sha256', signingSecret)
.update(signedPayload, 'utf8')
.digest('hex')
const provided = Buffer.from(signature)
const computed = Buffer.from(expected)
// Constant-time comparison - a plain === leaks timing information an
// attacker can use to forge a signature one byte at a time.
return provided.length === computed.length && crypto.timingSafeEqual(provided, computed)
}Delivery and retries
- Any 2xx response counts as success.
- Network failures, timeouts, 408, 429, and 5xx are retried, up to the notification job's maximum attempts.
- Any other 3xx or 4xx response is treated as permanent and is not retried.
- A Retry-After response header is honored, with jitter added and the wait capped at 15 minutes regardless of what the header requests.