Verifying signatures
Every webhook delivery carries a Zillo-Signatureheader. Verify it on every request — an attacker who guesses your URL can send fake payloads if you don't.
Header shape
Zillo-Signature: t=1717592400,v1=3a1b9d7e2c4f8a6b0d5e7c1f9a3b5d7e2c4f8a6b0d5e7c1f9a3b5d7e2c4f8a6b
t= unix seconds when we signed the payload.v1= hex-encoded HMAC-SHA256 over`${t}.${raw_body}`.
Node.js / TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyZilloSignature(opts: {
header: string; // value of the Zillo-Signature header
body: string; // raw request body — read BEFORE JSON.parse
secret: string; // your endpoint's signing secret
toleranceSeconds?: number; // default 300 (5 min)
}): boolean {
const tolerance = opts.toleranceSeconds ?? 300;
const parts = Object.fromEntries(
opts.header.split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}),
);
const t = Number(parts.t);
const v1 = parts.v1 as string;
if (!Number.isFinite(t) || !v1) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > tolerance) return false;
const mac = createHmac("sha256", opts.secret);
mac.update(`${t}.${opts.body}`);
const expected = mac.digest("hex");
if (expected.length !== v1.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Python
import hmac, hashlib, time
def verify_zillo_signature(header: str, body: bytes, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
v1 = parts["v1"]
if abs(int(time.time()) - t) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Pitfalls
- Sign the raw body, not parsed JSON. Frameworks that auto-parse the body before your code runs will change whitespace and break the HMAC.
- Compare in constant time. Use
timingSafeEqual/hmac.compare_digest— never===. - Reject stale timestamps. A 5-minute tolerance kills most replay attempts.