Webhooks
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
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"]
}'{
"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
| Event | Fires when |
|---|---|
| score_update | Home or away score changes during a live match |
| match_start | A match transitions to live |
| match_end | Match status transitions to final |
Delivery payload
{
"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.
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.