# Webhook signing

> 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

JavaScriptPythonCopy
```
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.
