Verify webhook signatures and handle retries
Compute HMAC-SHA256 with the subscription secret and exact raw body, compare the lowercase `sha256=` value in constant time, then deduplicate `x-callonline-delivery` before applying side effects.
The signature covers the exact JSON string sent by CallOnline. Parsing and re-serializing JSON can change whitespace or key formatting and produce a different digest, so verification must use the raw body.
Expected signature format
Section titled “Expected signature format”The header is exactly sha256= followed by 64 lowercase hexadecimal characters:
sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefReject a missing header, a malformed value, or a digest that does not match. Do not log the subscription secret or full signature in an error message.
Node.js verification example
Section titled “Node.js verification example”import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyCallOnlineWebhook( rawBody: Buffer, signatureHeader: string | null, secret: string,): boolean { if (!signatureHeader || !/^sha256=[0-9a-f]{64}$/.test(signatureHeader)) { return false; }
const expected = `sha256=${createHmac("sha256", secret) .update(rawBody) .digest("hex")}`; const receivedBytes = Buffer.from(signatureHeader, "utf8"); const expectedBytes = Buffer.from(expected, "utf8");
return ( receivedBytes.length === expectedBytes.length && timingSafeEqual(receivedBytes, expectedBytes) );}Capture the raw bytes using the primitive provided by your framework. For example, read request.arrayBuffer() before calling request.json() in a Fetch-compatible server.
Make processing idempotent
Section titled “Make processing idempotent”Verification proves the sender knew the secret; it does not guarantee a delivery is new. Use x-callonline-delivery as a unique key:
INSERT INTO accepted_webhooks (delivery_id, event_type, call_id, body)VALUES (?, ?, ?, ?)ON CONFLICT (delivery_id) DO NOTHING;Only the first insert should enqueue or apply business effects. Later attempts should return success after confirming the original was accepted.
Retry behavior
Section titled “Retry behavior”CallOnline records a failed attempt when the endpoint returns a non-2xx response, stores the error, and calculates a progressively delayed next-attempt time capped internally. The public API does not promise an exact delivery schedule or maximum attempt count, so do not build time-sensitive logic around a fixed retry timetable.
Design the receiver so any delivery can be delayed, repeated, or followed by a newer event. Reconcile the call record when order matters.
Secret rotation
Section titled “Secret rotation”Webhook v1 does not expose a separate rotate-secret operation. Create a replacement subscription with a new secret, deploy support for it, verify deliveries, and then delete the old subscription. During the overlap, associate each endpoint or subscription ID with the correct secret.