Webhooks

Webhook delivery is available on Edge and higher plans. Registration and re-enabling require an API key with webhook:write scope. Destinations must use HTTPS on port 443.

Every tier can list and delete its existing subscriptions. If a stored subscription is not currently entitled to delivery, API responses for that key include a meta.webhook_access notice; no events are sent until Edge access and the webhook:write scope are both present.

Events are delivered at least once. Use the event id to deduplicate retries; ordering across different events is not guaranteed.

Subscribing

POST /v1/webhooksbash
curl -X POST https://api.bigballsdata.com/v1/webhooks \
  -H "Authorization: Bearer bbs_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: sub_5f3a..." \
  -d '{
    "url": "https://example.com/webhooks/bbs",
    "events": ["score_update", "match_end"]
  }'
Responsejson
{
  "data": {
    "id": "whsub_5f3a8c2d...",
    "url": "https://example.com/webhooks/bbs",
    "events": ["score_update", "match_end"],
    "secret": "a1b2c3d4...",
    "enabled": true,
    "secret_notice": "Shown once. Store it now; it cannot be retrieved later."
  },
  "meta": { "request_id": "..." },
  "error": null
}
secret is returned only at creation. Store it immediately. To replace a lost or exposed secret, delete the registration and create a new one.

Event types

EventFires when
score_updateHome or away score changes during a live match
match_startA match transitions to live
match_endMatch status transitions to final

Delivery payload

jsonjson
{
  "id": "evt_8c2d5f3a...",
  "type": "score_update",
  "occurredAt": "2026-05-15T19:43:11Z",
  "data": {
    "match_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "home_score": 2,
    "away_score": 1,
    "status": "live",
    "minute": 67
  }
}

Verifying signatures

Each request carries X-BBS-Signature in the form t=<unix_ts>,v1=<hmac_hex>. Compute the HMAC over t + '.' + raw_body using your signing secret and compare in constant time.

Node.js verificationtypescript
import crypto from 'node:crypto';

function verify(raw: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = parts.t;
  const sig = parts.v1;
  if (!t || !sig) return false;

  // Reject timestamps older than 5 minutes to prevent replay
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(t + '.' + raw)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Retry policy

A non-2xx response triggers four retries: after 30s, 2m, 10m, and 1h (five attempts total). After five failed attempts the delivery is marked permanently failed and visible in /dashboard/webhooks. Registrations with three consecutive permanently failed deliveries are parked. After fixing the endpoint, re-enable it with POST /v1/webhooks/{id}/unpark.

Each delivery attempt is auditable from the dashboard with event id, attempt number, HTTP status, and response time.

Idempotency

Webhook deliveries can fire more than once, a retry after a network blip is the most common case. Treat evt_* ids as idempotency keys on your side. The same event id is never paired with two different payloads.