Skip to Content
WebhooksSignature verification

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=4f5a89e2b1d2c8f3a4c5b6e7d8f9012a3b4c5d6e7f8901a2b3c4d5e6f78901a2b
  • t= — Unix timestamp (seconds) of when Inferio signed the payload
  • v1= — hex-encoded HMAC-SHA256 of {t}.{request_body} with your whsec_* 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.

  1. Dashboard → Settings → Webhooks → endpoint → Rotate secret
  2. Update your servers to verify against BOTH secrets during the window (try new first; on mismatch, try old)
  3. 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.