An SDK should include a webhook verification helper that computes an HMAC over the raw request body, compares it to the signature header in constant time, and rejects stale timestamps. Make the safe path one function call, so customers do not write their own comparison and get it wrong.
Why the helper belongs in the SDK
If you only document the algorithm, every customer reimplements it. Common mistakes follow: parsing the JSON first and signing the re-serialised text, comparing with an ordinary equality check, ignoring replays, and logging the secret. A helper in the SDK removes those choices.
The Python standard library documentation is direct about one of them: when comparing a computed digest with an externally supplied one in a verification routine, it recommends the compare_digest function rather than the equality operator, to reduce the vulnerability to timing attacks. That function is described as designed to avoid content based short circuiting.
What the helper should do
- Take the raw bytes of the body exactly as received, the signature header value and the endpoint secret. Parsers and framework middleware often alter the body, so say clearly in docs that verification must happen before parsing.
- Compute HMAC with a strong hash over a defined string. A common design signs the timestamp and the body joined by a separator, so the timestamp is covered by the signature.
- Compare in constant time using the language's dedicated function.
- Check the timestamp against the current time with a tolerance, and reject anything outside it. This limits replay of captured requests.
- Return a clear failure type that does not echo the expected signature.
- Support more than one active secret during rotation, so customers can roll keys without dropping events.
A sketch
import hmac, hashlib, time
def verify(body: bytes, timestamp: str, signature: str, secret: bytes, tolerance=300):
if abs(time.time() - int(timestamp)) > tolerance:
raise ValueError("timestamp outside tolerance")
signed = timestamp.encode() + b"." + body
expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("signature mismatch")
This is a teaching sketch, not a drop in implementation. Add error handling for a malformed timestamp and for multiple signature values.
Design choices at a glance
| Choice | Safer default |
|---|---|
| Input to the signature | Raw body bytes plus timestamp |
| Comparison | Constant time function |
| Replay protection | Timestamp window, plus event ID tracking for strict cases |
| Secret handling | Per endpoint secret, never logged, rotation supported |
| Failure behavior | Reject with a generic error and a 4xx status |
Tests to ship with the helper
- A valid signature passes.
- A body changed by one byte fails.
- A timestamp outside the window fails.
- A signature in the wrong case or length fails without throwing an unexpected exception.
- Two secrets, one old and one new, both verify during rotation.
Publish these as examples customers can copy. Also document the status code your senders expect so retries behave, as covered in safe retry rules.
Quick check
Why not compare signatures with the equality operator?
Ordinary comparison can stop at the first differing character, which can leak timing information. The Python documentation recommends compare_digest for this purpose.
Does a timestamp prevent all replays?
It limits them to the tolerance window. For stricter needs, also record event IDs you have already processed.
Safeguard's policies and gates can check that pull requests use your approved helper, and its dependency scanning shows which crypto libraries are in the tree. Try the free scan.
Sources
Last reviewed: 19 October 2026.