A webhook sample should compute an HMAC of the raw request body with a shared secret, compare it to the signature header using a constant time comparison, and reject the request if they differ. Samples that skip this step teach readers to accept events from anyone who finds the URL.
Why the sample matters
A webhook endpoint is a public URL that performs actions when it receives a request. Anyone who learns the address can send a forged event unless the receiver checks that it came from the real sender. Tutorials often show only the happy path: parse the JSON, do the thing. The reader ships exactly that.
The signature check is a few lines. Leaving it out of the sample is the most expensive omission in the tutorial.
The pattern
- The sender and receiver share a secret, stored outside source code.
- The sender computes an HMAC of the request body and sends it in a header.
- The receiver recomputes the HMAC over the raw bytes it received, before any parsing.
- The receiver compares the two values with a constant time function.
- If they differ, the receiver returns an error and does nothing else.
The Python documentation for the hmac module carries a warning relevant to step four. When comparing a digest to an externally supplied one in a verification routine, it recommends using compare_digest() instead of the == operator, to reduce the vulnerability to timing attacks.
import hashlib
import hmac
import os
SECRET = os.environ["WEBHOOK_SECRET"].encode()
def is_valid(raw_body: bytes, signature_hex: str) -> bool:
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_hex)
Check your sender's documentation for the exact header name, the encoding of the signature and whether a timestamp is included. Formats differ between providers, so follow the sender's specification rather than copying this shape blindly.
Details that tutorials get wrong
| Mistake | Consequence |
|---|---|
| Verifying a re-serialised JSON body | The bytes differ from what was signed, so valid events fail, and people disable the check |
| Comparing with the plain equality operator | Opens the door to timing attacks |
| Hardcoding the secret in the sample | Readers commit it |
| Parsing and acting before verifying | Forged data gets processed |
| No replay protection | An old valid request can be sent again |
For replay protection, if the sender includes a timestamp in the signed data, reject events outside a short window, and keep a record of event IDs you have already processed. Process each event once, since senders may retry.
Make the sample testable
Include a test in the sample: one request with a valid signature that is accepted, one with a wrong signature that is rejected, and one with a tampered body that is rejected. Readers learn the failure case, and your docs build proves the sample works.
Related reading: require reauthentication for sensitive API actions and how to answer security questionnaires about API handling.
Try it yourself
An API scan shows what an endpoint accepts from an unauthenticated caller. Click through one.
Click through it at your own pace, or open it full screen.
Safeguard offers API security scanning and code scanning that can run on the sample receivers you publish. The free scan is at https://gold.safeguard.sh/scan.
Quick check
Why not use the equality operator to compare signatures?
The Python hmac documentation recommends compare_digest to reduce exposure to timing attacks.
Should the sample parse the JSON first?
No. Verify the signature over the raw body first, then parse.
Sources
Last reviewed: 19 October 2026.