Developer Guides
Verifying webhook signatures, and the mistakes that void them
A webhook endpoint accepts POST requests from the internet and acts on them — suppressing addresses, marking messages delivered, updating a customer record.
Without verification, anyone who learns the URL can do all of that. The URL being hard to guess is not a control; it leaks into logs, proxies and error trackers.
How it works
The sender computes an HMAC over the request body using a secret only the two of you hold, and sends it in a header with a timestamp. You compute the same HMAC and compare.
The secret is never transmitted. Anyone can see the signature; nobody can produce a valid one for different content without the secret.
Mistake one: verifying the parsed body
The signature covers the exact bytes that were sent. Parse the JSON and re-serialise it and you have changed key order, whitespace and number formatting — the signature will not match, or worse, you will reconstruct something that matches while differing from what was actually sent.
Capture the raw body before any parsing middleware touches it. In most frameworks this needs explicit configuration, because the default is to parse eagerly.
Mistake two: comparing with ==
String comparison short-circuits on the first differing byte, so it takes measurably longer to reject a signature that shares a prefix with the correct one. That timing difference is enough to recover a valid signature byte by byte.
Use the constant-time comparison your language provides —
hmac.compare_digest, crypto.timingSafeEqual,
MessageDigest.isEqual. It exists for this.
Mistake three: ignoring the timestamp
A signature stays valid forever unless you bound it. Anyone who captures one valid request can replay it indefinitely — re-suppressing an address you removed, re-marking a message delivered.
Include the timestamp in the signed payload, and reject anything outside a tolerance of a few minutes. Allow enough slack for clock skew and no more.
Then make the handler idempotent
Delivery is at-least-once. A timeout on your side means the sender retries, and the same event arrives twice. Verification does not change that — a replayed legitimate event has a perfectly valid signature.
Record the event ID and ignore ones you have seen. This also protects against out-of-order delivery, which happens whenever a retry overtakes a later event.
Respond fast, work later
Return 200 as soon as you have verified and recorded the event. Do the work asynchronously.
Senders time out quickly and treat a slow response as a failure, so a handler that does ten seconds of work before responding will receive the same event repeatedly and do that work repeatedly.
Rotating the secret
Accept two secrets during a rotation and try both. Switching atomically means every event in flight fails verification, which is the thing you were trying to prevent.
DKIM signing, SPF alignment and DMARC are set up the moment you verify a domain.