Skip to content

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.

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);
});
import hashlib
import hmac
import 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