Webhooks
POST /v1/webhooks returns 501 not_implemented. We are not delivering webhooks, so accepting a registration would mean promising traffic that never arrives.This page previously described retrying, per-attempt-audited delivery. That was never true — the delivery path was never wired to anything, so no webhook has ever been sent. We would rather say that plainly than leave you building a receiver for a feed that does not exist.
What works today: poll the REST API, or use the WebSocket feed at
wss://api.bigballsdata.com/live for live score push on the Pro plan and above. Webhooks will be reopened only once delivery is built and verified end to end.The rest of this page documents the interface as it is designed, and is kept as a reference for what will return. Nothing below is callable today.
Subscribing (not available)
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_final"],
"filters": { "sport": "football", "league_id": "epl" }
}'{
"data": {
"id": "whsub_5f3a8c2d...",
"url": "https://example.com/webhooks/bbs",
"events": ["score_update", "match_final"],
"filters": { "sport": "football", "league_id": "epl" },
"signing_secret": "whsec_a1b2c3d4..."
},
"meta": { "request_id": "..." },
"error": null
}signing_secret is returned only at creation. Store it immediately. Verifying signatures without it is impossible; rotating it requires creating a new subscription.Event types
| Event | Fires when |
|---|---|
| score_update | Home or away score changes during a live match |
| match_status_change | Status transitions (scheduled → live → final) |
| match_final | Match status transitions to final |
| odds_update | Aggregated market price moves for a tracked match |
| event_added | A goal, card, substitution, or scoring play is added |
Delivery payload
{
"id": "evt_8c2d5f3a...",
"type": "score_update",
"created": "2026-05-15T19:43:11Z",
"data": {
"match_id": "match_4a2f...",
"home": { "team_id": "team_arsenal", "score": 2 },
"away": { "team_id": "team_liverpool", "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.
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 retry with exponential backoff: 30s, 2m, 10m, 1h, 6h. After five failed attempts the delivery is marked permanently_failed and visible in /dashboard/webhooks. Subscriptions with three consecutive permanently_failed deliveries are auto-disabled.
Each delivery attempt is auditable from the dashboard with request id, attempt number, HTTP status, response time, and replay button.
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.