Skip to main content
Security

Webhook signature verification with HMAC: verify the bytes before you parse

Verify a webhook by computing an HMAC over the exact raw request body, comparing in constant time, and rejecting stale timestamps — then parse. The order matters.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 3 min read

To trust a webhook, compute an HMAC-SHA256 over the exact raw bytes the provider sent, compare it against the signature header in constant time, and reject the request if the timestamp is too old — all before you call JSON.parse. Get that ordering wrong and the signature check is decorative.

Your endpoint sits on the public internet. Anyone who finds the URL can POST to it, and a forged payment.succeeded event is worth real money. The signature is what separates "the provider sent this" from "someone knew my URL." HMAC works because it mixes a shared secret into a hash — per RFC 2104, only a party that holds the secret can produce a matching digest, and you can't recover the secret from the signature.

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

const expected = createHmac("sha256", secret).update(rawBody).digest();
const received = Buffer.from(signatureHeader, "hex");
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
  throw new Error("invalid signature");
}

What a real verification checks

Stripe's scheme is a good concrete model: the Stripe-Signature header carries t=<timestamp>,v1=<hmac>, and the signed payload is the literal string ${t}.${rawBody}. Walking one request through it:

StepValue
Header t1721558400
Header v15f2a…c9 (provider's HMAC)
Signed string1721558400.{raw body bytes}
Your HMAC of that string5f2a…c9 (must match v1)
Now − t12s — within a 300s tolerance, accept

Takeaway: the timestamp is inside the signed string, so an attacker can't replay an old-but-validly-signed request with a fresh timestamp — changing t changes the HMAC, and they can't recompute it without the secret.

Mistakes that pass code review and fail in production

Verifying the parsed object instead of the raw body

A framework that auto-parses JSON hands you an object, and it's tempting to JSON.stringify it back to check the signature. Don't. Re-serialization reorders keys, changes whitespace, and re-escapes Unicode — the bytes no longer match what the provider signed, so every signature fails (or worse, you loosen the check until they pass). Capture the raw body before any body parser runs. In Express that means a raw-body middleware scoped to the webhook route; in Next.js route handlers, read await req.text(), not req.json().

Using === or == to compare signatures

String equality short-circuits on the first differing byte, so response time leaks how many leading bytes matched. That's enough to forge a signature byte by byte over many requests. Use crypto.timingSafeEqual (or your platform's constant-time compare), and length-check first because it throws on mismatched lengths.

No timestamp tolerance

Without a freshness window, any signed request is replayable forever. If one valid request is ever captured — a proxy log, a leaked HAR file — it can be resent indefinitely. Reject anything outside a few minutes of skew.

Treating retries as duplicates to fear

Providers retry on timeout, so the same event arrives more than once by design. Signature verification says nothing about uniqueness. Store the provider's event ID and make processing idempotent, then return 2xx fast — persist or enqueue, and do slow work asynchronously so you don't trigger another retry by being slow.

Signature verification and idempotency are two different guarantees, and an endpoint needs both. HMAC tells you who sent the bytes; it says nothing about whether you've already acted on them. Build the check as verify-then-parse-then-dedupe, in that order, and the whole class of forged-and-replayed events stops mattering.

Cover photo by Laura Gigch on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

SecurityWebhooksAPIs

Related articles

All articles