Signature verification
Every webhook Inferio sends is signed with HMAC-SHA256 using your
endpoint’s whsec_* secret. Verifying the signature is mandatory in
production — without it, anyone with your webhook URL can POST fake
payloads.
The signature header
X-Inferio-Signature: t=1735634283,v1=4f5a89e2b1d2c8f3a4c5b6e7d8f9012a3b4c5d6e7f8901a2b3c4d5e6f78901a2bt=— Unix timestamp (seconds) of when Inferio signed the payloadv1=— hex-encoded HMAC-SHA256 of{t}.{request_body}with yourwhsec_*secret
Verification recipe
import crypto from "node:crypto";
function verifyInferio(
rawBody: string, // the raw request body — DO NOT JSON.parse first
signatureHeader: string, // X-Inferio-Signature header
secret: string, // whsec_…
): boolean {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("=")),
);
const timestamp = parts.t;
const signature = parts.v1;
// Reject if the signature is older than 5 minutes — replay protection
const ageSeconds = Math.floor(Date.now() / 1000) - Number(timestamp);
if (ageSeconds > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Constant-time comparison — prevents timing-attack token guessing
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
);
}Python equivalent:
import hmac, hashlib, time
def verify_inferio(raw_body: bytes, signature_header: str, secret: str) -> bool:
parts = dict(kv.split("=") for kv in signature_header.split(","))
if int(time.time()) - int(parts["t"]) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{parts['t']}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Key rotation with grace
Endpoints can hold up to two active secrets at once — when you rotate, the new secret signs new deliveries while the old one stays valid for 24h. That lets you roll the secret across your servers without an outage.
- Dashboard → Settings → Webhooks → endpoint → Rotate secret
- Update your servers to verify against BOTH secrets during the window (try new first; on mismatch, try old)
- After 24h, revoke the old secret
Common pitfalls
- Don’t JSON.parse before verifying. The signature is over the raw body bytes, exactly as sent. Frameworks that auto-parse (Express default body parser, Next.js API routes) need a raw-body middleware.
- Timing-safe comparison is mandatory. A naive
==comparison leaks the signature byte-by-byte over enough requests. - The 5-minute window is enforced for replay protection. If your servers’ clocks drift past 5 minutes, every webhook will be rejected — keep them NTP-synced.
Test endpoints (via the dashboard’s “Send test event”) are also HMAC-signed with the same secret. If your test events are bouncing, your verifier has a bug — fix it before going to production.