Verifying webhooks
Every webhook request is signed so you can confirm it really came from Chatonio and wasn’t tampered with. Verify the signature before trusting the payload.
Request headers
Chatonio-Signature: t=1751366400,v1=5257a869...e3f0
Chatonio-Webhook-Id: d5e1a220-4f77-4c33-a1b2-c3d4e5f60718
Chatonio-Event: message.created
Chatonio-Delivery-Attempt: 1
Content-Type: application/json- Chatonio-Signature —
t=<unix>,v1=<hex>.v1isHMAC-SHA256(secret, "<t>.<raw_body>"). - Chatonio-Webhook-Id — a unique id for this delivery; use it as an idempotency key so retries aren’t processed twice.
- Chatonio-Event — the event type.
- Chatonio-Delivery-Attempt — the attempt number.
How to verify
- Read the raw request body (bytes / string) — do not re-serialize the parsed JSON, or the signature won’t match.
- Parse
tandv1from the signature header. - Compute
HMAC-SHA256(signing_secret, t + "." + raw_body)and compare it tov1with a constant-time comparison. - Reject the request if the timestamp
tis more than ~5 minutes from now (replay protection).
import crypto from 'node:crypto';
// Express example. Use the RAW request body, not a re-serialized object.
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('='))
);
const t = Number(parts.t);
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1)
);
const fresh = Math.abs(Date.now() / 1000 - t) < 300; // 5-min replay window
return ok && fresh;
}Delivery & reliability
- Retries — a failed delivery (timeout, connection error,
408,429, or5xx) is retried with exponential backoff, up to 5 times. Other4xxresponses are treated as permanent and not retried. - Idempotency — because of retries your endpoint may see the same event more than once; de-duplicate on
Chatonio-Webhook-Id. - Auto-disable — after 15 consecutive failed deliveries the endpoint is disabled and the project owner is emailed. Re-enable it from the Developer page once your endpoint is healthy.
- Delivery log — recent attempts (status, response code, errors) are visible per endpoint on the Developer page, retained for 30 days.
- Ordering — delivery is not strictly ordered; rely on the payload’s
createdtimestamps rather than arrival order.