wav1
DashboardOpenAPI document
WEBHOOKS

Verifying the signature

Each delivery carries an HMAC-SHA256 of ${timestamp}.${body} in hex, keyed with the device's webhook secret — the same construction Stripe and GitHub use. Check it before you trust a single field.

Headers

FieldTypeDescription
X-Wa-Event
string

Which event this is, matching the envelope. Lets you route before parsing.

X-Wa-Timestamp
integer

The event time in unix seconds, and the first half of the signed string. It is the same on every retry of the same delivery.

X-Wa-Signature
hex

The HMAC itself, lowercase hex, with no algorithm prefix. Compare it in constant time.

X-Wa-Signature-Previous
hex

Present only while a rotated-out secret is still in its 24-hour grace. Accept a delivery whose signature matches either header, and you can rotate a secret without dropping one.

The check

javascript
import { createHmac, timingSafeEqual } from "node:crypto";

// raw must be the untouched request body. Parsing and re-stringifying
// it changes the bytes and the signature will not match.
export function verify(raw, headers, secret) {
  const timestamp = headers["x-wa-timestamp"];
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${raw}`)
    .digest("hex");

  const got = Buffer.from(headers["x-wa-signature"] ?? "", "utf8");
  const want = Buffer.from(expected, "utf8");
  return got.length === want.length && timingSafeEqual(got, want);
}
Three things that trip people upSign the raw body, not a re-serialized object. Compare in constant time, never with ===. And do not reject an old timestamp: it is the event time, not the send time, so a delivery retried an hour later still carries the original.