Webhooks
Webhook signing
Current documentation
Verify that events really came from Beam using the Beam-Signature header.
The scheme
- Header:
Beam-Signature: t=<unix_seconds>,v1=<hex signature> - Signed payload:
t + "." + raw_body(the exact raw bytes, never re-serialized JSON) - Algorithm: HMAC-SHA256 with your signing secret (Settings → Event webhooks)
- Reject events older than 5 minutes to block replays, and compare with a constant-time function
import crypto from "node:crypto"; function verifyBeamSignature(rawBody, header, secret) { const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header ?? ""); if (!m) return false; const [, t, v1] = m; if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const expected = crypto.createHmac("sha256", secret) .update(`${t}.${rawBody}`).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); }
import hmac, hashlib, re, time def verify_beam_signature(raw_body: bytes, header: str, secret: str) -> bool: m = re.match(r"t=(\d+),v1=([a-f0-9]+)", header or "") if not m: return False t, v1 = m.group(1), m.group(2) if abs(time.time() - int(t)) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1)
Use the raw bodyVerify against the exact bytes you received. Parsing the JSON and re-serializing it will produce different bytes and a failed signature, even for a genuine event.