Skip to content
CallOnline app iconCallOnline Resources
Get API key

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.

Updated Jul 14, 2026v1Reviewed by engineering

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.

Raw webhook body verified with HMAC-SHA256, deduplicated, and durably accepted

The header is exactly sha256= followed by 64 lowercase hexadecimal characters:

sha256=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

Reject 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.

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.

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.

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.

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.