A payment processor sends your server a webhook: "subscription.cancelled," with a customer ID and a timestamp. Nothing about the request looks unusual. It has the right headers, the right JSON shape, the right endpoint. The only question that actually matters is one your code has to answer in milliseconds: did this really come from the payment processor, or did someone who found your endpoint URL just send the exact same payload themselves?
A checksum can't answer that question. Neither can a plain SHA-256 hash of the payload. That's not a limitation of those tools, it's a mismatch between what they're built to prove and what a webhook actually needs proven. Understanding that gap is the whole reason HMAC exists, and why nearly every webhook system, from payment processors to source control platforms, signs its payloads with it instead of just hashing them.
Photo by Fernando Lucas on Pexels
What a Plain Hash Actually Proves (and What It Doesn't)
A hash function like SHA-256 takes any input and produces a fixed-length fingerprint of it. Change one character in the input and the output changes completely. That property makes hashing genuinely useful for one specific job: confirming that a piece of data wasn't corrupted or altered after the fact. Download a file, hash it, compare the result against a published hash, and you know the bytes match what was originally published.
Here's the catch. Anyone can compute a SHA-256 hash of anything. The algorithm is public, deterministic, and free to run. If an attacker crafts a fake webhook payload and hashes it themselves, they get a perfectly valid hash, one that matches their own fake data flawlessly. A plain hash proves internal consistency. It says nothing about origin. For a file download, that's fine, because you're the one who chose to trust the source publishing the hash. For a webhook hitting a public endpoint, origin is the entire question, and a plain hash simply doesn't answer it.
The Missing Piece: Proving Who Sent It, Not Just What Was Sent
What a webhook receiver actually needs is a way to verify that the sender knows something an attacker doesn't. That's the core idea behind message authentication, and it's a different problem than data integrity. Integrity asks "was this changed in transit?" Authentication asks "could only the real sender have produced this?" A plain hash answers the first question. It can't touch the second, because it doesn't involve any secret at all, just public math applied to public data.
Photo by Zaidan Falaah on Pexels
How HMAC Adds a Secret Key Into the Mix
HMAC, short for Hash-based Message Authentication Code, solves this by combining a cryptographic hash function with a secret key that only the sender and receiver know. Instead of hashing just the payload, HMAC hashes the payload together with the secret key, run through the hash function twice in a specific, standardized way that prevents a class of attacks that a naive "just concatenate the key and hash it" approach would be vulnerable to. The HMAC specification defines that construction precisely so implementations across different languages and platforms produce identical results given the same key and message.
The Basic Flow
The sending service (the payment processor, the source control platform, whatever is triggering the webhook) computes an HMAC of the outgoing payload using a secret key it shares with you, then attaches that HMAC as a signature header. Your server, on receipt, computes its own HMAC of the payload using the same shared secret and compares the two. If they match, the request could only have come from someone who knows the secret key, which should be nobody but the two of you. If they don't match, either the payload was altered in transit or whoever sent it doesn't have the real key. Either way, you reject it.
This is why the secret key matters so much operationally. It's usually issued once, when you set up the webhook integration, and it should be treated with the same care as a password or API credential. If it leaks, anyone who has it can forge signatures that pass your verification, which defeats the entire point.
Why This Matters for Webhooks Specifically
Webhooks are a soft spot in a lot of systems precisely because the endpoint URL is, by necessity, public and reachable from the internet. Unlike an API you call, where you control who initiates the request, a webhook endpoint has to accept incoming requests from a service you don't control the network path to. That inversion of the usual client-server trust model is exactly why signature verification isn't optional for anything that triggers a real action, refunds, account changes, order fulfillment, on the receiving end.
"The mistake I see most often isn't skipping signature verification entirely, it's implementing it and then never testing the failure path. Teams verify that a valid signature passes, and never confirm that a tampered payload actually gets rejected. That untested branch is where the real vulnerability usually lives." - Dennis Traina, founder of 137Foundry
Photo by Esra Korkmaz on Pexels
Common Mistakes When Implementing Webhook Verification
A few implementation details separate a genuinely secure verification step from one that looks correct but isn't:
Using a non-constant-time string comparison. Comparing two signature strings with a standard == operator can leak timing information about how many characters matched before the comparison failed, which in theory lets an attacker reconstruct a valid signature one byte at a time across enough requests. Most languages provide a constant-time comparison function specifically for this (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js) and it should always be used for signature checks.
Reusing MD5 or SHA-1 as the underlying hash. HMAC's security depends partly on the strength of the hash function it wraps. MD5 and SHA-1 both have known collision weaknesses. SHA-256 or stronger is the standard choice for new implementations, and most webhook providers have already standardized on HMAC-SHA256 for exactly this reason.
Skipping timestamp or replay checks. A valid signature on an old, previously-sent payload is still a valid signature. Without a timestamp check and a reasonable tolerance window, an attacker who intercepts one legitimate webhook can replay it indefinitely. Most robust implementations sign the timestamp along with the payload and reject requests where the timestamp is too far from the current time.
Comparing against the raw parsed object instead of the raw request body. If your framework parses the JSON body before your signature check runs, and you re-serialize it to verify, small differences in key ordering or whitespace can produce a different byte string than what was actually signed, causing valid requests to fail verification. The signature should always be computed against the exact raw bytes received, before any parsing.
Testing Your Own Webhook Handler Without Waiting for a Real Event
You don't need a live webhook to fire in order to test whether your verification logic actually rejects a tampered payload. Take a sample JSON payload, generate an HMAC-SHA256 signature against it using a known test secret, and send that payload to your endpoint with the matching signature header. Then change one character in the payload and resend it with the original, now-invalid signature attached, to confirm your handler actually rejects it rather than silently accepting it.
EvvyTools' Hash Generator includes an HMAC-SHA256 mode built for exactly this kind of manual check: paste in a payload and a secret key, get the signature back instantly, no local script or dependency required. It's a fast way to confirm your server-side verification logic produces the same output as a known-correct implementation before you wire up a real integration.
Photo by Brett Sayles on Pexels
What to Check Before You Trust a Signature Match
Before shipping a webhook handler to production, walk through this short list:
- Are you comparing signatures with a constant-time function, not a standard equality check?
- Is the underlying hash SHA-256 or stronger, not MD5 or SHA-1?
- Are you signing and verifying against the raw request body, not a re-serialized version of the parsed JSON?
- Does your handler check a timestamp and reject requests outside a reasonable window?
- Has someone actually tested the rejection path with a deliberately tampered payload, not just the happy path?
- Is the shared secret stored the same way you'd store any other credential, not hardcoded or logged anywhere?
Photo by Athena Sandrini on Pexels
The Underlying Principle Extends Beyond Webhooks
The plain-hash-versus-HMAC distinction shows up anywhere a system needs to verify origin, not just integrity: signed API requests, pre-signed upload URLs, and some token formats all lean on the same secret-key-plus-hash construction for the same reason. Once you've internalized why a webhook needs more than a checksum, the pattern becomes recognizable everywhere a service has to trust an incoming request it didn't initiate itself.
If you're building or auditing a webhook integration, a free hash generator by EvvyTools is a reasonable first stop for sanity-checking signatures by hand before trusting a production implementation. For general background on the cryptographic building blocks involved, Wikipedia's entry on HMAC covers the construction in more mathematical detail, OWASP maintains broader guidance on API and webhook security practices worth reviewing alongside this, and NIST publishes the underlying federal standards that most HMAC implementations are built to satisfy. You can browse the rest of the EvvyTools tools directory for related developer utilities, or check the EvvyTools blog for more deep dives like this one.