Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.allium.so/llms.txt

Use this file to discover all available pages before exploring further.

When a webhook sink has a signing key configured, Beam signs every request with HMAC-SHA-256. Your endpoint verifies the signature to confirm the request came from Beam and was not tampered with or replayed. Signing is opt-in per webhook sink. Without a key, Beam still POSTs to your URL but adds no signature headers.

How signing works

Each request carries three headers:
HeaderValue
X-Webhook-TimestampUnix timestamp (seconds) when Beam sealed the request
X-Webhook-NoncePer-request UUID v4
X-Signature-256sha256=<hex> — HMAC-SHA-256 of the signed payload
The signed payload is the concatenation:
<nonce>.<timestamp>.<raw request body>
Binding the nonce and timestamp into the signature means an attacker cannot replay an old request with a fresh nonce or shift the timestamp forward without invalidating the signature.

Enable signing on a sink

  1. Store your signing key as an organization secret. Pick any random high-entropy value (32+ bytes recommended).
  2. Set hash_key on the webhook sink to the secret’s name.
{
  "type": "webhook",
  "name": "my-webhook",
  "url": "https://example.com/webhook/receive",
  "hash_key": "MY_WEBHOOK_SIGNING_KEY"
}
If you rotate the secret value, you must redeploy the pipeline for the new value to take effect. The sink will be flagged as stale until then.

Verify the signature

The verification recipe is the same in every language:
  1. Read the three headers off the incoming request.
  2. Reject if X-Webhook-Timestamp is too old (e.g. more than 5 minutes ago) — this is your replay window.
  3. Recompute sha256= + hmac_sha256(key, nonce + "." + timestamp + "." + raw_body) in hex.
  4. Compare against X-Signature-256 using a constant-time comparison.
Verify against the raw request body bytes, not a re-serialized JSON object. Any whitespace or key-order difference will change the hash and break verification.
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException

SIGNING_KEY = b"your-signing-key"  # same value stored in the org secret
MAX_AGE_SECONDS = 300

app = FastAPI()

@app.post("/webhook/receive")
async def receive(request: Request):
    ts = request.headers.get("X-Webhook-Timestamp", "")
    nonce = request.headers.get("X-Webhook-Nonce", "")
    sig = request.headers.get("X-Signature-256", "")

    if not (ts and nonce and sig):
        raise HTTPException(401, "missing signature headers")
    if abs(time.time() - int(ts)) > MAX_AGE_SECONDS:
        raise HTTPException(401, "stale timestamp")

    body = await request.body()
    signed = f"{nonce}.{ts}.".encode() + body
    expected = "sha256=" + hmac.new(SIGNING_KEY, signed, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, sig):
        raise HTTPException(401, "bad signature")

    return {"ok": True}

Replay protection

The timestamp + nonce pair lets you reject duplicates:
  • Timestamp window — reject any request whose X-Webhook-Timestamp is more than ~5 minutes off your server clock. A short window keeps the nonce cache small.
  • Nonce cache (optional, defense-in-depth) — record each X-Webhook-Nonce you’ve accepted in a short-TTL cache (Redis, etc.) and reject repeats. Cache TTL should match your timestamp window.
For most consumers the timestamp check alone is sufficient.