Compute an HMAC with SHA-256 over the exact raw request body using your shared secret, then compare it to the signature header with a constant time function. Reject the request if they differ. Add a timestamp check to stop replays. Never parse and re-serialize the body before checking.
What an HMAC gives you
An HMAC combines a hash function with a secret key. Only parties who know the key can produce a matching value, so a valid signature shows the message came from the key holder and was not changed. It does not hide the content.
The sender signs the body and sends the result in a header. You repeat the computation and compare.
The verification code
The Python documentation shows the pattern. The digestmod argument is required since Python 3.8, and hmac.compare_digest is recommended instead of the == operator for comparing against an externally supplied digest.
import hmac
import hashlib
def verify(secret: bytes, raw_body: bytes, header_hex: str) -> bool:
expected = hmac.new(secret, raw_body, digestmod=hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header_hex)
Both values must be the same type. Here both are ASCII hex strings. Normalize case first if the sender may use uppercase.
Steps in a handler
- Read the raw body bytes before any framework parsing.
- Read the signature header and the timestamp header, if the provider sends one.
- Reject if the timestamp is outside a short window, to limit replays.
- Compute the HMAC over the exact bytes the provider documents, which may include the timestamp.
- Compare in constant time.
- Only after it passes, parse the JSON and act.
- Store the event id and ignore duplicates, since providers retry.
Common mistakes
| Mistake | Effect |
|---|---|
| Verifying a parsed and re-serialized body | Whitespace or key order changes break or weaken the check |
| Using == on the signature | Timing leak, see CWE-208 |
| Hard coding the secret in the repo | OWASP says keys must not be committed to source repositories |
| One secret for every provider and environment | A leak in one place exposes all |
| Skipping the check in test environments | The skip reaches production |
| Returning detailed errors | Helps an attacker probe |
| No replay protection | A captured valid request works again |
Secret handling
Treat the signing secret like any key. Keep it in a secrets manager, one per integration and environment, and have a rotation plan that lets old and new secrets work for a short overlap. Do not log it or the full signature.
Which hash
SHA-256 is the usual choice for HMAC. Follow the provider's documented algorithm, and avoid MD5 and SHA-1 in any new work, as CWE-327 lists both among algorithms once thought strong that can now be broken.
Related reading
For the timing issue in detail, see constant time comparison for secrets and signatures.
Safeguard can generate an SBOM and scan your dependencies, so outdated cryptography libraries with known advisories show up in one inventory, and policies can gate a build on them. A free scan is at gold.safeguard.sh/scan.
Quick check
Is HTTPS enough without a signature?
HTTPS protects the channel. A signature proves who sent the message. Anyone who learns your webhook URL can post to it, so you want both.
Why use the raw body?
The signature covers exact bytes. Any change in whitespace or field order produces a different HMAC.
Sources
Last reviewed: 19 October 2026.