Growing Discord community β€” direct access to the developer, live coverage & picks. Join now β†’

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.

1

One endpoint per concern

Core info, stats, incidents, odds, lineups, metadata are split into focused sub-resources. Fetch only what you need.

2

Predictable types

Ratio stats are structured: {"value": 2, "total": 14, "pct": 14} instead of the string "2/14 (14)". No more client-side parsing.

3

Booleans are booleans

v1 fields like is_neutral_ground could be true, false, or null β€” three states for two outcomes. v2 collapses to two.

4

Decoded enums

Weather codes are decoded for you: each detail response carries a description alongside the numeric code.

5

Stable IDs

Internal primary keys everywhere β€” including incidents (player_id, player_in_id, player_out_id). No juggling between two ID spaces.

6

Enriched stats

Per-side stats merge ball-tracking metrics (attack, ball_safe, dangerous_attack) with traditional stats and add computed pass_accuracy_pct.

7

Better metadata

New fields you didn't have before: derby, neutral-ground, travel distance, pitch condition, weather, attendance, jerseys, pre-match facts.

8

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.

Headerhttp
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.

URL
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.

Open the Live WebSocket page β†’

List events

Paginated list of events. Each item has the same shape as the detail endpoint.

GET/api/v2/events/

Query parameters

NameTypeDescription
league_idintFilter by league
statusstrnotstarted, inprogress, finished, …
date_fromstrYYYY-MM-DD or ISO (UTC)
date_tostrYYYY-MM-DD or ISO (UTC)
limitintPage size, default 50, max 200
offsetintPage offset
Requestcurl
curl -H "Authorization: Token $TOKEN" \
  "https://sports.bzzoiro.com/api/v2/events/?league_id=49&status=finished&limit=5"
