A Stripe engineer once told me that the most common webhook integration mistake they see is developers who verify the event type but never verify the signature. Their endpoint accepts any POST request from anywhere on the internet, processes it as if Stripe sent it, and moves on. That's not a theoretical risk — it's an auth bypass waiting to happen.
Use HMAC-SHA256 with a shared secret to sign your webhook payloads, and always validate that signature before processing anything. The rest of this article shows you exactly how to implement both sides correctly, and where even experienced developers get it wrong.
How Webhook Signing Works
The sending service (Stripe, GitHub, Shopify, etc.) holds a secret key that only you and they know. When they dispatch a webhook, they compute an HMAC over the raw request body using that secret and include the result in a request header — typically X-Hub-Signature-256, Stripe-Signature, or something similar.
Sender and receiver use identical body bytes — signatures match.
On your end, you recompute the HMAC using the same secret and the raw body you received, then compare your result to the one in the header. If they match, the payload is authentic and unmodified. If they don't, something's wrong — either the secret is mismatched, the body was tampered with, or the request didn't come from the legitimate sender.
The signature covers the body bytes, not the parsed object. That distinction matters more than most developers realize.
Generating HMAC Signatures
If you're building the sending side — your own webhook system, an internal event bus, or a public API — here's how to generate signatures correctly.
Validating Incoming Signatures
This is where most implementations break down. The key rule: read the raw bytes from the request body before any parsing happens. Once your framework deserializes the JSON, you've lost the original byte sequence — and even a single character difference (a space, key reordering, trailing newline) will produce a completely different HMAC.
In Express, that means using express.raw() or reading req.rawBody before express.json() touches it. In Django, use request.body directly. In Spring, read the HttpServletRequest input stream before any @RequestBody binding.
The Timing Attack You're Probably Missing
If you're comparing signature strings with ===, ==, or .equals(), you're vulnerable to a timing attack. A sufficiently motivated attacker can measure response time differences to brute-force signatures one byte at a time.
The fix is a constant-time comparison function. Every language has one: crypto.timingSafeEqual() in Node.js, hmac.compare_digest() in Python, MessageDigest.isEqual() in Java, hash_equals() in PHP. These functions always compare the full length regardless of where the strings differ.
This isn't theoretical paranoia — it's a published attack class with proof-of-concept tooling. At high enough request volumes, the timing signal is measurable.
Replay Attacks and Timestamp Validation
A valid signature doesn't prove the request is fresh. An attacker who captures a legitimate webhook can replay it five minutes — or five days — later. Stripe's SDK defends against this by including a timestamp in the signature header and rejecting requests older than five minutes.
Add a timestamp to your signature payload:
X-Webhook-Timestamp: 1711234567
X-Webhook-Signature: sha256=abc123...
On the receiving end, include the timestamp in what you sign: timestamp + "." + body. Then reject any request where the timestamp is more than 5 minutes old. This makes captured signatures useless after a short window.
Rotating Secrets Without Downtime
When you rotate your webhook secret — and you should rotate it periodically, or immediately after any suspected exposure — you'll have a gap where in-flight requests were signed with the old key but your validator is already using the new one.
The clean solution: briefly accept signatures from both the old and new secret during rollover. Validate against the new key first; if that fails, try the old key. Log when the old key is used so you know when it's safe to fully retire it. Most major webhook platforms (including Stripe) do exactly this with dual-signature headers.
If you need to generate a strong secret for your webhook system right now, the EncryptCodec HMAC tool lets you compute HMAC-SHA256 signatures in-browser — useful for testing your implementation against a known-good reference before you ship.
Frequently Asked Questions
Related posts
Secure Password Reset Tokens — Expiry, Storage, and What Most Implementations Get Wrong
A practical guide to building secure password reset flows: token generation, expiry windows, one-time use enforcement, and the edge cases that cause real account takeovers.
Mar 30, 2026 · 7 min readIncident Response for Developers: What to Do When You Get Hacked
A practical incident response guide for developers covering detection, containment, eradication, recovery, and communication when a security breach happens.
Mar 29, 2026 · 9 min readPhishing Prevention: A Developer's Guide to SPF, DKIM, and DMARC
Understand how email spoofing enables phishing attacks and how to implement SPF, DKIM, and DMARC to protect your domain from being impersonated.
Mar 29, 2026 · 9 min read