UPDATED · 24 JUN 2026 · EDIT ON GITHUB
GUIDES · SECURE

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.

11 min read Intermediate Python · Node By A. Ortiz

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.

HeaderExampleUse
TD-Signaturet=1718900000,v1=5f3c9a…Timestamp + hex HMAC, comma-separated
TD-Deliverydlv_8Xa2QrUnique delivery id — your dedupe key
TD-Eventdetection.matchedEvent 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.

Make processing idempotent. We retry failed deliveries with backoff, so the same 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 2xx the 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.
RegionEgress range
us-east-1198.51.100.0/24
us-west-2203.0.113.0/24
eu-west-1192.0.2.0/24

Where to go next

← PREV Custom Enrichments NEXT → BYOC Deployment