Verify inbound webhooks.
threatDefendr signs every webhook it sends. An unverified endpoint is an open door — anyone who learns the URL can forge an event. This guide shows how to verify the signature, reject stale and replayed deliveries, and harden the receiver, exactly the way our own services consume one another.
How we sign
Every delivery carries an HMAC-SHA256 signature computed over the timestamp and the exact raw request body, plus headers you use to dedupe and route. Sign over the raw bytes — parse the JSON only after the signature checks out, never before.
| Header | Example | Use |
|---|---|---|
TD-Signature | t=1718900000,v1=5f3c9a… | Timestamp + hex HMAC, comma-separated |
TD-Delivery | dlv_8Xa2Qr | Unique delivery id — your dedupe key |
TD-Event | detection.matched | Event type, for routing |
Verify a webhook
The check is the same in any language: split the header, reject anything older than five minutes, recompute the HMAC over timestamp.body, and compare in constant time. A non-constant-time compare leaks the signature one byte at a time.
PYTHONverify.py import hashlib, hmac, time def verify(secret: str, headers: dict, body: bytes) -> bool: parts = dict(p.split("=", 1) for p in headers["TD-Signature"].split(",")) ts, v1 = parts["t"], parts["v1"] # 1. reject stale timestamps (more than 5 min of skew) if abs(time.time() - int(ts)) > 300: return False # 2. recompute over "timestamp.body" and compare in constant time signed = ts.encode() + b"." + body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1)
NODEverify.mjs import crypto from "node:crypto"; export function verify(secret, headers, body) { const p = Object.fromEntries( headers["td-signature"].split(",").map((kv) => kv.split("=")) ); const skew = Math.abs(Date.now() / 1000 - Number(p.t)); if (skew > 300) return false; const expected = crypto .createHmac("sha256", secret) .update(p.t + ".") .update(body) // Buffer of the raw body .digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1)); }
Replay protection
A valid signature only proves the payload is authentic, not that it is fresh. The timestamp window stops yesterday's capture from being replayed today; deduplicating on TD-Delivery stops the same delivery from being processed twice during a retry storm.
TD-Delivery can legitimately arrive more than once. Record processed ids for at least 24 hours and treat a repeat as a no-op — the safest receiver can be delivered the same event ten times with no side effect.Rotate the secret
Rotate signing secrets on a schedule and after any suspected exposure. Rotation runs a dual-sign window: both the old and new secret verify until you finalize, so there is no flag-day where in-flight deliveries fail.
SHELLrotate $ td webhooks rotate --endpoint ep_4kQ -> generated secret v2 . both v1 and v2 now verify -> dual-sign window: 24h (expires 25 JUN 14:02 UTC) ok roll your verifier to v2, then finalize: td webhooks rotate --finalize ep_4kQ
Harden the endpoint
Verification is the core, but the endpoint around it matters too. Acknowledge fast and process asynchronously so a slow handler never triggers our retries; restrict who can even reach the receiver.
- Ack in under a second — return
2xxthe moment the signature verifies, then enqueue the work; do the heavy lifting off the request path. - TLS only — we will not deliver to plaintext HTTP, and we pin to modern ciphers.
- Allowlist our egress — accept webhook traffic only from the published ranges for your region.
| Region | Egress range |
|---|---|
| us-east-1 | 198.51.100.0/24 |
| us-west-2 | 203.0.113.0/24 |
| eu-west-1 | 192.0.2.0/24 |
Where to go next
- API reference — the webhook event catalog and retry semantics.
- Security model — how we manage keys on our side.
- Trust center — attestations and disclosure.