Changelog

Notable changes to the EsportsOdds API and documentation.

Notable, developer-facing changes to the API. Breaking changes ship under a new version prefix; within v1, changes are additive.

2026-07-26

Action required: the error body shape changed

Error responses moved from {"error": "message"} to a coded envelope. If you match on the old flat string, update before this reaches you — everything else in this release is additive.

  • Coded error envelope. Every non-2xx response now returns:

    { "error": { "code": "invalid_cursor", "message": "invalid cursor", "request_id": "019f…" } }

    code is a stable identifier you can switch on; message is prose and explicitly not a contract. This exists because the flat shape gave you nothing machine-readable beyond the HTTP status, which collapses a dozen distinct 400s into one bucket — so clients were left string-matching text we were free to change. The codes that matter most: a 429 is now distinguishably rate_limited (retry after a moment) or quota_exceeded (retrying cannot help until the window resets). See Errors for the full list and a worked handler.

  • X-Request-Id on every response, success or failure, and repeated as error.request_id. Log it — quoting it turns a support question into a single log lookup. A panic now also returns the envelope rather than an empty body.

  • Slug addressing. A path {id} accepts either a UUID or the resource's slug on teams, players and tournaments: /v1/cs2/teams/natus-vincere works. ?opponent= on head-to-head accepts both too. Matches have no slug.

  • ?include= on match detail. ?include=teams,tournament,odds embeds the related resources, so a match view is one request instead of four. Omit it and the response is byte-for-byte what it was.

  • ?sort= on /teams, /players and /rankings, with a - prefix for descending. Previously the parameter was accepted and silently ignored; an unrecognised key is now a 400 listing the accepted ones. Cursors are bound to the sort they were minted under — cursors issued before this change keep working on the default order.

  • CORS for browser callers. The rate-limit and quota headers are now readable cross-origin via Access-Control-Expose-Headers, and the interactive "try it" panel in these docs executes real requests.

  • Documentation. Every operation, parameter and schema field is now documented, every endpoint carries a real response example captured from production, and the reference is grouped by resource rather than by URL segment (old reference URLs redirect). The spec is published at /openapi.yaml. Nothing in these docs describes unshipped behaviour any more — the fields previously flagged as intended-but-not-yet-live are all shipped.

2026-07

  • Per-match data availability — matches now carry data_available, telling you which sub-resources actually hold data for that match: {maps, stats, vetoes, rounds, depth}. Each key is the sub-resource's own path segment, so GET /v1/{game}/matches/{id}/{key} returns rows if and only if that key is true — no more fetching an endpoint to discover it's empty. ?has=rounds,depth filters a list the same way (comma-separated, AND-ed). Coverage is not uniform: round-by-round and per-player depth come from a single upstream, so matches it doesn't cover carry results and little else. The flags describe what is stored now, not a forecast — a scheduled match reads all-false and flips as data lands. /coverage also now declares round_stats and player_depth, two capabilities that were being served but never listed.
  • Roster historyGET /v1/{game}/teams/{id}/roster-history returns the five a team actually fielded in each recent completed match, newest first, with the roster changes between them (players_in/players_out, has_debutant, days_since_prev). Read continuity before interpreting changed_count: a reset is an organisation rename or full rebuild, not five stand-ins, and matches where a team used more than five players across maps are omitted rather than guessed at.
  • Market odds line liveGET /v1/{game}/odds serves the derived eo_market line: a de-vigged aggregate combined from multiple bookmakers and exchanges (published only with ≥2 contributing books; book_count on every line; contributing books are never named).
  • WebSocket ticketsPOST /v1/{game}/ws-token mints a short-lived (60s) signed ticket for the WebSocket handshake (wss://…/v1/ws?token=…), so raw API keys stay out of URLs. The ?apiKey= handshake fallback is deprecated and will be removed.
  • Initial v1 surface for Counter-Strike 2: matches, odds, teams, tournaments, players, rankings, coverage.
  • WebSocket channel for update notifications. Metering is settled: a per-key concurrent-connection cap (5 on the standard plan), one metered request to mint the ticket, and no per-message charge.
  • Interactive API reference generated directly from the OpenAPI spec.

Coming soon

  • Modeled odds — our own proprietary line (eo_model source) is in development, gated behind an accuracy-validation step. It will be documented and served here once that clears, with its track record (calibration + Brier score) published alongside.
  • Additional markets beyond match winner, as their data clears the same quality bar.
  • Additional game namespaces beyond cs2, following the same /v1/{game}/{resource} convention.

On this page