Conventions & limits#
Every BSD API follows the same conventions. Learn them once and every endpoint on this site behaves the same way.
Base URL#
All APIs live under one host:
https://sports.bzzoiro.com
| Family | Base path |
|---|---|
| Football v2 (recommended) | /api/v2/ |
| Football v1 (legacy) | /api/ |
| Tennis / CS2 / Darts / Hockey / Basketball / Horse racing | /<sport>/api/v2/ |
| Signals | /api/signals/ |
| MCP servers | /mcp and /<sport>/mcp |
| WebSockets | wss://sports.bzzoiro.com/live/football/ and /ws/live/ |
| Images | /img/ |
Pagination#
List endpoints are paginated with limit and offset. Default page size is
50, maximum 200.
GET /api/v2/events/?limit=50&offset=50 # items 51–100
{
"count": 408,
"next": "https://sports.bzzoiro.com/api/v2/events/?limit=50&offset=100",
"previous": "https://sports.bzzoiro.com/api/v2/events/?limit=50",
"results": [ ... ]
}
Dates and times#
- Timestamps are ISO 8601 in UTC:
2026-08-02T14:00:00+00:00. - Date filters take
YYYY-MM-DD:?date_from=2026-08-01&date_to=2026-08-07. - The football v1 API accepts a
tzparameter to shift datetimes; v2 always returns UTC — convert client-side.
Status values#
| Status | Meaning |
|---|---|
upcoming |
Not started yet |
live |
In progress |
finished |
Ended, result final |
cancelled |
Called off before starting |
postponed |
Rescheduled to a future date |
unresolved |
Kickoff passed, the match was never seen played, and no result was ever recorded. Scores stay null — nothing is inferred |
Odds format#
All odds everywhere are decimal (e.g. 1.85). null means the market is
not priced yet. Implied probabilities, where provided, are 0–1 floats.
IDs#
Use BSD numeric ids (id fields) for everything — they are stable and work
across REST, MCP, WebSockets and the Image API. Never guess ids: resolve them
through list/search endpoints first.
Error shape#
Every API error is JSON with the same envelope:
{ "error": true, "status": 404, "detail": "Not found." }
| Code | Meaning |
|---|---|
200 |
Success |
400 |
Invalid parameter (detail says which) |
401 |
Missing or invalid API token — see Authentication |
402 |
Valid token but the endpoint needs a paid add-on |
404 |
Resource doesn't exist |
429 |
Rate limit exceeded — code says which of the two limits, see Rate limits |
503 |
Feature temporarily unavailable (e.g. a prediction model is offline) |
Unknown query parameters#
A /api/v2/ endpoint rejects any query parameter it does not accept, rather
than ignoring it. This is deliberate: a dropped filter would return 200 with
the unfiltered list, and there is no way to tell that apart from a filter
that matched everything.
GET /api/v2/events/?league=17
{
"detail": "Unknown query parameter(s): league. Did you mean: league -> league_id?",
"unknown_parameters": ["league"],
"accepted_parameters": ["date_from", "date_to", "league_id", "limit",
"offset", "season_id", "status", "team_id", "team_name"],
"suggestions": { "league": "league_id" }
}
accepted_parameters is the full list that endpoint takes, so the response
tells you the fix without a trip to the docs. suggestions is present only
when we can guess what you meant.
Two parameters are never treated as filters and are always allowed:
format (renderer switch) and token (query-string auth).
Endpoints outside /api/v2/ are unaffected.
v1 parameter names still work#
If you moved from /api/ to /api/v2/ and kept your querystring, you do not
have to rewrite it. The v1 names are translated before the request is handled,
so these pairs are equivalent:
| v1 | v2 |
|---|---|
?league=17 |
?league_id=17 |
?season=61627 |
?season_id=61627 |
?team=Arsenal |
?team_name=Arsenal |
?bookmaker=pinnacle |
?bookmaker_slug=pinnacle |
?page_size=50 |
?limit=50 |
?page=3&limit=50 |
?offset=100&limit=50 |
?date=2026-08-17 |
?date_from=2026-08-17T00:00:00Z&date_to=2026-08-17T23:59:59Z |
A v1 name is only translated on endpoints that have the v2 equivalent — sending
?bookmaker= to an endpoint with no bookmaker filter is still a 400. If you
send both spellings, the v2 one wins.
Three more v1 parameters are accepted and do nothing, rather than erroring:
tz— v2 timestamps are always UTC, by contract.full— the v2 list endpoints carry no spatial fields to opt into.ordering— supported on/api/v2/transfers/; elsewhere the order is fixed.
Everything else is still rejected. ?leage=17 is a 400, because nobody meant
to send it.
Rate limits#
Two separate limits apply, and they are checked independently.
Per-account daily quota. From 17 August 2026 the free football API
allows 7,500 requests per day, counted per account and reset at midnight
UTC. Going over returns 429 with "code": "taster_exhausted" and a
Retry-After header holding the seconds until reset. Every response tells you
where you stand — see Rate limit headers below.
Football Unlimited removes the daily quota entirely and is also what unlocks per-bookmaker prices; without it the odds endpoints return the consensus price and how many bookmakers formed it. The other sports keep their own smaller daily allowance.
Per-IP burst limit. Independently of the above, the cached read endpoints
— /api/v2/events/ and /live/, /leagues/, /teams/, /tournaments/,
/referees/{id}/, /venues/{id}/, /predictions/, and the legacy /api/events/
and /api/live/ — are capped at 25 requests/second (burst 110) per client
address. Going over returns 429 with "code": "rate_limited" and
Retry-After: 1. A paid plan does not lift this one — it exists to stop a
single address from crowding everyone else out. If you need sustained
throughput, spread requests out or cache responses on your side; those
endpoints are already cheap to poll (see below).
The two 429s are told apart by code, and they want opposite reactions:
code |
Means | What to do |
|---|---|---|
rate_limited |
Per-second burst limit for your address | Retry after Retry-After (1 second) |
taster_exhausted |
Your account's daily quota is gone | Retry after Retry-After (seconds to midnight UTC) — sooner is pointless |
WebSocket connections are not rate-limited but each socket may hold at most 10 concurrent subscriptions.
Caching#
- Hot football endpoints (
/api/v2/events/,/leagues/,/teams/,/tournaments/) are edge-cached for ~5 seconds. TheX-Cache-Statusresponse header tells you whether you gotHIT,MISSorSTALE. - Live-score endpoints are server-cached 10–30 s; reference data (players, rankings, tournaments) about 5 minutes.
- Odds do not behave like the 5-second endpoints: they are re-read on a
schedule that runs from tens of minutes near kickoff to hours pre-match, so
polling them on a fast clock returns the same rows. Use
updated_afterto fetch only what we have re-read since your last call — see Refresh cadence. - Images are cached aggressively (up to 30 days) — see Image API.
Poll live lists no more often than every 10 s; for true real-time data use the WebSockets instead of polling.
Rate limit headers#
Responses carry the two RateLimit fields from
draft-ietf-httpapi-ratelimit-headers,
the IETF replacement for the X-RateLimit-Limit / -Remaining / -Reset trio.
We do not send the legacy X- headers.
RateLimit-Policy: "football";q=7500;w=86400
RateLimit: "football";r=7213;t=52800
RateLimit-Policy— the fixed shape of the policy.qis the quota,wthe window in seconds. The quoted name is the scope:football, or the sport (tennis,csgo,darts,hockey,basketball,horseracing).RateLimit— where you stand right now.ris what remains,tthe seconds until it resets.
Windows are daily and reset at midnight UTC, not local time. Both fields are HTTP Structured Fields — parse them with a structured-fields library rather than a regex.
Accounts on a paid plan have no quota, so they receive neither field. Their
absence means unlimited, not zero — do not treat a missing RateLimit as a
reason to back off.
Exceed the daily allowance and you get 429 with Retry-After in seconds,
alongside the same two fields with r=0.
Fair use#
the free api is provided under a fair use policy. usage that materially exceeds typical individual or small-project consumption, or that degrades service for other users, may be subject to throttling, rate limiting, or account-level restrictions. we may contact accounts with exceptional usage to discuss appropriate plans.