Core concepts

Match lifecycle

What populates when, from scheduled through live to completed.

A match moves through three states, and which fields are trustworthy depends on where it is.

The states

statusMeaningScoreswinner_team_id
scheduledNot startednullnull
liveIn progressPartialnull
completedFinishedFinalSet, once the result is ingested
cancelledCalled off — will not be playednullnull

A cancelled match keeps its id and any odds history captured while it was scheduled, and stays fetchable by id — but it never appears in upcoming listings. There is no postponed state: a rescheduled fixture keeps scheduled and its scheduled_at moves.

scheduled_at is the planned start. There is no separate actual-start field, so a match that started late still shows its scheduled time. Treat it as the fixture's slot, not as evidence of when play began.

What appears when

Fixtures are created ahead of time with teams, tournament and format. Odds start being captured as soon as a market opens — often days before. Everything else lands as the match plays out or shortly after:

  • Map results appear per map as each finishes.
  • Round stats and player stats arrive with the map they belong to.
  • Vetoes are known before play starts, since the pick/ban happens first.
  • Depth (weapons, grenades, hitgroups, duels, flashes) is the last and least universal layer.

A completed match therefore does not guarantee every sub-resource exists.

Walkovers: empty is the right answer

Some completed matches were never played. A team forfeits, the fixture is awarded to their opponent, and there is genuinely nothing to collect — no maps, no rounds, no player stats. result_type tells you which kind of result you are looking at:

result_typeMeaning
playedThe match was played.
walkoverAwarded on a forfeit. No map was played, so the empty sub-resources are correct, not missing.
nullWe were not told. Do not infer it from the score.

Check it before reporting a completed match as a coverage gap. Within a played match, an individual map can also be forfeited or abandoned — that is status on the map result, not this field.

null is deliberate rather than lazy. A genuine 1–0 best-of-one whose map detail simply has not arrived yet looks identical to a forfeit by score alone, so we leave the field unset rather than assert something we cannot see.

Don't guess — read data_available

Every match row carries flags for exactly which sub-resources have data:

"data_available": {
  "maps": true, "stats": true, "map_stats": false,
  "vetoes": true, "rounds": true, "depth": false, "odds": true
}

stats and map_stats both describe the /stats endpoint: stats is true when it returns anything at all, map_stats only when it also returns the per-map rows rather than just the whole-match aggregate. See player match stats.

Use them instead of speculatively fetching five endpoints and discarding empty responses. Each avoided request is one you keep in your monthly quota.

You can also filter the list to matches that have what you need:

# Only completed matches with round-level AND per-player depth data
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
  "https://api.esportsodds.gg/v1/cs2/matches?status=completed&has=rounds,depth&limit=20"

has= accepts any comma-separated subset of maps,stats,map_stats,vetoes,rounds,depth,lineups,odds, AND-ed together. An unknown value is a 400 rather than a silently ignored filter.

Settled odds

Once a match settles, its odds lines gain is_winner (which outcome came in) and one line per outcome is flagged is_closing — the last price captured before the match began (its started_at when we hold one, its scheduled_at when we don't). The closing line is the standard benchmark for evaluating a forecast, so if you are scoring predictions, that's the row you want. Every line also carries in_play, which separates pre-match prices from live ones. See odds movement.

How fast it changes

Measured over a recent 7-day window, the whole fixture corpus takes about 6 writes an hour — matches simply don't change often. Odds move roughly 110× faster. Polling both on the same timer is the single most common way to burn a monthly quota; the arithmetic is in how often to poll.

On this page