API v2 Stable
Streamlined event endpoints. Smaller payloads, structured types, predictable shapes.
Why v2?
v1 grew organically into a single fat detail endpoint. v2 fixes the friction points integrators reported.
One endpoint per concern
Core info, stats, incidents, odds, lineups, metadata are split into focused sub-resources. Fetch only what you need.
Predictable types
Ratio stats are structured: {"value": 2, "total": 14, "pct": 14} instead of the string "2/14 (14)". No more client-side parsing.
Booleans are booleans
v1 fields like is_neutral_ground could be true, false, or null β three states for two outcomes. v2 collapses to two.
Decoded enums
Weather codes are decoded for you: each detail response carries a description alongside the numeric code.
Stable IDs
Internal primary keys everywhere β including incidents (player_id, player_in_id, player_out_id). No juggling between two ID spaces.
Enriched stats
Per-side stats merge ball-tracking metrics (attack, ball_safe, dangerous_attack) with traditional stats and add computed pass_accuracy_pct.
Better metadata
New fields you didn't have before: derby, neutral-ground, travel distance, pitch condition, weather, attendance, jerseys, pre-match facts.
Live-only fields cleaned up
Finished matches no longer leak in-progress flags. is_live is always false on ended events; the added_time: 999 sentinel is gone.
Authentication
Every request needs an API token. Send it in the Authorization header. Get a free token by registering on the BSD landing page.
Authorization: Token YOUR_API_KEY
Base URL
All v2 endpoints live under one path. Examples below show full URLs; everything else in this page is relative to it.
Prefer Postman? Download the v2 Postman collection (35 requests across 7 folders) β set the token variable to your API key and you're set. Or import the OpenAPI schema directly.
https://sports.bzzoiro.com/api/v2/
Live WebSocket Add-on Β· $3/mo
Real-time push stream for matches in progress. Full protocol, frame schemas, pricing and activation live on the dedicated page.
List events
Paginated list of events. Each item has the same shape as the detail endpoint.
Query parameters
| Name | Type | Description |
|---|---|---|
| league_id | int | Filter by league |
| status | str | notstarted, inprogress, finished, β¦ |
| date_from | str | YYYY-MM-DD or ISO (UTC) |
| date_to | str | YYYY-MM-DD or ISO (UTC) |
| limit | int | Page size, default 50, max 200 |
| offset | int | Page offset |
curl -H "Authorization: Token $TOKEN" \ "https://sports.bzzoiro.com/api/v2/events/?league_id=49&status=finished&limit=5"
{
"count": 92,
"next": "β¦?limit=5&offset=5",
"previous": null,
"results": [ /* event objects */ ]
}
Live window
Lightweight list of events in their live window — from event_date − 5 min through ~10 min after final whistle. Built for high-frequency polling by livetracker dashboards. Cached in Redis with TTL 30 s; do not poll faster than that.
Query parameters
| Name | Type | Description |
|---|---|---|
| league_id | int | Filter by league. |
| season_id | int | Filter by season. |
| team_id | int | Matches where the team played home or away. |
Other params (status, date_from, date_to, limit, offset) are ignored — the window is fixed and the response is never paginated.
Response shape
No next/previous and no time-window object — the result is always the full live set (typically 0–80 events worldwide). Each item carries a last_updated timestamp so clients can short-circuit polls when the row hasn't changed.
Item schema
Item is intentionally narrow. Heavy fields (weather, pitch_condition, attendance, is_local_derby, is_neutral_ground, travel_distance_km, referee_id, venue_id, coaches, round_number, penalty_shootout) are NOT included — fetch them from /api/v2/events/{id}/ when needed.
status:notstarted|inprogress|penalties|finishedperiod:1st_half|halftime|2nd_half|extra_time|FT|null—nullwhen status isnotstartedorpenaltiescurrent_minute: int ornull(non-null only forinprogressandpenalties)home_score_ht/away_score_ht: int ornulllive_websocket:truewhen the event is wired to/ws/live/last_updated: ISO-8601 UTC withZsuffix — the row'supdated_atevent_date: ISO-8601 UTC with literalZsuffix
curl -H "Authorization: Token $TOKEN" \ "https://sports.bzzoiro.com/api/v2/events/live/?league_id=49"
{
"count": 5,
"events": [
{
"id": 204863,
"league_id": 49,
"league_name": "J1 League",
"home_team_id": 1943,
"home_team": "Shimizu S-Pulse",
"away_team_id": 1938,
"away_team": "Cerezo Osaka",
"event_date": "2026-05-06T04:00:00Z",
"status": "penalties",
"period": null,
"current_minute": 8,
"home_score": 1,
"away_score": 1,
"home_score_ht": 0,
"away_score_ht": 1,
"live_websocket": true,
"last_updated": "2026-05-06T04:32:11Z"
}
]
}
Event detail
Lightweight core fields for one event. Foreign keys are exposed as IDs only.
Use this endpoint to power match cards and headers. Sub-resources (stats, incidents, lineups β¦) load on demand.
home_team and away_team ship alongside the IDs as a convenience for the most common UI use case β display the match name without an extra request.
Tip If venue_id is null on the event, fall back to the home team's venue.
{
"id": 204851,
"league_id": 49,
"home_team_id": 1945,
"home_team": "Kashiwa Reysol",
"away_team_id": 1936,
"away_team": "FC Tokyo",
"home_coach_id": 1302,
"away_coach_id": 1293,
"referee_id": 215,
"venue_id": 1114,
"event_date": "2026-04-29T07:00:00+00:00",
"status": "finished",
"round_number": 13,
"period": "FT",
"current_minute": 90,
"home_score": 1,
"away_score": 3,
"home_score_ht": 0,
"away_score_ht": 1,
"penalty_shootout": null,
"is_local_derby": false,
"is_neutral_ground": false,
"travel_distance_km": 45,
"weather": {
"code": 3,
"description": "rain",
"wind_speed": null,
"temperature_c": null
},
"pitch_condition": 1,
"attendance": null,
"live_websocket": true
}
boolean β true when this match is wired into the real-time push stream. Subscribe via the Live WebSocket add-on to receive event and livedata frames while the match is in progress.
Stats
All quantitative metrics: per-team stats, shotmap, momentum, average pitch positions, per-minute xG.
Per-team stats merge traditional counters, ball-tracking metrics, and computed values like pass_accuracy_pct.
Ratio stats use the structured shape; pure counters and percentages stay as plain integers. See stat shapes.
Top-level keys
| stats.home / stats.away | obj | Per-side merged metrics |
| shotmap | list | Per-shot xG, coords, body part, situation |
| momentum | list | Per-minute pressure index |
| average_positions | obj | Average pitch positions per player |
| xg_per_minute | list | xG buckets and cumulative totals |
{
"event_id": 204851,
"stats": {
"home": {
"total_shots": 11,
"ball_possession": 56,
"crosses": { "value": 2, "total": 14, "pct": 14 },
"dribbles": { "value": 6, "total": 11, "pct": 55 },
"long_balls":{ "value": 25, "total": 52, "pct": 48 },
"attack": 89,
"ball_safe": 68,
"dangerous_attack": 59,
"pass_accuracy_pct": 81.5,
"xg": { "actual": 1.23 }
},
"away": { /* β¦ */ }
},
"shotmap": [ /* per-shot details */ ],
"momentum": [ /* {m, v} */ ],
"average_positions": { /* β¦ */ },
"xg_per_minute": [ /* {m, xg_home, xg_away, cum_*} */ ]
}
Incidents
Chronological match events: goals, cards, substitutions, period markers, VAR decisions, injury time.
Every *_id field is a BSD primary key β use it directly with other endpoints. On finished events is_live is always false.
Common types
| goal | player, sequence with coords | |
| card | card_type: yellow / red / yellowRed | |
| substitution | player_in / player_out | |
| period | 1st half, HT, 2nd half, FT | |
| injuryTime | Stoppage announcement | |
| varDecision | Decision class + confirmed flag |
{
"event_id": 204851,
"incidents": [
{
"type": "goal",
"minute": 89,
"player": "R. Sato",
"player_id": 17236,
"is_home": false,
"sequence": [ /* coord steps */ ]
},
{
"type": "substitution",
"minute": 73,
"player_in": "T. Nakagawa",
"player_in_id": 43672,
"player_out": "K. Sato",
"player_out_id": 43671
},
{
"type": "card",
"minute": 13,
"player": "D. Sugioka",
"player_id": 33125,
"card_type": "yellow"
}
]
}
Odds
Consensus across bookmakers. Decimal format only.
A value of null means we don't have a consensus for that market β usually because too few bookmakers are quoting it.
Markets
| 1X2 | home_win / draw / away_win | |
| Over / Under | {1.5, 2.5, 3.5} goals | |
| BTTS | both teams to score yes/no |
{
"event_id": 204851,
"odds": {
"home_win": 2.58,
"draw": 3.43,
"away_win": 2.85,
"over_15_goals": 1.33,
"over_25_goals": 2.04,
"over_35_goals": 3.56,
"under_15_goals": 3.55,
"under_25_goals": 1.854,
"under_35_goals": 1.33,
"btts_yes": 1.793,
"btts_no": 2.08
}
}
Metadata
Non-statistical context: jerseys, pre-match facts, AI-generated preview text.
Pre-match facts are short narrative sentences (e.g. "Bayern haven't lost in 19 games"). The AI preview is a Twitter-style hype writeup generated for fixtures kicking off in the next 24 hours.
{
"event_id": 204851,
"jerseys": {
"home": { "player": { /* β¦ */ }, "GK": { /* β¦ */ } },
"away": { /* β¦ */ }
},
"funfacts": [
{
"type_id": 2,
"sentence": "When playing at home, Kashiwa have not lost to Tokyo in their last 3 encounters."
}
],
"ai_preview": {
"text": "β¦markdown textβ¦",
"generated_at": "2026-04-28T18:30:00Z"
}
}
Lineups
Starting XI plus substitutes per side, plus the unavailable players list.
Player objects include rating, jersey number, sub-in/out minute, and substitution links. Unavailable players list covers injured, suspended and doubtful players for the matchday.
{
"event_id": 204851,
"lineups": {
"home": {
"formation": "4-3-3",
"players": [ /* β¦ */ ],
"substitutes": [ /* β¦ */ ]
},
"away": { /* β¦ */ }
},
"unavailable_players": {
"home": [
{ "name": "β¦", "status": "injured", "reason": "β¦" }
],
"away": []
}
}
Player stats
Per-player statistics for every player who appeared in this match (typically 22+ records). Same record shape as /players/{id}/stats/.
Tagged under the players domain because the records belong to players, not the event itself. Sorted by team_id then descending minutes played.
To get one specific player's stats across many events use /players/{id}/stats/ instead.
{
"event_id": 204851,
"count": 28,
"player_stats": [
{
"id": 1839201,
"player_id": 5821,
"event_id": 204851,
"team_id": 267,
"minutes_played": 90,
"rating": 7.4,
"goals": 1,
"goal_assist": 0,
"expected_goals": 0.42,
"expected_assists": 0.11,
"total_shots": 3,
"shots_on_target": 2,
"total_pass": 52,
"accurate_pass": 47,
"key_pass": 2,
"total_tackle": 1,
"interception": 0,
"yellow_card": 0,
"red_card": 0,
"saves": null
}
]
}
Leagues
A football competition. Lightweight detail plus seasons and standings sub-resources.
List leagues
Paginated list of leagues. Filter by country and women/men.
Query parameters
| Name | Type | Description |
|---|---|---|
| country | str | Filter by country (case-insensitive) |
| is_women | bool | Default: both |
| is_active | bool | Default: only active |
| limit | int | Page size, default 50, max 200 |
| offset | int | Page offset |
{
"count": 4,
"results": [
{
"id": 17,
"name": "Premier League",
"country": "England",
"is_women": false,
"is_active": true,
"current_season": {
"id": 243,
"name": "Premier League 25/26",
"year": 2025,
"is_current": true
}
}
]
}
League detail
A single league. current_season is embedded for convenience β it's the most common follow-up lookup.
For the season ID alone, hit /leagues/{id}/season/. For the full archive, use /leagues/{id}/seasons/.
{
"id": 17,
"name": "Premier League",
"country": "England",
"is_women": false,
"is_active": true,
"current_season": {
"id": 243,
"name": "Premier League 25/26",
"year": 2025,
"start_date": "2025-07-01",
"end_date": "2026-06-30",
"is_current": true
}
}
Current season
The active season for a league. Returns null when none is flagged current (e.g. between cycles).
Useful when you only need the season ID (e.g. to query /events/?β¦ with date filters in that season).
{
"league_id": 17,
"season": {
"id": 243,
"name": "Premier League 25/26",
"year": 2025,
"start_date": "2025-07-01",
"end_date": "2026-06-30",
"is_current": true
}
}
All seasons
Every season we have on file for this league, newest first.
Use a season's id to scope a standings request: /leagues/{id}/standings/?season_id=ID.
{
"league_id": 17,
"count": 15,
"seasons": [
{
"id": 243,
"name": "Premier League 25/26",
"year": 2025,
"is_current": true
},
{ /* β¦ older seasons */ }
]
}
Standings
League table for one season (defaults to current). Cup competitions return a groups map; league competitions return a flat standings array.
Query parameters
| season_id | int | Season ID. Defaults to the league's current season. |
Row fields
| position | int | 1-indexed rank |
| team_id / team_name | Team identifier and display name | |
| played, won, drawn, lost | int | Match counts |
| gf, ga, gd, pts | int | Goals for / against / difference / points |
| xgf, xga, xgd | float | Expected-goals equivalents (only counts matches with shotmap data) |
| xg_games | int | Number of matches contributing to the xG totals |
| form | str | Last 5 results, e.g. "WWLDW" |
| live | bool | Has an in-progress match this matchday |
{
"league_id": 17,
"season": { "id": 243, "name": "Premier League 25/26" },
"grouped": false,
"standings": [
{
"position": 1,
"team_id": 267,
"team_name": "β¦",
"played": 29,
"won": 25,
"drawn": 1,
"lost": 3,
"gf": 79,
"ga": 21,
"gd": 58,
"pts": 76,
"xgf": 25.5,
"xga": 5.4,
"xgd": 20.1,
"xg_games": 9,
"form": "WWWWW",
"live": false
}
]
}
League venues
Venues used by this league across one or more seasons. Each row is a tournament assignment with the venue object embedded.
Query parameters
| season_id | int | Restrict to assignments in this season. |
| limit | int | Page size, default 50, max 200 |
| offset | int | Page offset |
For knockout-round filters (semifinal venue, final venue, host country) use the season-scoped variant below.
{
"count": 16,
"results": [
{
"id": 421,
"league_id": 17,
"season_id": 243,
"host_country_code": "",
"hosts_opening": false,
"hosts_final": false,
"hosts_third_place": false,
"rounds": [],
"match_count": 19,
"fifa_role": "",
"venue": { /* full venue object */ }
}
]
}
League / season venues
Venues for a specific league + season pair. Adds knockout-round and host-country filters β the canonical replacement for the legacy World Cup venues endpoint.
Query parameters
| host_country_code | str | ISO 3166-1 alpha-2 host country |
| hosts_opening | bool | Venues hosting the opening match |
| hosts_final | bool | Venues hosting the final |
| hosts_third_place | bool | Venues hosting the third-place match |
| round | str | One of R32, R16, QF, SF, 3RD, F |
| limit | int | Page size, default 50, max 200 |
| offset | int | Page offset |
Same shape as /leagues/{id}/venues/.
curl -H "Authorization: Token $TOKEN" \ "https://sports.bzzoiro.com/api/v2/leagues/16/seasons/82/venues/?round=F"
Teams
A football club or national team. Lightweight detail plus squad and fixtures sub-resources.
List teams
Paginated teams list. Filter by country, league, season, name, or competition status.
Query parameters
| country_code | str | ISO 3166-1 alpha-2. Unknown codes return an empty list. |
| league_id | int | Teams with at least one event in this league |
| season_id | int | Teams that participated in this season |
| in_competition | bool | True = teams with upcoming matches in league_id. False = teams that played there but no longer do. Requires league_id. |
| is_women | bool | Filter by women / men competitions (needs league_id or season_id) |
| name | str | Partial, case-insensitive name match |
| limit | int | Default 50, max 200 |
| offset | int | Page offset |
{
"count": 20,
"results": [
{
"id": 267,
"name": "Manchester City",
"short_name": "Man City",
"country": "England",
"venue_id": 42
}
]
}
Team detail
A single team. Same shape as a list row.
Squad
Active players currently linked to this team via current_team.
For full per-player detail (market value, foot, contract, etc.) hit /players/{id}/. The squad endpoint returns a brief shape for list rendering.
{
"team_id": 267,
"count": 28,
"players": [
{
"id": 5821,
"name": "β¦",
"short_name": "β¦",
"position": "M",
"jersey_number": 17,
"nationality": "Belgium",
"date_of_birth": "1991-06-28"
}
]
}
Fixtures
Matches for this team. Defaults to a window of now β 3h to now + 7d when no date filters are provided. Each item has the same shape as /api/v2/events/{id}/.
Query parameters
| date_from | str | ISO 8601 UTC, e.g. 2026-04-30T00:00:00Z |
| date_to | str | ISO 8601 UTC |
| league_id | int | Restrict to this league |
| status | str | notstarted, inprogress, finished, postponed, cancelled |
| limit / offset | int | Standard pagination |
Players
Identity, per-match stats, transfers, career aggregates, and national-team data.
List players
Paginated players list. Filter by current club, national team, nationality, position, or name.
Query parameters
| team_id | int | Filter by current club |
| national_team_id | int | Filter by national-team |
| nationality_code | str | ISO 3166-1 alpha-2 |
| position | str | Generic (G/D/M/F) or specific position code |
| name | str | Matches name or short_name (case-insensitive) |
| limit / offset | int | Standard pagination |
{
"id": 5821,
"name": "β¦",
"short_name": "β¦",
"position": "M",
"specific_position": "AM",
"jersey_number": 17,
"date_of_birth": "1991-06-28",
"height_cm": 181,
"weight_kg": 70,
"preferred_foot": "right",
"nationality": "Belgium",
"current_team_id": 267,
"national_team_id": 5142,
"market_value_eur": 35000000,
"contract_until": "2027-06-30",
"availability": "available"
}
Player detail
A single player. Same shape as a list item.
Per-match stats
Per-match statistics for one player, newest first. Same record shape as /events/{id}/player-stats/.
Query parameters
| season_id | int | Filter by season |
| team_id | int | Stats logged with this team (handles transfers) |
| league_id | int | Filter by league |
| date_from / date_to | str | ISO 8601 UTC |
| limit / offset | int | Standard pagination |
Filtering by event_id is rejected β use /events/{id}/player-stats/ for that case.
// Goals scored this season GET /api/v2/players/5821/stats/?season_id=243&limit=200 // Then sum results[].goals client-side
Transfers
Transfer history for the player, newest first.
fee_eur is the integer EUR fee when known. fee_description covers the long-tail cases (free transfer, loan, undisclosed). Both can coexist on the same record.
{
"id": 8121,
"transfer_date": "2015-08-30",
"from_team_id": 2685,
"from_team_name": "β¦",
"to_team_id": 267,
"to_team_name": "β¦",
"fee_eur": 76000000,
"fee_description": "",
"transfer_type": "transfer"
}
Career
Career totals aggregated by season β matches, goals, assists, minutes, average rating. Newest season first.
Each row covers a unique (season, league, team) tuple, so a player loaned out mid-season produces two rows. avg_rating is null when none of the matches in a row have a rating recorded.
{
"player_id": 5821,
"seasons": [
{
"season_id": 243,
"league_id": 17,
"team_id": 267,
"matches": 29,
"minutes": 2480,
"goals": 8,
"assists": 11,
"avg_rating": 7.42
}
]
}
National team
National-team identity plus aggregate caps and goals derived from event statistics where the player appeared for that team.
When the player has no national team set, national_team_id is null and counters are zero. last_appearance is the date of the most recent stats record for the national team.
{
"player_id": 5821,
"national_team_id": 5142,
"caps": 102,
"goals": 26,
"last_appearance": "2026-03-26T19:45:00Z"
}
Managers
Coaches with computed match aggregates plus tenure history.
List managers
Paginated managers list with computed match aggregates baked into each row.
Query parameters
| team_id | int | Active tenure at this club |
| league_id | int | Coached at least one match in this league |
| nationality_code | str | ISO 3166-1 alpha-2 |
| min_matches | int | Minimum matches managed (default 0) |
| tactical_profile | str | attacking, defensive, balanced |
| team_style | str | possession, counter, pressing, defensive, wide, fan |
| name | str | Matches name or short_name |
| limit / offset | int | Standard pagination |
{
"id": 214,
"name": "β¦",
"short_name": "β¦",
"country": "Spain",
"tactical_profile": "attacking",
"preferred_formation": "4-3-3",
"current_team_id": 267,
"matches_total": 412,
"wins": 281,
"draws": 75,
"losses": 56,
"win_pct": 68.2,
"avg_goals_scored": 2.4,
"avg_goals_conceded": 0.9,
"avg_possession": 63.1,
"clean_sheet_pct": 42.5,
"btts_pct": 48.3,
"over_25_pct": 61.4,
"stats_updated_at": "2026-04-29T05:00:00Z"
}
Manager detail
A single manager. Same shape as a list item.
Career
Tenure history at each club with date range and per-tenure record (matches/wins/draws/losses/win%).
Tenures with date_to: null are still active. Per-tenure aggregates only count matches in the finished status, so they line up with the manager-level totals on the detail endpoint.
{
"manager_id": 214,
"count": 3,
"tenures": [
{
"team_id": 267,
"team_name": "β¦",
"date_from": "2023-07-01",
"date_to": null,
"matches": 98,
"wins": 71,
"draws": 14,
"losses": 13,
"win_pct": 72.4
}
]
}
Matches
Matches managed by this manager. Defaults to a now β 3h to now + 7d window when no date filters are provided.
Query parameters
| date_from / date_to | str | ISO 8601 UTC |
| league_id | int | Restrict to this league |
| team_id | int | Match where this manager was on the bench for this team (handles multiple tenures) |
| status | str | notstarted, inprogress, finished, postponed, cancelled |
| limit / offset | int | Standard pagination |
Odds
Multi-bookmaker odds (~15 books, including the synthetic oddssafari-consensus). The standalone /api/v2/events/{id}/odds/ consensus endpoint is unchanged. This section adds the rest of the odds universe: a global list, detail, best-upcoming, side-by-side comparison, Polymarket, and the bookmakers index.
List odds
Paginated list of latest odds β one row per (event, bookmaker, market, outcome) tuple at its most recent observation. Cached 3 min.
Query parameters
| Name | Type | Description |
|---|---|---|
| event_id / league_id / season_id / team_id | int | Standard event-scope filters. |
| bookmaker_slug | str | Slug only (e.g. pinnacle). Display names rejected with 400. |
| market | enum | 1x2, btts, over_under_15, over_under_25, over_under_35, double_chance, draw_no_bet. |
| outcome | enum | HOME, DRAW, AWAY, over, under, yes, no, 1X, 12, X2. |
| min_decimal_odds / max_decimal_odds | float | Bounds on price. |
| movement | enum | SHORTENING | DRIFTING. |
| is_max_quote | bool | Source flagged the row as best price across books. |
| updated_after | str | ISO-8601 UTC. Only odds with observed_at ≥ this. |
| limit / offset | int | Standard pagination. |
Incoherent market+outcome combinations return 400: e.g. ?market=btts&outcome=HOME → "Outcome 'HOME' is not valid for market 'btts'. Valid outcomes: yes, no.".
{
"id": 51713,
"event_id": 997,
"market": "1x2",
"outcome": "HOME",
"outcome_name": "Osasuna",
"bookmaker_slug": "pinnacle",
"bookmaker_name": "Pinnacle",
"decimal_odds": 2.11,
"previous_decimal_odds": 2.18,
"implied_probability": 0.4739,
"movement": "SHORTENING",
"is_max_quote": false,
"updated_at": "2026-04-12T09:00:00Z"
}
Odds detail
One odds row by id. Same shape as a list item. Unknown id → 404. Unlike v1, there is no event-id fallback.
Best upcoming
Per upcoming event, the highest decimal odds for each outcome of the requested market across all bookmakers. Defaults: market=1x2, date_from=now, date_to=now+7 days. Cached 5 min.
Query parameters
| Name | Type | Description |
|---|---|---|
| market | enum | Default 1x2. |
| league_id / season_id / team_id | int | Standard scoping. |
| date_from / date_to | str | ISO-8601 UTC. Replaces v1's days=N. |
| limit / offset | int | Standard pagination (default 50, max 200). |
{
"event_id": 997,
"event_date": "2026-04-12T12:00:00Z",
"league_id": 8, "league_name": "La Liga",
"home_team": "Osasuna", "away_team": "Real Betis",
"market": "1x2",
"best_odds": [
{ "outcome": "HOME", "outcome_name": "Osasuna", "decimal_odds": 2.20, "bookmaker_slug": "interwetten", "bookmaker_name": "Interwetten" },
{ "outcome": "DRAW", "decimal_odds": 3.60, "bookmaker_slug": "unibet" },
{ "outcome": "AWAY", "decimal_odds": 3.62, "bookmaker_slug": "pinnacle" }
]
}
Comparison (by event)
All bookmakers side-by-side for one event. Outcome keys are codes (HOME/DRAW/AWAY/over/under/...), not team names β stable parsing even when teams are renamed. Bookmaker keys are slugs. outcome_name still appears inside each outcome for legibility. Cached 3 min.
Event found but with no odds yet → 200 with {"markets": {}}. Unknown event → 404.
Polymarket (by event)
Polymarket prediction-market prices for one event. Prices are implied probabilities (0–1); decimal odds = 1/price. Distinct shape from traditional odds. Cached 1 min.
Event with no Polymarket coverage → 404 with body {"detail": "No Polymarket markets available for this event."}.
Bookmakers
All active bookmakers as {slug, name}. No detail, no pagination. Cached 1 hour.
oddssafari-consensus is a synthetic average across the rest, not a real bookmaker.
Predictions
CatBoost ML predictions, restructured: response is grouped by market (match_result, expected_goals, over_under, btts, score) instead of v1's flat shape. The embedded event is a slim summary, not the full event detail.
List predictions
Paginated list of predictions for football matches. Cached in Redis for 120 s.
Query parameters
| Name | Type | Description |
|---|---|---|
| status | enum | upcoming (default) | past | all. Ignored when date_from/date_to is set — the explicit window wins. |
| league_id | int | Filter by league. |
| season_id | int | Filter by season. |
| team_id | int | Predictions where the team played home or away. |
| date_from / date_to | str | ISO-8601 UTC, or YYYY-MM-DD. |
| min_confidence | float | 0–1. Lower bound on model confidence. |
| recommended | bool | true = at least one *_recommend is true. false = all false. Omit to skip filter. |
| limit / offset | int | Standard pagination (default 50, max 200). |
{
"id": 87,
"created_at": "2026-02-09T18:00:00Z",
"event": {
"id": 580,
"event_date": "2026-02-10T17:30:00Z",
"status": "notstarted",
"home_team_id": 1,
"home_team": "Liverpool",
"away_team_id": 2,
"away_team": "Arsenal",
"league_id": 17,
"league_name": "Premier League"
},
"markets": {
"match_result": { "prob_home": 52.3, "prob_draw": 24.1, "prob_away": 23.6, "predicted": "H" },
"expected_goals": { "home": 1.82, "away": 1.14 },
"over_under": { "prob_over_15": 87.4, "prob_over_25": 61.2, "prob_over_35": 28.7 },
"btts": { "prob_yes": 58.9 },
"score": { "most_likely": "2-1" }
},
"recommendations": {
"favorite": "H", "favorite_prob": 52.3,
"bet_favorite": true, "over_15": false, "over_25": true,
"over_35": false, "btts": true, "winner": true
},
"model": { "confidence": 0.72, "version": "v5.0" }
}
Prediction detail
Same shape as a list item, retrieved by prediction ID.
By event
Direct lookup by event_id. Same response shape as /predictions/{id}/. Returns 404 with body { "detail": "No prediction available for this event." } when the event has no prediction.
Referees
New in v2 (the v1 endpoint was a single un-paginated list with no detail). Adds detail, a matches sub-resource, pagination, and ISO-2 country filtering.
List referees
Paginated referees list with computed aggregates (cards, goals, fouls per match). Ordered by matches officiated, descending.
Query parameters
| league_id | int | At least one match officiated in this league. When set, aggregates are scoped to this league. |
| name | str | Partial, case-insensitive name match |
| country_code | str | ISO 3166-1 alpha-2 |
| min_matches | int | Minimum matches officiated (default 0) |
| limit / offset | int | Standard pagination |
{
"id": 1206,
"name": "β¦",
"country": "Peru",
"nationality_a3": "per",
"birthdate": "1981-05-04",
"matches": 142,
"total_yellow_cards": 581,
"total_red_cards": 21,
"avg_yellow_per_match": 4.09,
"avg_red_per_match": 0.15,
"avg_goals_per_match": 2.61,
"avg_fouls_per_match": 22.3,
"career_games": null,
"career_yellow_cards": null,
"career_red_cards": null
}
Referee detail
A single referee. Same shape as a list item, with aggregates over all played matches.
Matches
Matches officiated by this referee. Defaults to a now β 3h to now + 7d window.
Query parameters
| date_from / date_to | str | ISO 8601 UTC |
| league_id | int | Restrict to this league |
| season_id | int | Restrict to this season |
| status | str | notstarted, inprogress, finished, postponed, cancelled |
| limit / offset | int | Standard pagination |
Venues
Stadiums with location, capacity, pitch dimensions, and per-tournament assignments.
List venues
Paginated venues list. Filter by country, city, name, capacity, or home team.
Query parameters
| country_code | str | ISO 3166-1 alpha-2 |
| city | str | Partial, case-insensitive city match |
| name | str | Partial, case-insensitive venue name |
| min_capacity | int | Minimum seating capacity |
| team_id | int | Home stadium of this team |
| limit / offset | int | Standard pagination |
{
"id": 42,
"name": "Etihad Stadium",
"city": "Manchester",
"country": "England",
"country_code": "GB",
"capacity": 53400,
"latitude": 53.4831,
"longitude": -2.2004,
"pitch_length_m": 105,
"pitch_width_m": 68,
"built_year": 2002,
"home_team_id": 267
}
Venue detail
A single venue. Same shape as a list item plus an embedded competition_assignments array β venues typically have fewer than five assignments so they ship inline.
Each assignment links the venue to a (league, season) pair with extra context about its tournament role: which knockout rounds it hosts, opening/final/third-place flags, host country, and FIFA role tag.
{
"id": 812,
"league_id": 16,
"season_id": 82,
"host_country_code": "US",
"hosts_opening": false,
"hosts_final": true,
"hosts_third_place": false,
"rounds": ["SF", "F"],
"match_count": 8,
"fifa_role": "final"
}
Competitions
Paginated list of every tournament/season this venue is used in. Same assignment shape as the inline array on the detail endpoint.
TV Channels
Catalogue of licensed TV channels per country, plus their broadcast schedules. Drops the always-true is_official and the upstream channel_id from v1 responses.
List channels
Paginated list. Cached 1 hour.
Query parameters
| Name | Type | Description |
|---|---|---|
| country_code | str | ISO 3166-1 alpha-2 (e.g. PT, ES). 400 if malformed. |
| name | str | Case-insensitive partial match. |
| limit / offset | int | Standard pagination. |
{
"id": 142,
"name": "Sport TV1",
"country_code": "PT",
"link": ""
}
Channel detail
Same shape as a list item. Unknown id → 404.
Broadcasts (by channel)
Paginated broadcasts emitted by this channel. Default temporal window: now − 3 h to now + 7 d.
Query parameters
| Name | Type | Description |
|---|---|---|
| date_from / date_to | str | ISO-8601 UTC. Override the default window. |
| league_id / season_id | int | Standard scoping. |
| limit / offset | int | Standard pagination. |
Broadcasts
Event-to-channel broadcast mappings. v2 breaking change: channel_id filter and response field both refer to the internal TVChannel PK, not the upstream id used by v1.
List broadcasts
Paginated list. Cached 1 hour.
Query parameters
| Name | Type | Description |
|---|---|---|
| event_id / league_id / season_id / team_id | int | Standard event-scope filters. |
| country_code | str | ISO 3166-1 alpha-2. |
| channel_id | int | Internal TVChannel PK (not the upstream id used by v1). |
| date_from / date_to | str | ISO-8601 UTC. |
| limit / offset | int | Standard pagination. |
{
"id": 9821,
"event_id": 204851,
"home_team_id": 1945,
"home_team": "Kashiwa Reysol",
"away_team_id": 1936,
"away_team": "FC Tokyo",
"league_id": 49,
"league_name": "J1 League",
"event_date": "2026-04-29T07:00:00Z",
"country_code": "PT",
"channel_id": 142,
"channel_name": "Sport TV1",
"channel_link": "",
"scheduled_start_time": "2026-04-29T07:00:00Z"
}
Broadcast detail
Same shape as a list item. Unknown id → 404.
By event
Paginated broadcasts for one event across countries and channels. Optional country_code filter narrows to a single market.
No date_from/date_to — the event already bounds the window. Event with no broadcasts → 200 with results: []. Unknown event → 404.
Social
Tweets and video highlights linked to teams, events, players, and managers. v2 drops vendor source, normalizes media_urls into a structured media array, groups account_* under account, and groups the four entity arrays under linked. News items (v1 type=news) are intentionally excluded from v2.
Weather codes
The weather.code field uses this dictionary. weather.description is the same mapping pre-decoded for clients that only need the label.
| Code | Description |
|---|---|
| 0 | unknown / closed venue |
| 1 | clear |
| 2 | cloudy |
| 3 | rain |
| 4 | snow |
| 5 | extreme |
Stat shapes
Three shapes appear inside stats.home / stats.away:
Counter
Plain integer counter. Example: "total_shots": 11
Percentage
Plain integer 0β100. Example: "ball_possession": 56
Ratio
Object with successful count, attempts, and the rounded percentage.
Stats currently using the ratio shape:
crosses dribbles long_balls aerial_duels ground_duels final_third_phase
{
"value": 2, // successful
"total": 14, // attempts
"pct": 14 // rounded percentage
}
Migration from v1
Equivalence map and a recommended loading strategy.
Field mapping
| v1 | v2 | |
|---|---|---|
| GET /api/events/{id}/?full=true | Multiple calls β one per sub-resource | |
| league (object) | league_id | |
| home_team_obj (object) | home_team_id + home_team | |
| referee (object) | referee_id β /api/referees/?name=β¦ | |
| weather_code, wind_speed, β¦ | weather.{code,description,wind_speed,temperature_c} | |
| is_neutral_ground may be null | always boolean | |
| live_stats.home.crosses = "2/14 (14)" | stats.home.crosses = {value,total,pct} | |
| shotmap, momentum (on detail) | under /events/{id}/stats/ | |
| lineups (on detail) | /events/{id}/lineups/ | |
| incidents (on detail) | /events/{id}/incidents/ | |
| odds_home, odds_over_25, β¦ | /events/{id}/odds/ β odds.{home_win,over_25_goals,β¦} | |
| incident player_id = upstream id | incident player_id = BSD primary key | |
| /api/teams/?β¦ | /api/v2/teams/?country_code=&league_id=&in_competition= | |
| /api/players/?β¦ | /api/v2/players/?team_id=&nationality_code=&position= | |
| /api/managers/?β¦ | /api/v2/managers/?team_id=&tactical_profile=&team_style= | |
| /api/referees/ (un-paginated) | /api/v2/referees/ (paginated, with detail + matches sub-resources) | |
| /api/worldcup/venues/ | /api/v2/leagues/{wc_league_id}/seasons/{season_id}/venues/?round=β¦ | |
| /api/predictions/?upcoming=true | /api/v2/predictions/?status=upcoming | |
| /api/predictions/?upcoming=false | /api/v2/predictions/?status=past | |
| /api/predictions/?league=X | /api/v2/predictions/?league_id=X | |
| /api/predictions/?tz=β¦ | not supported β UTC (Z) only | |
| prob_home_win / prob_draw / prob_away_win | markets.match_result.{prob_home, prob_draw, prob_away} | |
| predicted_result | markets.match_result.predicted (enum H/D/A) | |
| expected_home_goals / expected_away_goals | markets.expected_goals.{home, away} | |
| prob_over_15 / prob_over_25 / prob_over_35 | markets.over_under.prob_over_{15,25,35} | |
| prob_btts_yes | markets.btts.prob_yes | |
| most_likely_score | markets.score.most_likely | |
| favorite (string) | recommendations.favorite (H/A/null) | |
| favorite_prob | recommendations.favorite_prob | |
| favorite_recommend (bool) | recommendations.bet_favorite (renamed to avoid collision with the favorite string sibling) | |
| over_15_recommend / over_25_recommend / over_35_recommend | recommendations.{over_15, over_25, over_35} | |
| btts_recommend | recommendations.btts | |
| winner_recommend | recommendations.winner | |
| confidence | model.confidence (always normalized to 0–1) | |
| model_version | model.version | |
| event (full object) | event (slim summary: id + teams + league + date + status) | |
| /api/odds/?event=X | /api/v2/odds/?event_id=X (paginated, no envelope) | |
| /api/odds/?event=X (envelope) | /api/v2/events/{id}/odds/comparison/ | |
| /api/odds/?bookmaker=Pinnacle | /api/v2/odds/?bookmaker_slug=pinnacle (slug only β display name returns 400) | |
| /api/odds/?league=X | /api/v2/odds/?league_id=X | |
| /api/odds/ default market=1x2 | /api/v2/odds/ default = all markets | |
| silent empty for incoherent market+outcome | 400 with explicit detail message | |
| /api/odds/{id}/ event-id fallback | /api/v2/odds/{id}/ β strict 404 if not an odds row | |
| /api/odds/best/?days=N | /api/v2/odds/best/?date_from=...&date_to=... (default 7 d) | |
| /api/odds/compare/?event=X | /api/v2/events/{X}/odds/comparison/ | |
| compare keys = team names | compare keys = outcome codes (HOME/DRAW/AWAY); outcome_name stays | |
| /api/odds/bookmakers/ | /api/v2/bookmakers/ | |
| /api/odds/polymarket/?event=X | /api/v2/events/{X}/polymarket/ β own shape, volume_usd/liquidity_usd not exposed (data not stored) | |
| /api/tv-channels/?country=X | /api/v2/tv-channels/?country_code=X (ISO-2) | |
| /api/tv-channels/?search=X | /api/v2/tv-channels/?name=X | |
tv-channel response: is_official | removed (always true, redundant) | |
tv-channel response: upstream channel_id | removed from public response | |
| /api/broadcasts/?event=X | /api/v2/broadcasts/?event_id=X | |
| /api/broadcasts/?country=X | /api/v2/broadcasts/?country_code=X (ISO-2) | |
| /api/broadcasts/?channel=X | /api/v2/broadcasts/?channel_id=X β internal TVChannel PK (v1 used upstream id) | |
| /api/broadcasts/?league=X | /api/v2/broadcasts/?league_id=X | |
broadcasts response: only home_team/away_team labels | adds home_team_id, away_team_id, league_id (id+label pattern) | |
| broadcasts: no pagination | standard limit/offset wrapper, plus a detail endpoint | |
| broadcasts for one event | /api/v2/events/{id}/broadcasts/ (sub-resource) | |
| broadcasts for one channel | /api/v2/tv-channels/{id}/broadcasts/ (sub-resource) | |
| /api/social/?team=X / event=X / player=X / manager=X | /api/v2/social/?team_id=X / event_id=X / player_id=X / manager_id=X | |
| /api/social/?since=X | /api/v2/social/?published_after=X (ISO-8601 UTC) | |
social response: source | removed (vendor name no longer exposed) | |
social response: media_urls | media normalized: [{type, url, width, height}] | |
social response: account_handle / account_name / account_verified | grouped under account.{handle, name, verified} | |
social response: teams / events / players / managers arrays | grouped under linked.{teams, events, players, managers} | |
social news items | excluded from v2 (only tweet + video) | |
| social embedded in /events/{id}/ or /teams/{id}/ detail | not embedded (was never embedded in v2; use the new /<entity>/{id}/social/ sub-resources) |
Recommended loading strategy
Don't fetch everything upfront. Load detail first, defer sub-resources to user actions:
| UI surface | Endpoint | |
|---|---|---|
| Match list view | /events/?β¦ | ~1 KB per row |
| Match summary card | /events/{id}/ | core fields |
| Stats tab | /events/{id}/stats/ | per-team + analytics |
| Timeline tab | /events/{id}/incidents/ | chronological events |
| Lineups tab | /events/{id}/lineups/ | XI + subs + injuries |
| Player ratings tab | /events/{id}/player-stats/ | per-player rating + box score |
| Team page | /teams/{id}/ + /teams/{id}/squad/ + /teams/{id}/fixtures/ | parallel fetch |
| Player page | /players/{id}/ + /players/{id}/career/ | defer transfers/national-team to a tab |
| Manager page | /managers/{id}/ + /managers/{id}/career/ | aggregates already on detail |
List social
Paginated feed sorted newest first. Cached 15 min.
Query parameters
tweet|video.{ "id": 7821, "type": "tweet", "url": "https://x.com/granadacf/status/...", "text": "LleΓ³ hat-trick! Vamos Granadaaaa", "title": "", "thumbnail": "https://pbs.twimg.com/media/...", "media": [ { "type": "image", "url": "https://...", "width": null, "height": null } ], "account": { "handle": "@granadacf", "name": "Granada CF", "verified": true }, "published_at": "2026-04-12T19:32:00Z", "linked": { "teams": [{ "id": 44, "name": "Granada CF" }], "events": [{ "id": 580, "home_team": "Granada", "away_team": "Sevilla", "event_date": "2026-04-12T17:30:00Z" }], "players": [], "managers": [] } }