Webhooks

Register an endpoint and we deliver each confirmed event as a signed HTTP POST — with retries, replay, and HMAC signatures. Best when you'd rather receive pushes than hold a socket open.

Register an endpoint

bash
curl -X POST https://api.firstbuzzer.com/v1/webhooks \
  -H "Authorization: Bearer $FB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.com/firstbuzzer", "sports": ["nba","nhl"], "tier": "confirmed" }'

The response includes a secret — store it; you'll use it to verify signatures.

Payload & headers

http
POST /firstbuzzer HTTP/1.1
X-FB-Topic: event.score
X-FB-Event-Id: evt_8f21
X-FB-Delivery-Id: dlv_55a0
X-FB-Signature: t=1782693490,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08

{ "type": "score", "tier": "confirmed", "game_id": "nba_401766",
  "team": "BOS", "points": 3, "ts": "2026-06-29T01:14:22.184Z",
  "confidence": { "agreement": 0.94, "reporters": 7, "latency_ms": 820 },
  "schema": "fb.event.v1" }

Signature verification

The signature is HMAC-SHA256 over {timestamp}.{raw_body}, keyed by your endpoint secret. Always verify before trusting a payload, and reject timestamps older than ~5 minutes.

node.js
import crypto from "node:crypto";

const TOLERANCE_S = 300; // reject signatures older than 5 minutes (replay)

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > TOLERANCE_S) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
python
import hmac, hashlib, time

TOLERANCE_S = 300  # reject signatures older than 5 minutes (replay)

def verify(raw_body: str, header: str, secret: str) -> bool:
    parts = dict(p.split("=") for p in header.split(","))
    if "t" not in parts or "v1" not in parts:
        return False
    if abs(time.time() - int(parts["t"])) > TOLERANCE_S:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Retries & auto-disable

Return 2xx within 5s to acknowledge. Non-2xx or a timeout is retried with backoff:

AttemptDelay
1immediate
21 minute
35 minutes
430 minutes
5+2 hours (up to your plan's max attempts)

An endpoint that fails every delivery for 24 hours is auto-disabled and you're notified. Re-enable it from the dashboard once it's healthy.

Delivery statuses

StatusMeaning
pendingQueued or mid-retry.
deliveredYour endpoint returned 2xx.
failedExhausted retries.
disabledEndpoint auto-disabled after sustained failure.

Replay any delivery from the dashboard or POST /v1/webhooks/:id/replay. Delivery caps are per plan.

Questions? Talk to us. Pre-launch — endpoints illustrate the shape of the API.