Safeguard
Security Guides•

How should a webhook sample in docs verify the sender's signature?

A webhook sample that skips signature checks teaches readers to trust any caller. Verify an HMAC over the raw body and compare it in constant time.

Safeguard Security Team
4 min read

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

  1. The sender and receiver share a secret, stored outside source code.
  2. The sender computes an HMAC of the request body and sends it in a header.
  3. The receiver recomputes the HMAC over the raw bytes it received, before any parsing.
  4. The receiver compares the two values with a constant time function.
  5. 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

MistakeConsequence
Verifying a re-serialised JSON bodyThe bytes differ from what was signed, so valid events fail, and people disable the check
Comparing with the plain equality operatorOpens the door to timing attacks
Hardcoding the secret in the sampleReaders commit it
Parsing and acting before verifyingForged data gets processed
No replay protectionAn 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.