# EsportsOdds API documentation - **Get started** - [Introduction](https://docs.esportsodds.gg/docs): Counter-Strike 2 odds, match data, and player stats — one REST + WebSocket API. - [Quickstart](https://docs.esportsodds.gg/docs/quickstart): From an API key to live matches, odds, and a WebSocket stream in four steps. - [Authentication](https://docs.esportsodds.gg/docs/authentication): Authenticate every request with an API key in the Authorization Bearer header. - [Your first integration](https://docs.esportsodds.gg/docs/first-integration): A complete, runnable script that lists matches, expands one, and reads its odds. - [What's in the data](https://docs.esportsodds.gg/docs/whats-in-the-data): An honest inventory of what exists, what's thin, and what isn't there at all. - Core concepts - [The data model](https://docs.esportsodds.gg/docs/concepts/data-model): How games, tournaments, matches, maps, rounds and players relate — and which endpoint returns each. - [Match lifecycle](https://docs.esportsodds.gg/docs/concepts/match-lifecycle): What populates when, from scheduled through live to completed. - [The response envelope](https://docs.esportsodds.gg/docs/concepts/envelope): Every endpoint returns {data, meta} — what each part means and the two exceptions. - [Pagination](https://docs.esportsodds.gg/docs/concepts/pagination): Forward cursors, why they aren't page numbers, and how to page without missing rows. - [Filtering and lookups](https://docs.esportsodds.gg/docs/concepts/filtering): The filters each list accepts, how they combine, and resolving a name to an id. - [IDs, slugs and time](https://docs.esportsodds.gg/docs/concepts/ids-and-time): UUIDv7 identifiers, human-readable slugs, and the timestamp formats the API uses. - [Markets and outcomes](https://docs.esportsodds.gg/docs/concepts/markets-and-outcomes): How an odds line is structured — markets, outcomes, lines and prices. - [How the market line is computed](https://docs.esportsodds.gg/docs/concepts/market-line): eo_market — de-vig, median, renormalise, and the coverage rule behind every published line. - [Odds movement](https://docs.esportsodds.gg/docs/concepts/odds-movement): open_price, delta_since_open and is_closing — reading a price series correctly. - [The model line and its validation gate](https://docs.esportsodds.gg/docs/concepts/model-line): eo_model, why it isn't served yet, and how to read the accuracy we publish. - CS2 data reference - [Player match stats](https://docs.esportsodds.gg/docs/cs2-data/match-stats): The per-player stat line — which fields are always present, which are nullable, and the map_number trap. - [Round stats](https://docs.esportsodds.gg/docs/cs2-data/round-stats): Two rows per round, the signed economy tier, and what each counter measures. - [Player depth](https://docs.esportsodds.gg/docs/cs2-data/player-depth): Weapons, grenades, hitgroups, duels and flashes — the deepest layer, and the least universal. - [Map results](https://docs.esportsodds.gg/docs/cs2-data/map-results): Per-map scores, the half-by-half split, and why overtime is null rather than zero. - [Map vetoes](https://docs.esportsodds.gg/docs/cs2-data/vetoes): The pick/ban sequence, including the decider that no team chose. - [Team records — form, head-to-head, maps, playstyle](https://docs.esportsodds.gg/docs/cs2-data/team-records): The four derived team endpoints and what each is actually measuring. - [Ratings and rankings](https://docs.esportsodds.gg/docs/cs2-data/ratings-and-rankings): Glicko-2, why the board sorts on a discounted rating, and the sample floors. - [Roster history](https://docs.esportsodds.gg/docs/cs2-data/roster-history): The five a team actually fielded, match by match, and what each continuity state means. - [Enum reference](https://docs.esportsodds.gg/docs/cs2-data/enums): Every fixed value the API returns or accepts, in one place. - How-to - [Budget your requests](https://docs.esportsodds.gg/docs/how-to/request-budget): 20,000 a month is about 28 an hour. Here is the arithmetic, not a reassurance. - [Build a fixtures cache](https://docs.esportsodds.gg/docs/how-to/fixtures-cache): Keep a local schedule in sync for a few hundred requests a month. - [Fetch everything about one match](https://docs.esportsodds.gg/docs/how-to/fetch-a-full-match): One request instead of five, and how to skip the sub-resources that don't exist. - [Resolve a name to an id](https://docs.esportsodds.gg/docs/how-to/resolve-by-slug): Going from "NAVI" to a UUID — or skipping the lookup entirely. - [Track odds movement](https://docs.esportsodds.gg/docs/how-to/track-odds-movement): Store a price series correctly, given lines are only written when they change. - [Handle errors and retries](https://docs.esportsodds.gg/docs/how-to/errors-and-retries): Which failures are worth retrying, which aren't, and how to back off correctly. - [Connect a WebSocket](https://docs.esportsodds.gg/docs/how-to/connect-websocket): A working client, from ticket to subscription, with the mistakes pre-removed. - [Recover from disconnects](https://docs.esportsodds.gg/docs/how-to/recover-from-disconnects): Sequence numbers, gap detection, and why there is no replay buffer. - [Backfill historical data](https://docs.esportsodds.gg/docs/how-to/backfill-history): Walking the archive once without spending your whole month on it. - [Build your own leaderboard](https://docs.esportsodds.gg/docs/how-to/build-a-board): Using /rankings as a base, and what to do when you want different weights. - [Migrate from a scraper](https://docs.esportsodds.gg/docs/how-to/migrate-from-scraping): Mapping the concepts you already have onto this API, and what changes. - **Platform** - [Versioning & game namespacing](https://docs.esportsodds.gg/docs/versioning): The /v1/{game}/{resource} path convention, and how a second game extends it. - [Rate limits](https://docs.esportsodds.gg/docs/rate-limits): A per-key token bucket governs request rates; a hard monthly quota governs volume. - [Errors](https://docs.esportsodds.gg/docs/errors): HTTP status codes and the neutral JSON error body the API returns. - [Live data & WebSocket](https://docs.esportsodds.gg/docs/live-data): Subscribe to CS2 score and odds updates over a WebSocket channel. - [Changelog](https://docs.esportsodds.gg/docs/changelog): Notable changes to the EsportsOdds API and documentation. - Headers, CORS & policy - [Response headers](https://docs.esportsodds.gg/docs/platform/response-headers): Every header the API sets, what it means, and which ones a browser can read. - [CORS and browser clients](https://docs.esportsodds.gg/docs/platform/cors): Which origins are allowed, which headers are readable, and why you should proxy anyway. - [Versioning and deprecation policy](https://docs.esportsodds.gg/docs/platform/deprecation): What we promise about /v1, how changes are announced, and what counts as breaking. - [Status and support](https://docs.esportsodds.gg/docs/platform/status-and-support): Checking whether it's us, and what to include when it is. - Help - [Troubleshooting](https://docs.esportsodds.gg/docs/help/troubleshooting): The behaviours that surprise people most often, and what to do about each one. - [FAQ](https://docs.esportsodds.gg/docs/help/faq): The questions that come up before and during an integration. - [Known limitations](https://docs.esportsodds.gg/docs/help/limitations): What the API doesn't do today, stated plainly so you find out before you build. - [Glossary](https://docs.esportsodds.gg/docs/help/glossary): API terms defined as they are used here, with pointers to the deeper explainers. - **API reference** - Live - [Mint a short-lived WebSocket ticket](https://docs.esportsodds.gg/docs/live/mint-ws-token): Exchanges the caller's API key for a short-lived (60s) signed ticket used to open the WebSocket at `wss://api.esportsodds.gg/v1/ws?token=...`. Keeps the raw key off the wire and out of the browser — browsers can't set headers on a `WebSocket`, which is why tickets exist. Minting is authenticated, rate-limited and metered like any REST call. This is the only part of the WebSocket surface expressible in OpenAPI: the socket itself (`/v1/ws`) is a persistent connection with its own message protocol, so it is documented as prose instead — see **[Live data & WebSocket](https://docs.esportsodds.gg/docs/live-data)** for the handshake, the message envelope, subscribing, sequence numbers, reconnection and the connection cap. Read that page before building against this endpoint; a ticket on its own does nothing. - Matches - [Per-player CS2 depth breakdowns for one match](https://docs.esportsodds.gg/docs/matches/get-match-depth): The five CS2 depth breakdowns for one match, grouped into a single object: per-player `weapons`, `grenades`, `hitgroups` (body hit-group distribution), `duels` (the killer→victim kills matrix) and `flashes` (the flasher→flashed blind matrix). These are per-MATCH aggregates (not per map). Each group is a flat array whose elements carry their own `player_id` (and, for duels/flashes, the ordered pair of player ids), so a client groups by player itself. A match with no depth on record (older or lower-tier matches — roughly one in six lack it) returns every group as an empty array (never null); an unknown match id does the same. Single-resource envelope: `{"data": {weapons, grenades, hitgroups, duels, flashes}}`. - [A single match](https://docs.esportsodds.gg/docs/matches/get-match): One match by id, with the same fields as a list row minus the denormalised names — a detail row carries bare `team_a_id`/`team_b_id`/`tournament_id`, so resolve those separately or read them off the list route instead. The `data_available` flags tell you which sub-resources (`maps`, `stats`, `vetoes`, `rounds`, `depth`) actually exist for this match, so you can skip requests that would return an empty list. - [Per-map scores for one match](https://docs.esportsodds.gg/docs/matches/list-match-maps): Map-by-map results (half/overtime splits included when known), ordered by map number. Unknown match id yields an empty list. Enveloped: `{"data": [MapResult, ...], "meta": {...}}` (not cursor-paginated; `next_cursor` is null). - [Round-by-round team stats for one match](https://docs.esportsodds.gg/docs/matches/list-match-rounds): Per-team, per-round CS2 breakdown — two rows per round (one per team): side, win, economy, equipment, opening duels, trades, clutches, bomb events, KAST, utility. Ordered by map number, then round number, then side. Team-level (not per-player). Unknown match id yields an empty list. Enveloped: `{"data": [RoundTeamStat, ...], "meta": {...}}` (not cursor-paginated; `next_cursor` is null). - [Per-player performance stats for one match](https://docs.esportsodds.gg/docs/matches/list-match-stats): Player stat lines — whole-match aggregates (null map_number) first, then per-map rows. Ratings are our own proprietary computation. Enveloped: `{"data": [PlayerMatchStats, ...], "meta": {...}}` (not cursor-paginated; `next_cursor` is null). - [Map veto (pick/ban) sequence for one match](https://docs.esportsodds.gg/docs/matches/list-match-vetoes): The match's map pick/ban sequence in veto order (a CS2 bo3 runs ban/ban/pick/pick/ban/ban/decider). The decider step carries a null `team_id` — no team makes that choice. Unknown match id yields an empty list. Enveloped: `{"data": [MapVeto, ...], "meta": {...}}` (not cursor-paginated; `next_cursor` is null). - [List matches for a game](https://docs.esportsodds.gg/docs/matches/list-matches): Matches for the game, newest scheduled first. An unknown `{game}` 404s cleanly. Listing hygiene: by default, `cancelled` fixtures and `scheduled` rows more than 6 hours past their slot are excluded (a cancelled match is a known non-event; a long-overdue scheduled row means the upstream fixture no longer exists as listed). Pass `?status=cancelled` to list cancelled fixtures explicitly; `?status=scheduled` is the upcoming list and keeps the 6-hour exclusion. Any match remains fetchable by id regardless of status. - Meta - [Data & model coverage matrix (honest reliability)](https://docs.esportsodds.gg/docs/meta/get-coverage): The capability × status matrix behind the dashboard Coverage page and the docs coverage doc: per capability, its status and cadence. `docs/data-sourcing/reconciliation-policy.md`. - [Model transparency metrics (Brier, calibration)](https://docs.esportsodds.gg/docs/meta/get-model-metrics): Published accuracy of the model — Brier, Brier skill score vs the 0.25 no-skill baseline, and the settled-match sample, per market. A trust surface no competitor publishes. Empty until the validation gate clears (`odds-methodology.md`). - [Service health (incl. DB reachability)](https://docs.esportsodds.gg/docs/meta/health): Unauthenticated liveness/readiness probe. 200 when the DB is reachable, 503 otherwise. - Odds - [Odds lines (market + model)](https://docs.esportsodds.gg/docs/odds/list-odds): Time-series odds lines, newest first — the derived market line (`source: "eo_market"`) and the proprietary model line (`source: "eo_model"`; ADR 0009). With `?match={id}` the response is that match's line history, and `?cursor=` pages through it. **Without `?match=` the default is a bulk snapshot** — the latest line per source per open market across the game, the product's headline endpoint for pulling a whole slate in one call; the snapshot is a single page, so it **ignores `?cursor=`** (its `next_cursor` is always null). The movement fields (`open_price`, `delta_since_open`) are computed per response from the line's own series. Market rows appear once the multi-book scrape runs (`EXPOSE_MARKET_ODDS`; at least 2 contributing books per line); model rows only after the model's validation gate clears (`EXPOSE_MODEL_ODDS`) — before then model rows are simply absent, never faked. Raw per-book bookmaker rows are never returned (see the top-level description). **Markets & the `line` field.** Beyond match winner, the endpoint serves the derived line for additional markets as multi-book coverage arrives: `total_maps` (`outcome_key` `over`/`under`, `line` = the games threshold, e.g. `2.5`), `map_handicap` (`outcome_key` `home`/`away`, `line` = the **home team's signed handicap**, e.g. `-1.5`, carried by both complementary legs), and per-map `map_winner`. Because markets are distinguished by `line` as well as market and outcome, the **bulk snapshot may return more than one row per `market_id`** — one per distinct `line`. Each row is still the latest per `(market, outcome, line, source)`. - Players - [A player's all-time career aggregate](https://docs.esportsodds.gg/docs/players/get-player-career): Derived career totals + averages over the player's whole-match stat rows (maps are not double-counted). Averages are null for a player with no rated maps. 404 for an unknown player. Enveloped `{"data": PlayerCareerStats}`. - [A single player](https://docs.esportsodds.gg/docs/players/get-player): One player by id. Adds `own_rank` — the player's position on our own leaderboard — but only when you ask for it with `?rank=1` or `?include=rank`. Players carry no `external_rank`. - [List players for a game](https://docs.esportsodds.gg/docs/players/list-players): Every player in the game, alphabetically by nickname, cursor-paginated. Filter by `team`, `slug` or `role`, or fetch a specific batch with `?ids=` (1–500 comma-separated ids) — which is the efficient way to resolve the player ids you got back from a match's stats in a single request instead of one call per player. - Rankings - [Team or player rankings (leaderboard)](https://docs.esportsodds.gg/docs/rankings/list-rankings): Ranked leaderboard over a rolling **3-month** window. `?type=team` (default) or `?type=player` — the two are ranked on different metrics, reported in each row's `metric` field. **Teams** rank on `glicko2_conservative`: a Glicko-2 rating discounted by twice its rating deviation (`rating − 2·RD`), so a team with a volatile, thinly-evidenced rating sorts below an equally-rated team the model is confident about. **Players** rank on `rating`. Sorting rows by `value` reproduces `rank` exactly. Entities below the sample floor are omitted rather than ranked on noise: teams need at least 5 rated matches in the window, players at least 30 rated maps. This is always our own rating, never a third party's — a team's separately-sourced `external_rank` is reported alongside for comparison, not used for ordering. Not cursor-paginated: `limit` truncates the computed board and `meta.next_cursor` is always null. - Teams - [Head-to-head record between two teams](https://docs.esportsodds.gg/docs/teams/get-head-to-head): Completed-match record between the path team and `opponent`. Enveloped `{"data": HeadToHead}`. - [A team's round-level playstyle profile](https://docs.esportsodds.gg/docs/teams/get-team-round-stats): Aggregate round-level rates for the team (CT/T side win %, pistol %, opening-duel %, eco %, clutch %), derived over the team's recorded rounds. Each rate is a percentage in [0,100], or null when its denominator is empty. Enveloped `{"data": TeamRoundStats}`. - [A single team](https://docs.esportsodds.gg/docs/teams/get-team): One team by id. Adds `external_rank`, a separately-sourced world ranking where one is on file, and — only when you ask for it with `?rank=1` or `?include=rank` — `own_rank`, the team's position on our own leaderboard. - [A team's recent completed results (form)](https://docs.esportsodds.gg/docs/teams/list-team-form): Recent completed matches, newest first, from the team's own perspective (won + series map score for/against). `limit` default 10, max 50. Enveloped `{"data": [TeamFormResult, ...], "meta": {...}}` (not cursor-paginated). - [A team's per-map win/loss record](https://docs.esportsodds.gg/docs/teams/list-team-maps): The team's aggregate record on each map it has a completed result on, best win rate first. Raw counts (`played`, `wins`); the win rate is `wins / played`. Enveloped `{"data": [TeamMapStat, ...], "meta": {...}}` (not cursor-paginated). - [A team's Glicko-2 rating history](https://docs.esportsodds.gg/docs/teams/list-team-ratings): The team's own computed Glicko-2 rating over time (one point per played match), oldest→newest for a left-to-right chart. `limit` default 30, max 200. Enveloped `{"data": [TeamRatingPoint, ...], "meta": {...}}` (not cursor-paginated). - [A team's roster timeline (the five it actually fielded)](https://docs.esportsodds.gg/docs/teams/list-team-roster-history): The five players a team actually fielded in each recent completed match, newest first, with the derived roster-change facts. `limit` default 20, max 100. Enveloped `{"data": [TeamRosterEntry, ...], "meta": {...}}` (not cursor-paginated). Read `continuity` BEFORE interpreting `changed_count`: `stable` (same five as last match), `changed` (1–4 players swapped — the only value for which `has_debutant` is asserted), `reset` (all five swapped, i.e. an organisation rename or full rebuild, NOT five stand-ins), `resumed` (previous match beyond a 60-day gap — a transfer window, not a stand-in), and `new` (no prior match, so `players_in`/`players_out` are empty because there is no baseline to diff against). A match where a team used more than five players across maps is omitted rather than guessed at. - [List teams for a game](https://docs.esportsodds.gg/docs/teams/list-teams): Every team in the game, alphabetically by name, cursor-paginated. Use `?slug=` to resolve a known slug straight to its row — that is the intended way to turn a human-readable slug into the UUID the `/teams/{id}/…` routes need, and it never depends on where the team falls in the paging order. `logo_url` is always an empty string; see the field description. - Tournaments - [A single tournament](https://docs.esportsodds.gg/docs/tournaments/get-tournament): One tournament by id. For the field list see the `Tournament` schema; for who is playing, use the participating-teams sub-resource. - [A tournament's participating teams](https://docs.esportsodds.gg/docs/tournaments/list-tournament-teams): Participants ordered by seed then name. Seed is null until bracket data is sourced. Unknown tournament yields an empty list. Enveloped `{"data": [TournamentParticipant, ...], "meta": {...}}` (not cursor-paginated). - [List tournaments for a game](https://docs.esportsodds.gg/docs/tournaments/list-tournaments): Every tournament in the game, newest first by start date, cursor-paginated. Filter by `slug`, `tier`, `region`, `year` or `status`; the filters combine with AND. Note `status` is derived from the event's dates rather than stored, so a tournament with no `starts_at` has a null status and is excluded by any `status` filter. ## Machine-readable - [OpenAPI specification](https://docs.esportsodds.gg/openapi.yaml): the complete contract — every endpoint, parameter, schema and a real response example. Prefer this over crawling the reference pages. - [Full documentation text](https://docs.esportsodds.gg/llms-full.txt): every guide concatenated as markdown. - Any single guide as markdown: replace `/docs/` with `/md/` in its URL — e.g. https://docs.esportsodds.gg/md/concepts/pagination ## Notes - Base URL: https://api.esportsodds.gg — auth is `Authorization: Bearer `. - Coverage is CS2 only. Paths are game-namespaced as `/v1/{game}/{resource}`. - Odds are served as two derived sources (`eo_market`, `eo_model`). Individual bookmaker prices and names are never returned.