HMAC-SHA256 Webhook Signatures: Verification, Raw Bodies & Production Hardening
Engineering searches cluster around HMAC SHA256, webhook signature verification, Stripe signature, and timing safe equal. This article walks the full path: capture the raw body, parse provider-specific headers, recompute HMAC(secret, payload), compare with crypto.timingSafeEqual, and add replay protection plus idempotent handlers—without leaking debug details in production logs.
Anatomy of a signed webhook delivery
Outbound webhooks are HTTP POST requests from a SaaS provider to your endpoint. The provider shares a signing secret; each request includes a signature derived from that secret and the exact byte sequence of the body (sometimes prefixed with a timestamp). Your job is to independently compute the same HMAC and accept the request only if signatures match and freshness checks pass.
Different vendors encode metadata differently—GitHub's X-Hub-Signature-256, Stripe's Stripe-Signature with t= and v1= pairs—read their docs; the cryptography is HMAC-SHA256 under the hood with format sugar on top.
Why JSON.parse before verify breaks signatures
Middleware that eagerly parses JSON for convenience destroys the original byte stream. Pretty-printing, Unicode normalization, or key reordering changes bytes—your recomputed HMAC will never match. Configure the framework to expose req.rawBody, use Express express.raw with type: 'application/json' on the webhook route only, or read await request.text() in Edge runtimes before parsing.
// Node.js conceptual flow
// const raw = req.body; // Buffer from express.raw
// const sig = req.headers['x-hub-signature-256']; // provider-specific
// const expected = crypto.createHmac('sha256', secret).update(raw).digest('hex');
// crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(provided, 'hex'));Timing attacks, replay windows, and idempotency
Timing-safe comparison closes a side channel where attackers learn correct prefix bytes. Combine with a timestamp nonce: reject payloads older than N minutes to limit replay. Store processed event IDs (Redis SET with TTL) so duplicate deliveries from at-least-once systems do not double-charge customers.
Return 2xx only after durable enqueue or processing; otherwise providers retry and you may create duplicates—design handlers to be safe under retry.
Common production mistakes
Logging raw secrets or full signatures
Rotate if leaked. Log event IDs and verification result codes only.
Different secrets per environment
Staging and production must use different signing keys; CI should inject test secrets safely.
Stripe, GitHub, Shopify: provider-specific signature headers
Each SaaS encodes metadata differently. Stripe uses Stripe-Signature with t= timestamp and v1= HMAC over t.payload concatenation—follow their official verification steps exactly. GitHub delivers X-Hub-Signature-256 with a sha256= prefix. Shopify includes HMAC in query parameters for OAuth callbacks and headers for webhooks—never mix patterns across products. When building internal REST APIs that mimic these flows, document your header format in OpenAPI so client SDKs stay aligned.
Debugging signature failures usually means byte mismatch: proxies buffering bodies, gzip decompression changing content, or JSON parsing before verify. Capture the raw bytes in a failing test case and compare hex dumps. Our API debugging guide covers systematic approaches to request tracing. If you also issue JWT bearer tokens to partners, keep token signing separate—see JWT tokens explained.
For payload integrity without shared secrets in the browser, teams sometimes use SHA-256 checksums on downloads—different threat model than HMAC webhooks, but related tooling appears in the same security reviews.
Serverless, Lambda, and API Gateway body passthrough
AWS Lambda behind API Gateway often delivers JSON already parsed unless you configure binary media types or use Lambda Function URLs with raw body access. API Gateway's default "proxy integration" can alter how you access the original string—consult AWS docs for your integration type. The same caution applies to Cloudflare Workers and Vercel Edge: verify which API exposes the pre-parsed versus raw stream.
Recommended tools on Universal Dev Tools
Generate HMAC-SHA256 test vectors with custom keys and payloads.
Explore SHA-256 digests—distinct from HMAC but often taught in the same crypto reviews.
Inspect signed tokens that often accompany webhook integrations.
Pretty-print payloads after verification—never before computing signatures.
Related guides on this blog
Frequently Asked Questions
What is HMAC-SHA256 used for in webhooks?
HMAC-SHA256 produces a keyed message authentication code: only parties with the shared secret can compute the same signature for the same payload bytes. Providers send the signature in headers (e.g., X-Hub-Signature-256, Stripe-Signature); your endpoint recomputes HMAC(secret, raw_body) and compares with a timing-safe function.
Why must webhook verification use the raw request body?
JSON parsers normalize whitespace and key order. If you verify against pretty-printed JSON, the bytes differ from what the sender signed. Always read the raw buffer (express.raw, request.text(), etc.) before JSON.parse for verification purposes.
What is timing-safe comparison for signatures?
Regular string equality short-circuits on first mismatch, leaking bytes via timing. Use crypto.timingSafeEqual (Node), hmac.compare_digest (Python), or SubtleCrypto in browsers when appropriate. Both buffers must be the same length before compare.
How do replay attacks apply to webhooks?
Attackers could capture a valid signed request and resend it. Mitigate with timestamps in the signed payload or headers, rejecting events outside a tolerance window (often 5–10 minutes), and idempotent handlers keyed by provider event IDs.
Is HMAC the same as encryption?
No. HMAC proves authenticity and integrity; it does not hide the payload. Always use HTTPS so secrets and bodies are confidential on the wire.
Can I rotate webhook signing secrets?
Yes—support verifying with the current and previous secret during rotation windows, then retire the old key. Document the process for each provider (Stripe, GitHub, Shopify) because header formats differ.
Wrapping up
Treat HMAC webhook verification as three problems: byte-accurate payloads, cryptographic comparison, and operational idempotency. Our tool helps generate test vectors; wire the patterns above into your gateway.