Verify before parsing
HookMessage signs the exact string `${timestamp}.${rawBody}` with HMAC SHA-256. Read the raw request bytes first, calculate the digest with the endpoint secret, and compare signatures before trusting any event fields.
The signature arrives in `x-wa-signature` as `v1=<hex digest>`. The Unix timestamp arrives in `x-wa-timestamp`.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(
rawBody: string,
timestamp: string,
signature: string,
secret: string,
) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const expected = "v1=" + createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const left = Buffer.from(expected);
const right = Buffer.from(signature);
return left.length === right.length && timingSafeEqual(left, right);
}Prevent replay and duplicate processing
A valid signature proves integrity, but your receiver must still prevent duplicate business actions. Store the delivery ID or another stable event identifier and make repeated deliveries return success without repeating the side effect.
- Reject timestamps outside a short acceptance window.
- Store processed delivery IDs with a retention period.
- Return a 2xx response only after durable acceptance.
- Move slow work to a queue and acknowledge quickly.
Operate secrets safely
Webhook signing secrets are shown when an endpoint is created or rotated. Store them in a secret manager or protected environment variable and never send them to the browser.
- Use a separate endpoint and secret for each environment.
- Rotate immediately after suspected exposure.
- Do not log headers containing signatures or secrets.
- Restrict the receiver to the event types it actually uses.
Test failure behavior
Before production, submit altered bodies, old timestamps, missing headers, duplicate delivery IDs, and unknown event types. Every invalid case should fail closed without changing application state.