Responsejson
{
  "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.

GET/api/v2/events/live/

Query parameters

NameTypeDescription
league_idintFilter by league.
season_idintFilter by season.
team_idintMatches 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 | finished
  • period: 1st_half | halftime | 2nd_half | extra_time | FT | nullnull when status is notstarted or penalties
  • current_minute: int or null (non-null only for inprogress and penalties)
  • home_score_ht/away_score_ht: int or null
  • live_websocket: true when the event is wired to /ws/live/
  • last_updated: ISO-8601 UTC with Z suffix — the row's updated_at
  • event_date: ISO-8601 UTC with literal Z suffix
Requestcurl
curl -H "Authorization: Token $TOKEN" \
  "https://sports.bzzoiro.com/api/v2/events/live/?league_id=49"
Responsejson
{
  "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.

GET/api/v2/events/{id}/

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.

Responsejson
{
  "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
}
live_websocket  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.

GET/api/v2/events/{id}/stats/

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.awayobjPer-side merged metrics
shotmaplistPer-shot xG, coords, body part, situation
momentumlistPer-minute pressure index
average_positionsobjAverage pitch positions per player
xg_per_minutelistxG buckets and cumulative totals
Response (excerpt)json
{
  "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.

GET/api/v2/events/{id}/incidents/

Every *_id field is a BSD primary key β€” use it directly with other endpoints. On finished events is_live is always false.

Common types

goalplayer, sequence with coords
cardcard_type: yellow / red / yellowRed
substitutionplayer_in / player_out
period1st half, HT, 2nd half, FT
injuryTimeStoppage announcement
varDecisionDecision class + confirmed flag
Response (excerpt)json
{
  "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.

GET/api/v2/events/{id}/odds/

A value of null means we don't have a consensus for that market β€” usually because too few bookmakers are quoting it.

Markets

1X2home_win / draw / away_win
Over / Under{1.5, 2.5, 3.5} goals
BTTSboth teams to score yes/no
Responsejson
{
  "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.

GET/api/v2/events/{id}/metadata/

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.

Responsejson
{
  "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.

GET/api/v2/events/{id}/lineups/

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.

Response (excerpt)json
{
  "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/.

GET/api/v2/events/{id}/player-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.

Response (excerpt)json
{
  "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.

GET/api/v2/leagues/

Query parameters

NameTypeDescription
countrystrFilter by country (case-insensitive)
is_womenboolDefault: both
is_activeboolDefault: only active
limitintPage size, default 50, max 200
offsetintPage offset
Responsejson
{
  "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.

GET/api/v2/leagues/{id}/

For the season ID alone, hit /leagues/{id}/season/. For the full archive, use /leagues/{id}/seasons/.

Responsejson
{
  "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).

GET/api/v2/leagues/{id}/season/

Useful when you only need the season ID (e.g. to query /events/?… with date filters in that season).

Responsejson
{
  "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.

GET/api/v2/leagues/{id}/seasons/

Use a season's id to scope a standings request: /leagues/{id}/standings/?season_id=ID.

Responsejson
{
  "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.

GET/api/v2/leagues/{id}/standings/

Query parameters

season_idintSeason ID. Defaults to the league's current season.

Row fields

positionint1-indexed rank
team_id / team_nameTeam identifier and display name
played, won, drawn, lostintMatch counts
gf, ga, gd, ptsintGoals for / against / difference / points
xgf, xga, xgdfloatExpected-goals equivalents (only counts matches with shotmap data)
xg_gamesintNumber of matches contributing to the xG totals
formstrLast 5 results, e.g. "WWLDW"
liveboolHas an in-progress match this matchday
Responsejson
{
  "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.

GET/api/v2/leagues/{id}/venues/

Query parameters

season_idintRestrict to assignments in this season.
limitintPage size, default 50, max 200
offsetintPage offset

For knockout-round filters (semifinal venue, final venue, host country) use the season-scoped variant below.

Response (excerpt)json
{
  "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.

GET/api/v2/leagues/{id}/seasons/{season_id}/venues/

Query parameters

host_country_codestrISO 3166-1 alpha-2 host country
hosts_openingboolVenues hosting the opening match
hosts_finalboolVenues hosting the final
hosts_third_placeboolVenues hosting the third-place match
roundstrOne of R32, R16, QF, SF, 3RD, F
limitintPage size, default 50, max 200
offsetintPage offset

Same shape as /leagues/{id}/venues/.

Requestcurl
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.

GET/api/v2/teams/

Query parameters

country_codestrISO 3166-1 alpha-2. Unknown codes return an empty list.
league_idintTeams with at least one event in this league
season_idintTeams that participated in this season
in_competitionboolTrue = teams with upcoming matches in league_id. False = teams that played there but no longer do. Requires league_id.
is_womenboolFilter by women / men competitions (needs league_id or season_id)
namestrPartial, case-insensitive name match
limitintDefault 50, max 200
offsetintPage offset
Responsejson
{
  "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.

GET/api/v2/teams/{id}/

Squad

Active players currently linked to this team via current_team.

GET/api/v2/teams/{id}/squad/

For full per-player detail (market value, foot, contract, etc.) hit /players/{id}/. The squad endpoint returns a brief shape for list rendering.

Responsejson
{
  "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}/.

GET/api/v2/teams/{id}/fixtures/

Query parameters

date_fromstrISO 8601 UTC, e.g. 2026-04-30T00:00:00Z
date_tostrISO 8601 UTC
league_idintRestrict to this league
statusstrnotstarted, inprogress, finished, postponed, cancelled
limit / offsetintStandard 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.

GET/api/v2/players/

Query parameters

team_idintFilter by current club
national_team_idintFilter by national-team
nationality_codestrISO 3166-1 alpha-2
positionstrGeneric (G/D/M/F) or specific position code
namestrMatches name or short_name (case-insensitive)
limit / offsetintStandard pagination
Response itemjson
{
  "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.

GET/api/v2/players/{id}/

Per-match stats

Per-match statistics for one player, newest first. Same record shape as /events/{id}/player-stats/.

GET/api/v2/players/{id}/stats/

Query parameters

season_idintFilter by season
team_idintStats logged with this team (handles transfers)
league_idintFilter by league
date_from / date_tostrISO 8601 UTC
limit / offsetintStandard pagination

Filtering by event_id is rejected β€” use /events/{id}/player-stats/ for that case.

Tip
// 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.

GET/api/v2/players/{id}/transfers/

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.

Response itemjson
{
  "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.

GET/api/v2/players/{id}/career/

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.

Responsejson
{
  "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.

GET/api/v2/players/{id}/national-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.

Responsejson
{
  "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.

GET/api/v2/managers/

Query parameters

team_idintActive tenure at this club
league_idintCoached at least one match in this league
nationality_codestrISO 3166-1 alpha-2
min_matchesintMinimum matches managed (default 0)
tactical_profilestrattacking, defensive, balanced
team_stylestrpossession, counter, pressing, defensive, wide, fan
namestrMatches name or short_name
limit / offsetintStandard pagination
Response itemjson
{
  "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.

GET/api/v2/managers/{id}/

Career

Tenure history at each club with date range and per-tenure record (matches/wins/draws/losses/win%).

GET/api/v2/managers/{id}/career/

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.

Responsejson
{
  "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.

GET/api/v2/managers/{id}/matches/

Query parameters

date_from / date_tostrISO 8601 UTC
league_idintRestrict to this league
team_idintMatch where this manager was on the bench for this team (handles multiple tenures)
statusstrnotstarted, inprogress, finished, postponed, cancelled
limit / offsetintStandard 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.

GET/api/v2/odds/

Query parameters

NameTypeDescription
event_id / league_id / season_id / team_idintStandard event-scope filters.
bookmaker_slugstrSlug only (e.g. pinnacle). Display names rejected with 400.
marketenum1x2, btts, over_under_15, over_under_25, over_under_35, double_chance, draw_no_bet.
outcomeenumHOME, DRAW, AWAY, over, under, yes, no, 1X, 12, X2.
min_decimal_odds / max_decimal_oddsfloatBounds on price.
movementenumSHORTENING | DRIFTING.
is_max_quoteboolSource flagged the row as best price across books.
updated_afterstrISO-8601 UTC. Only odds with observed_at ≥ this.
limit / offsetintStandard 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.".

Itemjson
{
  "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.

GET/api/v2/odds/{id}/

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.

GET/api/v2/odds/best/

Query parameters

NameTypeDescription
marketenumDefault 1x2.
league_id / season_id / team_idintStandard scoping.
date_from / date_tostrISO-8601 UTC. Replaces v1's days=N.
limit / offsetintStandard pagination (default 50, max 200).
Result itemjson
{
  "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.

GET/api/v2/events/{id}/odds/comparison/

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.

GET/api/v2/events/{id}/polymarket/

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.

GET/api/v2/bookmakers/

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.

GET/api/v2/predictions/

Query parameters

NameTypeDescription
statusenumupcoming (default) | past | all. Ignored when date_from/date_to is set — the explicit window wins.
league_idintFilter by league.
season_idintFilter by season.
team_idintPredictions where the team played home or away.
date_from / date_tostrISO-8601 UTC, or YYYY-MM-DD.
min_confidencefloat0–1. Lower bound on model confidence.
recommendedbooltrue = at least one *_recommend is true. false = all false. Omit to skip filter.
limit / offsetintStandard pagination (default 50, max 200).
Response itemjson
{
  "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.

GET/api/v2/predictions/{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.

GET/api/v2/events/{id}/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.

GET/api/v2/referees/

Query parameters

league_idintAt least one match officiated in this league. When set, aggregates are scoped to this league.
namestrPartial, case-insensitive name match
country_codestrISO 3166-1 alpha-2
min_matchesintMinimum matches officiated (default 0)
limit / offsetintStandard pagination
Response itemjson
{
  "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.

GET/api/v2/referees/{id}/

Matches

Matches officiated by this referee. Defaults to a now βˆ’ 3h to now + 7d window.

GET/api/v2/referees/{id}/matches/

Query parameters

date_from / date_tostrISO 8601 UTC
league_idintRestrict to this league
season_idintRestrict to this season
statusstrnotstarted, inprogress, finished, postponed, cancelled
limit / offsetintStandard 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.

GET/api/v2/venues/

Query parameters

country_codestrISO 3166-1 alpha-2
citystrPartial, case-insensitive city match
namestrPartial, case-insensitive venue name
min_capacityintMinimum seating capacity
team_idintHome stadium of this team
limit / offsetintStandard pagination
Response itemjson
{
  "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.

GET/api/v2/venues/{id}/

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.

Assignment shapejson
{
  "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.

GET/api/v2/venues/{id}/competitions/

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.

GET/api/v2/tv-channels/

Query parameters

NameTypeDescription
country_codestrISO 3166-1 alpha-2 (e.g. PT, ES). 400 if malformed.
namestrCase-insensitive partial match.
limit / offsetintStandard pagination.
Itemjson
{
  "id": 142,
  "name": "Sport TV1",
  "country_code": "PT",
  "link": ""
}

Channel detail

Same shape as a list item. Unknown id → 404.

GET/api/v2/tv-channels/{id}/

Broadcasts (by channel)

Paginated broadcasts emitted by this channel. Default temporal window: now − 3 h to now + 7 d.

GET/api/v2/tv-channels/{id}/broadcasts/

Query parameters

NameTypeDescription
date_from / date_tostrISO-8601 UTC. Override the default window.
league_id / season_idintStandard scoping.
limit / offsetintStandard 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.

GET/api/v2/broadcasts/

Query parameters

NameTypeDescription
event_id / league_id / season_id / team_idintStandard event-scope filters.
country_codestrISO 3166-1 alpha-2.
channel_idintInternal TVChannel PK (not the upstream id used by v1).
date_from / date_tostrISO-8601 UTC.
limit / offsetintStandard pagination.
Itemjson
{
  "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.

GET/api/v2/broadcasts/{id}/

By event

Paginated broadcasts for one event across countries and channels. Optional country_code filter narrows to a single market.

GET/api/v2/events/{id}/broadcasts/

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.

List social

Paginated feed sorted newest first. Cached 15 min.

GET/api/v2/social/

Query parameters

NameTypeDescription
team_id / event_id / manager_idintFilter by linked entity.
player_idintFilter by mentioned player. Heuristic linking β€” best-effort NLP/regex over the text; false positives possible.
season_id / league_idintItems linked to events in that season/league.
typeenumtweet | video.
published_after / published_beforestrISO-8601 UTC.
account_verifiedboolFilter by verified-account flag.
limit / offsetintStandard pagination.
Itemjson
{
  "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": []
  }
}

Social detail

Same shape as a list item. Unknown id → 404. News items return 404 in v2.

GET/api/v2/social/{id}/

By team

Paginated social items linked to one team. Same item shape as /social/.

GET/api/v2/teams/{id}/social/

Filters: type, published_after, published_before, account_verified, limit, offset.

By event

Paginated social items linked to one event. Temporal filters omitted — the event already bounds the window.

GET/api/v2/events/{id}/social/

By player

Paginated social items mentioning one player. Heuristic linking β€” false positives possible.

GET/api/v2/players/{id}/social/

By manager

Paginated social items linked to one manager.

GET/api/v2/managers/{id}/social/

Weather codes

The weather.code field uses this dictionary. weather.description is the same mapping pre-decoded for clients that only need the label.

CodeDescription
0unknown / closed venue
1clear
2cloudy
3rain
4snow
5extreme

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

Ratio shapejson
{
  "value": 2,    // successful
  "total": 14,   // attempts
  "pct":   14    // rounded percentage
}

Migration from v1

Equivalence map and a recommended loading strategy.

Field mapping

v1v2
GET /api/events/{id}/?full=trueMultiple 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 nullalways 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 idincident 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_winmarkets.match_result.{prob_home, prob_draw, prob_away}
predicted_resultmarkets.match_result.predicted (enum H/D/A)
expected_home_goals / expected_away_goalsmarkets.expected_goals.{home, away}
prob_over_15 / prob_over_25 / prob_over_35markets.over_under.prob_over_{15,25,35}
prob_btts_yesmarkets.btts.prob_yes
most_likely_scoremarkets.score.most_likely
favorite (string)recommendations.favorite (H/A/null)
favorite_probrecommendations.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_recommendrecommendations.{over_15, over_25, over_35}
btts_recommendrecommendations.btts
winner_recommendrecommendations.winner
confidencemodel.confidence (always normalized to 0–1)
model_versionmodel.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+outcome400 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 namescompare 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_officialremoved (always true, redundant)
tv-channel response: upstream channel_idremoved 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 labelsadds home_team_id, away_team_id, league_id (id+label pattern)
broadcasts: no paginationstandard 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: sourceremoved (vendor name no longer exposed)
social response: media_urlsmedia normalized: [{type, url, width, height}]
social response: account_handle / account_name / account_verifiedgrouped under account.{handle, name, verified}
social response: teams / events / players / managers arraysgrouped under linked.{teams, events, players, managers}
social news itemsexcluded from v2 (only tweet + video)
social embedded in /events/{id}/ or /teams/{id}/ detailnot 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 surfaceEndpoint
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