CS2 API#
The CS2 API covers competitive Counter-Strike 2: tournaments, teams (with ELO ratings and map pools), players, matches with the series score and each side's ELO going into the match, per-match pre-match team and player stats, and model-based win predictions.
Base URL: https://sports.bzzoiro.com/csgo/api/v2/
Access#
Requires the Sports Addon ($7/month, $5 Founder rate) — it unlocks the tennis, CS2, darts, hockey, basketball and horse racing APIs plus their MCP servers. Get it at /addons/. You also need a free account token from /register/.
Authorization: Token YOUR_API_KEY
Without a token you get 401:
{"error": "authentication required", "code": "authentication_required",
"detail": "Send Authorization: Token <YOUR_TOKEN> on every request.",
"register": "https://sports.bzzoiro.com/register/"}
With a token but no addon you get 402:
{"error": "Sports Addon required", "code": "addon_required",
"detail": "Tennis, CS:GO, darts, hockey, basketball and horse racing APIs require the Sports Addon ($7/mo, $5 for Founder badge holders).",
"pricing_url": "https://sports.bzzoiro.com/pricing/"}
List endpoints paginate with limit (default 50, max 200) and offset, and
return {count, next, previous, results}.
Endpoints#
| Endpoint | Description |
|---|---|
GET /csgo/api/v2/tournaments/ |
List tournaments |
GET /csgo/api/v2/tournaments/{id}/ |
Tournament detail |
GET /csgo/api/v2/teams/ |
List/search teams with current ELO |
GET /csgo/api/v2/teams/{id}/ |
Team detail: current ELO, map pool, round winrates |
GET /csgo/api/v2/players/ |
List/search players |
GET /csgo/api/v2/players/{id}/ |
Player profile and current team |
GET /csgo/api/v2/matches/ |
List matches |
GET /csgo/api/v2/matches/live/ |
Matches in play right now |
GET /csgo/api/v2/matches/{id}/ |
Match detail: series score, format, pre-match ELO |
GET /csgo/api/v2/matches/{id}/odds/ |
Per-bookmaker odds, every market |
GET /csgo/api/v2/matches/{id}/stats/ |
Pre-match team form, map pool, head-to-head |
GET /csgo/api/v2/matches/{id}/player-stats/ |
Per-player form with percentiles |
GET /csgo/api/v2/predictions/ |
Model win probabilities |
GET /csgo/api/v2/predictions/{id}/ |
One prediction |
The machine-readable schema is public at /csgo/api/schema/, with an
interactive UI at /csgo/api/docs/ and a themed explorer at
/docs/explorer/csgo/.
Tournaments#
Query parameters
| Param | Type | Description |
|---|---|---|
include_inactive |
bool | Include finished/inactive tournaments |
limit / offset |
int | Pagination (default 50, max 200) |
Only active tournaments by default. Responses are cached ~5 minutes.
{"count": 403, "next": "...?limit=2&offset=2", "previous": null, "results": [
{"id": 110, "api_id": 1448, "name": "A1 Gaming League S11", "is_active": true},
{"id": 149, "api_id": 1646, "name": "Ace North American Masters Spring 2026", "is_active": true}
]}
Teams#
Query parameters
| Param | Type | Description |
|---|---|---|
search |
string | Name search |
country |
string | ISO country code |
limit / offset |
int | Pagination |
Cached ~5 minutes.
curl -H "Authorization: Token YOUR_API_KEY" \
"https://sports.bzzoiro.com/csgo/api/v2/teams/?search=vitality"
{"count": 2, "next": null, "previous": null, "results": [
{"id": 464, "name": "Team Vitality", "short_name": "Team Vitality",
"country": "", "country_code": "", "elo_rating": 1756, "elo_peak": 1756},
{"id": 943, "name": "Vitality Academy", "short_name": "Vitality. AC",
"country": "", "country_code": "", "elo_rating": 1510, "elo_peak": 1510}
]}
elo_rating is the team's rating today (1500 = average). For the rating a
team held going into a given match, use home_elo_before / away_elo_before
on the match. country and country_code are often empty.
GET /teams/{id}/ adds map_stats, from the latest snapshot we hold (as_of
is the start of the match that snapshot belongs to; null when we hold none):
the team's overall, T-side and CT-side round win rates (%), its overall map
win rate, and maps — the maps it picks. Despite its name, maps[].winrate is
the share of the team's picks that went to that map (they add up to about
100), not a win rate on the map.
{"id": 464, "name": "Team Vitality", "short_name": "Team Vitality",
"country": "", "country_code": "", "elo_rating": 1756, "elo_peak": 1756,
"map_stats": {"as_of": "2026-10-11T12:00:00+00:00",
"maps": [{"map": "Inferno", "winrate": 20}, {"map": "Nuke", "winrate": 25},
{"map": "Dust2", "winrate": 35}, {"map": "Cache", "winrate": 20.0},
{"map": "Anubis", "winrate": 5}],
"sample_size": 50, "map_winrate": 68.0, "round_winrate": 57.0,
"round_winrate_t": 51.5, "round_winrate_ct": 62.3}}
Players#
Query parameters
| Param | Type | Description |
|---|---|---|
search |
string | Name/nickname search |
team |
int | Filter by team id |
limit / offset |
int | Pagination |
GET /players/{id}/ returns the player's profile and current team. Per-player
performance (K/D, damage, opening kills, clutch rounds) is published per match,
at /matches/{id}/player-stats/ (see Match stats).
{"id": 906, "api_id": 108, "nickname": "ZywOo",
"first_name": "Mathieu", "last_name": "Herbaut",
"country": "", "country_code": "",
"current_team": {"id": 464, "name": "Team Vitality", "short_name": "Team Vitality",
"country_code": "", "country": ""}}
Matches#
Query parameters
| Param | Type | Description |
|---|---|---|
tournament |
int | Filter by tournament id |
team |
int | Matches involving this team id |
status |
string | notstarted, inprogress, finished, postponed, cancelled |
date_from / date_to |
date | YYYY-MM-DD. Without either, the list returns the next 7 days, oldest first. Any of the filters above lifts that window and returns the full archive, newest first. |
limit / offset |
int | Pagination |
curl -H "Authorization: Token YOUR_API_KEY" \
"https://sports.bzzoiro.com/csgo/api/v2/matches/?team=558&status=finished"
{"count": 110, "next": "...", "previous": null, "results": [
{"id": 10843, "api_id": 451015,
"tournament": {"id": 482, "name": "PGL Bucharest 2027: Closed Qualifier", "is_active": true},
"home_team": {"id": 558, "name": "Team Nemesis", "short_name": "Team Nemesis",
"country_code": "", "country": ""},
"away_team": {"id": 461, "name": "Nuclear TigeRES", "short_name": "Nuclear TigeRES",
"country_code": "", "country": ""},
"start_time": "2026-10-16T08:55:00+00:00", "status": "finished",
"home_score": 2, "away_score": 1, "best_of": 3, "stage": "",
"home_elo_before": 1625, "away_elo_before": 1636}
]}
| Field | Meaning |
|---|---|
home_score / away_score |
Maps won in the series |
best_of |
Series format. Raised to what the final score proves (a 2-1 is a Bo3). Our match feed does not carry the format, so 1 also means not known: a 1-0 is a Bo1 or an unfinished Bo3, and an upcoming match has nothing to go on. Read best_of: 1 as "Bo1 or unknown". |
home_elo_before / away_elo_before |
Each side's ELO going into this match (1500 = average). For a played match it is the rating held before it and does not change afterwards, so it is safe as a model feature. For an upcoming or live match it is the team's current rating, which does not include this match yet. null when not known — for example a match finished within the last few hours, before ratings are recalculated (every six hours). |
GET /matches/live/ returns in-play matches only (cached 30 seconds).
GET /matches/{id}/ returns the same fields plus maps.
mapsis always an empty list. Per-map results (map names, round scores, halves, overtime) are not collected at the moment. The field stays in the response so code written against it keeps working; the series score is inhome_score/away_score.
{"id": 10843, "status": "finished", "home_score": 2, "away_score": 1,
"best_of": 3, "home_elo_before": 1625, "away_elo_before": 1636,
"maps": []}
Odds#
GET /csgo/api/v2/matches/{id}/odds/ returns every price we hold for one match: the
match-winner market flattened per bookmaker, and below it every other market the
books offered, grouped by kind, line and period.
curl -H "Authorization: Token YOUR_API_KEY" \
"https://sports.bzzoiro.com/csgo/api/v2/matches/8980/odds/"
{"match_id": 8980, "start_time": "2026-07-09T13:00:00+00:00",
"home_team_name": "FaZe", "away_team_name": "MOUZ",
"bookmakers_count": 16, "source": "multi",
"bookmakers": [
{"bookmaker": "Bet365", "bookmaker_slug": "bet365",
"odds_home": 1.47, "movement_home": "STABLE",
"odds_away": 2.59, "movement_away": "STABLE",
"updated_at": "2026-07-09T10:04:43Z"}
],
"markets": [
{"market_kind": "OU", "market_family": "OU", "market_line": 2.5,
"market_period": "FT", "selections": ["OVER", "UNDER"],
"bookmakers": [
{"bookmaker": "Bet365", "bookmaker_slug": "bet365",
"prices": {"OVER": {"price": 2.02, "movement": "STABLE"},
"UNDER": {"price": 1.74, "movement": "STABLE"}},
"updated_at": "2026-07-09T10:04:45Z"}
]}
]}
| Field | Meaning |
|---|---|
source |
multi per-bookmaker prices, consensus the single stored price when the match is not yet linked to a priced feed event, none when nobody has priced it |
bookmakers_count |
Bookmakers with at least one match-winner price |
movement_* |
SHORTENING, DRIFTING or STABLE against the previous price we saw; null when there is no previous price |
market_kind |
Market type: WINNER, AH (map handicap), OU (total maps or rounds) |
market_family |
Distinguishes markets sharing a kind (total goals vs corners vs a single side's total) |
market_line |
The handicap or total the market is quoted on; null for a plain winner market |
market_period |
FT for the full match; period codes otherwise |
Prices come from several sources and are pooled per match. A bookmaker appears once per market with its most recent price.
Predictions#
Query parameters
| Param | Type | Description |
|---|---|---|
upcoming |
bool | Only matches not yet started |
date_from / date_to |
date | YYYY-MM-DD |
limit / offset |
int | Pagination |
Without dates, only matches that have not started (upcoming defaults to
true; pass upcoming=false for the archive). Probabilities are 0-1.
{"count": 17, "next": "...", "previous": null, "results": [
{"id": 6494,
"match": {"id": 10884, "start_time": "2026-10-18T17:00:00+00:00",
"status": "notstarted", "best_of": 1,
"home_team": {"id": 1048, "name": "6yuBoJlbl"},
"away_team": {"id": 446, "name": "HyperSpirit"},
"home_elo_before": 1500, "away_elo_before": 1413},
"home_win_prob": 0.729, "away_win_prob": 0.271,
"predicted_winner": "6yuBoJlbl", "confidence": 0.729,
"home_form_score": 50.0, "away_form_score": 33.3,
"h2h_advantage": 0.0, "is_correct": null}
]}
match is the full match object from the Matches section, shortened here.
Match stats#
GET /matches/{id}/stats/ returns how both sides were playing going into the
match: round win rates split by side, pistol and opening-duel conversion, how
each team picks and bans maps, and the head-to-head.
Every figure is an aggregate over the trailing three months, taken as one
snapshot per match. collected_at says when that snapshot was taken, and
pre_match whether that was before the scheduled start:
pre_match: true— taken before the start, so it cannot contain this match. Snapshots are only taken before the start and are never rewritten.pre_match: false— an older snapshot taken after the start. Its window may include this very match, so do not use it as a pre-match feature. These are kept for reference rather than deleted.null— nothing was collected for this match.
curl -H "Authorization: Token YOUR_API_KEY" \
"https://sports.bzzoiro.com/csgo/api/v2/matches/10843/stats/"
{"match_id": 10843,
"collected_at": "2026-10-15T14:17:10.130509+00:00", "pre_match": true,
"teams": [
{"team": {"id": 558, "name": "Team Nemesis", "short_name": "Team Nemesis",
"country_code": "", "country": ""},
"map_winrate": 66.13, "map_sample_size": 62,
"round_winrate_all": 53.92, "round_winrate_t": 50.54, "round_winrate_ct": 57.25,
"pistol_winrate": 49.19, "headshot_rate": 50.47, "opening_conversion": 72.44,
"elimination_winrate": 34.12, "objective_winrate": 19.8,
"first_to_rounds": {"3": 54.84, "6": 61.29, "9": 66.13, "12": 66.13},
"avg_kills": 3.44, "avg_deaths": 3.18, "avg_assists": 1.39, "kd_ratio": 1.08,
"avg_damage": 375.71, "avg_opening_kills": 0.55, "avg_awp_kills": 0.38,
"avg_utility_damage": 31.53, "avg_flash_assists": 0.2,
"form_results": [
{"win": false, "date": "2026-10-08T12:08:19Z", "home_team": "Team Nemesis",
"away_team": "Sinners Esports", "home_score": 1, "away_score": 2,
"tournament": "Private Club #2: Closed Qualifier"}
]}
],
"map_pool": [
{"team": {"id": 558, "name": "Team Nemesis", "short_name": "Team Nemesis",
"country_code": "", "country": ""},
"maps": [{"map_name": "Inferno", "winrate": 24, "type": "picked"},
{"map_name": "Dust2", "winrate": 28, "type": "picked"}]}
],
"head_to_head": {"total_matches": 2, "home_wins": 1, "away_wins": 1,
"recent_matches": [
{"date": "2026-10-02T13:45:00Z", "home_team": "Team Nemesis",
"away_team": "Nuclear TigeRES", "home_score": 2, "away_score": 1,
"tournament": "CIS LAN Championship #8"},
{"date": "2026-08-31T12:05:00Z", "home_team": "Team Nemesis",
"away_team": "Nuclear TigeRES", "home_score": 1, "away_score": 2,
"tournament": "Moscow Cyber Games 2026: European Qualifier"}
]}}
| Field | Meaning |
|---|---|
*_winrate, headshot_rate, opening_conversion, first_to_rounds |
Percentages (0-100) |
avg_*, kd_ratio |
Averages over the window |
form_results |
Recent results, most recent first; win is from this team's point of view |
map_pool[].maps[].winrate |
Despite its name, the share of the team's picks (type: picked) or bans (type: banned) that went to that map, in % — not a win rate. Picked shares add up to about 100. |
head_to_head.home_wins / away_wins |
Wins of this match's home and away team in the meetings listed |
head_to_head.recent_matches |
The last meetings, each with the home/away it was actually played with |
GET /matches/{id}/player-stats/ returns the same idea per player — kills,
deaths, damage, opening kills, utility and clutch rounds — each with the
percentile that player sits in, so a number can be read without knowing what
good looks like:
{"match_id": 10843,
"collected_at": "2026-10-15T14:17:10.207750+00:00", "pre_match": true,
"players": [
{"nickname": "tronic", "real_name": "Kamil Shakhov",
"team": {"id": 461, "name": "Nuclear TigeRES", "short_name": "Nuclear TigeRES",
"country_code": "", "country": ""},
"avg_kills": 0.68, "avg_deaths": 0.59, "avg_assists": 0.25,
"kd_ratio": 1.16, "kd_percentile": 67.09,
"avg_damage": 69.97, "damage_percentile": 42.01,
"headshot_rate": 38.3, "headshot_percentile": 19.75,
"avg_opening_kills": 0.11, "opening_kills_percentile": 71.27,
"avg_awp_kills": 0.31, "avg_flash_assists": 0.05,
"avg_utility_damage": 3.73, "avg_enemies_flashed": 0.53,
"avg_multi_kill_rounds": 0.16, "multi_kill_percentile": 53.86,
"avg_clutch_rounds": 0.01, "clutch_percentile": 88.39,
"favourite_weapons": [{"weapon": "AWP", "avg_kills": 0.4}]}
]}
Values are per round over the same trailing three-month window, and
collected_at / pre_match work exactly as on /stats/.
Both return empty lists (with collected_at and pre_match null) for matches
with no stats collected — mostly lower-tier fixtures and matches that were
already under way when we first saw them.
Coverage and off-season#
Tournaments run all year but not every week has one, and the schedule is usually published only days ahead.
GET /api/v2/coverage/?sport=csgo says so directly, without a token:
{"sport": "csgo", "status": "in_season", "events_next_7d": 6,
"events_next_30d": 6, "priced_next_7d": 0, "live_now": 0,
"next_event_at": "2026-08-20T08:00:00+00:00", "last_event_at": "2026-08-19T16:00:00+00:00"}
status is in_season (fixtures within seven days), between_events (nothing
this week, something later), off_season (nothing ahead and the last event is
more than a fortnight old) or no_fixtures. Drop the ?sport= to get every
sport in one call — worth doing before a polling loop decides what to ask for.