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#
- Grab the raw request body (bytes, unparsed).
- Compute
HMAC-SHA256(webhook_secret, raw_body)as lowercase hex. - Compare against
X-Webhook-Signature, stripping thesha256=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.