- Home
- Webhooks
- Verify signatures
Verify signatures
Check that a webhook really comes from Baiyar: the signature header, the tolerance, and examples in Node.js and Python.
Every request carries Baiyar-Signature: t=<unix seconds>,v1=<hex>. The v1 value is the HMAC-SHA256, with your endpoint
secret as the key, of the string <t>.<raw request body>. While a secret is being rotated there are two v1 values: accept
the request if any of them matches. Also reject a t that is more than 5 minutes from your clock, so a captured request
cannot be replayed later.
Verify against the raw body, byte for byte, before you parse it.
Node.js
Section titled “Node.js”import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBaiyarSignature(header, rawBody, secret, toleranceSeconds = 300) { const parts = header.split(",").map((part) => part.trim()); const timestamp = Number(parts.find((part) => part.startsWith("t="))?.slice(2)); if (!Number.isInteger(timestamp)) return false; if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest(); return parts .filter((part) => part.startsWith("v1=")) .some((part) => { const given = Buffer.from(part.slice(3), "hex"); return given.length === expected.length && timingSafeEqual(given, expected); });}
// Express: keep the raw body for this route.app.post("/baiyar/webhook", express.raw({ type: "application/json" }), (req, res) => { const ok = verifyBaiyarSignature( req.get("Baiyar-Signature") ?? "", req.body.toString("utf8"), process.env.BAIYAR_WEBHOOK_SECRET, ); if (!ok) return res.sendStatus(400); const event = JSON.parse(req.body.toString("utf8")); // Handle event.type, then answer quickly. res.sendStatus(200);});Python
Section titled “Python”import hashlibimport hmacimport time
def verify_baiyar_signature(header: str, raw_body: bytes, secret: str, tolerance_seconds: int = 300) -> bool: parts = [part.strip() for part in header.split(",")] timestamps = [part[2:] for part in parts if part.startswith("t=")] if not timestamps or not timestamps[0].isdigit(): return False timestamp = int(timestamps[0]) if abs(time.time() - timestamp) > tolerance_seconds: return False
signed = f"{timestamp}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any( hmac.compare_digest(part[3:], expected) for part in parts if part.startswith("v1=") )
# Flask@app.post("/baiyar/webhook")def baiyar_webhook(): raw = request.get_data() # the raw bytes, before any parsing if not verify_baiyar_signature(request.headers.get("Baiyar-Signature", ""), raw, os.environ["BAIYAR_WEBHOOK_SECRET"]): return "", 400 event = request.get_json() return "", 200