Each delivery is a POST with Content-Type: application/json and these headers:
X-TasScan-Signature: t=<unix seconds>,v1=<hex>[,v1=<hex>], wherev1 = HMAC-SHA256(secret, "<t>.<raw body>"). Twov1values are present while a rotated secret still overlaps; accept the delivery if any matches.X-TasScan-Event(the event type),X-TasScan-Event-Id(the same for every retry and resend of one event) andX-TasScan-Delivery-Id.
Rules
- Always verify against the raw request body before you parse it.
- Reject timestamps older than 5 minutes.
- Keep processed
X-TasScan-Event-Idvalues for at least 24 hours so retries and replays are ignored. - Answer with any 2xx within 10 seconds. Redirects are not followed.
Python
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = [p.split("=", 1) for p in header.split(",") if "=" in p]
t = next((v for k, v in parts if k == "t"), None)
sigs = [v for k, v in parts if k == "v1"]
if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, s) for s in sigs)
Node
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody, tolerance = 300) {
const parts = header.split(",").map((p) => p.split("="));
const t = parts.find(([k]) => k === "t")?.[1];
const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
if (!t || !/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > tolerance) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
return sigs.some((s) => {
const got = Buffer.from(s, "hex");
return got.length === expected.length && timingSafeEqual(got, expected);
});
}