Verify webhook signatures

Short guide — for the full reference see /docs/webhooks/signature.

Every webhook is HMAC-SHA256 signed with the merchant's webhook_secret. Always verify before reading any field.

Three-step verification#

  1. Grab the raw request body (bytes, unparsed).
  2. Compute HMAC-SHA256(webhook_secret, raw_body) as lowercase hex.
  3. Compare against X-Webhook-Signature, stripping the sha256= prefix, using a constant-time comparator.

Full reference#

The Signature scheme page has runnable samples for Node, Python, Ruby, Go, and PHP, plus a shared test vector you can use to validate your own implementation.

Minimal Node snippet#

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

export function verify(rawBody, header, secret) {
  const sig = (header ?? "").replace(/^sha256=/, "");
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(sig, "hex");
  const b = Buffer.from(expected, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

Things that will silently break signature verification#

  • Any body parser (express.json(), Next's default route handler body parsing) — replaces the raw body with a parsed JSON object. Use a raw-body escape hatch for the webhook route.
  • Proxy middleware that recompresses or re-encodes the body.
  • Character-set substitution — if your server reads the body as a string and the encoding does not match, bytes differ and HMAC mismatches.
  • Plain === comparison on the hex string — vulnerable to timing attacks. Always use a constant-time comparator.