Webhooks
Every webhook channel delivery is a POST of a JSON body, signed. Two headers ride every request:
X-Wirewatch-Timestamp: <unix-seconds>— when the request was signed.X-Wirewatch-Signature: sha256=<hex>—hex = HMAC_SHA256(secret, "<timestamp>.<rawBody>"), wheresecretis the value you configured for the channel,<timestamp>is the exact string sent inX-Wirewatch-Timestamp, and<rawBody>is the exact bytes of the request body — before any reformatting or JSON re-serialisation.
Verify the signature before you trust a delivery: recompute the HMAC over "<timestamp>.<rawBody>"
with your channel’s secret, and compare it to X-Wirewatch-Signature. Anyone who has not seen your
secret cannot produce a signature that matches.
A matching signature proves the delivery was produced with your secret. It does not prove the
delivery is recent: a signature has no expiry, so anyone who captures one legitimate delivery can
replay the same body, timestamp and signature later, and it still verifies. X-Wirewatch-Timestamp
exists to close that gap — check it against the current time, and reject a delivery that falls
outside your tolerance window, not only one whose signature is wrong. The sample below uses a
300-second (5-minute) window; that width is your allowance for clock skew between your server and
ours, not a security margin to widen without reason.
A sample that verifies it
Section titled “A sample that verifies it”async function verifyWebhookSignature( secret, timestamp, rawBody, signatureHeader, toleranceSeconds = 300,) { const timestampSeconds = Number(timestamp); if (!timestamp || !Number.isFinite(timestampSeconds)) { return false; } if (Math.abs(Date.now() / 1000 - timestampSeconds) > toleranceSeconds) { return false; } const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const mac = await crypto.subtle.sign("HMAC", key, encoder.encode(`${timestamp}.${rawBody}`)); const hex = Array.from(new Uint8Array(mac)) .map((byte) => byte.toString(16).padStart(2, "0")) .join(""); return signatureHeader === `sha256=${hex}`;}Call it with the raw request body exactly as received. Parsing it to JSON and re-serialising it can reorder keys or change whitespace and produce a body that no longer matches the signature.
Reject anything that fails
Section titled “Reject anything that fails”A delivery whose signature does not verify was not produced with your channel’s secret. Reject it —
do not act on the incident it claims, and do not skip verification “just this once”: that is the
whole gap this page exists to close. The same applies to a delivery whose timestamp is missing,
unparseable, or outside the tolerance window: verifyWebhookSignature already returns false for
all three, so there is nothing extra to wire up beyond calling it and honouring the result.