Signature verification

Verify every webhook before processing it.

Headers

HeaderDescription
X-Webhook-IdUnique event ID. Use it to avoid processing an event twice.
X-Webhook-TimestampUnix timestamp in seconds when the webhook was created.
X-Webhook-Signature-VersionSignature algorithm version. Currently v1.
X-Webhook-SignatureHMAC-SHA256 signature in the v1=<signature> format.

Verification steps

  1. Read the raw request body.
  2. Read X-Webhook-Timestamp, X-Webhook-Signature-Version and X-Webhook-Signature.
  3. Check that the signature version is v1.
  4. Make sure the timestamp is within 5 minutes of the current time in either direction.
  5. Build the string timestamp.raw_body.
  6. Compute HMAC-SHA256 with the webhook secret.
  7. Compare the result with X-Webhook-Signature using a constant-time comparison.
  8. Process the JSON and store X-Webhook-Id. If the ID was already processed, do not run the business logic again.
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook(rawBody, timestamp, signature, version, secret) {
  if (
    version !== "v1" ||
    !/^\d{10,11}$/.test(timestamp) ||
    !/^v1=[a-fA-F0-9]{64}$/.test(signature) ||
    Math.abs(Date.now() / 1000 - Number(timestamp)) > 300
  ) {
    return false;
  }

  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();

  return timingSafeEqual(expected, Buffer.from(signature.slice(3), "hex"));
}

If the signature does not match

Reject a webhook with an invalid signature. Example log entry:

Webhook received
ID (untrusted header): 4ad561fd-6ecd-445d-a65f-989ea5995f18
Signature: INVALID
Timestamp: fresh
Check: HMAC mismatch
Body: 1159 bytes