Webhook signature verification
Webhook signing proves the request came from the provider, not a forger. Provider HMACs the body, your server recomputes, constant-time compare.
A webhook endpoint is a publicly reachable URL. Anyone on the internet can POST to it. Without signature verification, anyone can pretend to be Stripe and tell you "the customer paid $1000" and your server believes them.
Signature verification proves the request came from the provider. The provider HMACs the body with a shared secret, includes the tag in a header, your server recomputes and compares.
The canonical Stripe verification.
import crypto from "node:crypto";
export function verifyWebhook(
rawBody: string,
sigHeader: string,
secret: string,
tolerance = 300,
): boolean {
const parts = Object.fromEntries(
sigHeader.split(",").map(p => p.split("="))
);
const ts = Number(parts.t);
const v1 = parts.v1;
if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const signed = `${ts}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signed)
.digest("hex");
const a = Buffer.from(v1);
const b = Buffer.from(expected);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Three things this does right.
- Raw body, not parsed JSON. In Express, use
express.raw({ type: 'application/json' })for the webhook route only. - Timing-safe compare. A
===leaks the tag byte by byte through timing. - Timestamp tolerance of 5 minutes. Stops replay attacks even if an attacker captured a valid webhook.
Four common provider patterns.
- Stripe: HMAC-SHA256 over
timestamp.body, sent inStripe-Signature: t=,v1=. - GitHub: HMAC-SHA256 over raw body, sent in
X-Hub-Signature-256: sha256=.... - Shopify: HMAC-SHA256 over raw body, base64-encoded, sent in
X-Shopify-Hmac-Sha256. - Slack: HMAC-SHA256 over
v0:timestamp:body, sent inX-Slack-Signature: v0=....
Patterns vary in format but the math is the same.
There is a Standard Webhooks spec (standardwebhooks.com) emerging in 2024 to unify the format. Adoption is growing but you will still need to handle each provider's flavor.
Learn more
- Docs
- Docs
- DocsStandard Webhooks specStandard Webhooks