What Is a Webhook Signature? β Definition and How It Works
Learn how HMAC webhook signatures work, why constant-time comparison matters, and how to prevent replay attacks.
A webhook signature is a cryptographic value a sender attaches to a webhook request so the receiver can verify it genuinely came from that sender and was not modified.
1. Why It Is Necessary
A webhook endpoint is a public URL that accepts POST requests. Without verification, anyone who discovers it can send fabricated events β a forged payment confirmation, a fake account update.
The signature turns "this request arrived at my endpoint" into "this request came from the sender who holds the shared secret".
2. How It Works
The sender computes an HMAC over the request body using a secret both parties hold, and sends it in a header:
POST /webhooks/provider HTTP/1.1
X-Signature: t=1787000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Content-Type: application/json
{"event":"payment.succeeded","id":"evt_123","amount":1999}
The receiver recomputes the HMAC over the raw body and compares.
3. Verifying It Correctly
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts, sig = parts.get("t"), parts.get("v1")
if not ts or not sig:
return False
if abs(time.time() - int(ts)) > tolerance: # replay window
return False
signed = f"{ts}.".encode() + raw_body # timestamp bound into the MAC
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig) # constant time
Three details in that function carry all the security:
The raw body. Sign and verify the exact bytes received. Parsing to JSON and re-serialising changes whitespace and key order, and the signature will not match β this is the single most common integration failure.
compare_digest. A plain == on strings short-circuits on the first differing byte, leaking information through timing that can be used to forge a signature byte by byte. Constant-time comparison is not optional.
The timestamp inside the MAC. Including the timestamp in the signed material is what prevents an attacker from replaying a captured request with a fresh timestamp.
4. Replay Protection Needs More Than a Timestamp
A timestamp tolerance narrows the replay window; it does not close it. Within the window, the same valid request can be sent again.
Full protection requires idempotency: record each event id and ignore repeats.
if store.seen(event_id):
return 200 # already processed; acknowledge and do nothing
store.mark_seen(event_id)
process(event)
That is the same idempotency discipline any at-least-once delivery system needs β and webhook senders retry, so duplicates are the normal case rather than an attack.
5. Common Mistakes
| Mistake | Consequence |
|---|---|
| Verifying re-serialised JSON | Signature never matches |
== instead of constant-time compare |
Timing attack |
| Skipping verification in development | The code ships that way |
| Ignoring the timestamp | Unlimited replay |
| No event-id deduplication | Duplicate processing on retries |
| Logging the signature header | Aids offline attack; and the secret is adjacent |
| Returning non-2xx on a duplicate | Sender retries forever |
6. Rotating the Secret
Signing secrets need rotation, and a sender that supports overlapping secrets makes it safe: accept a signature matching either the old or the new secret during the overlap, then drop the old one.
Treat the secret exactly as a credential β a secret manager, not a config file, and never in logs or a repository.
7. Where This Comes Up in Collection Work
Two directions:
Receiving β a data provider or scraping service notifying you that a job finished. Verify it, or your pipeline can be driven by fabricated completions.
Sending β if you notify downstream systems of new data, sign your own webhooks and publish how to verify them.
Either way, the endpoint is public and the signature is what makes it trustworthy.
8. Summary
A webhook signature is an HMAC over the request body with a shared secret, letting a receiver confirm origin and integrity on a publicly reachable endpoint.
Verify against the raw bytes rather than re-serialised JSON, use a constant-time comparison, bind a timestamp into the signed material and enforce a tolerance window β and deduplicate on event id, because senders retry and duplicates are routine rather than exceptional.