CS2 data reference

Match history

What a match used to say, and when it stopped saying it — the prior values behind every change.

GET /v1/{game}/matches/{id}/history returns the changes made to a match's own fields, newest first, with the before and after for each one.

Everywhere else in this API tells you that something changed. ?changed_since= gives you the matches that moved; the WebSocket's changed field names the fields that moved. Neither can tell you what they moved from, because every other record is updated in place. This endpoint is the one that keeps the old value.

That difference matters when you are reconciling a copy of your own. If your stored copy says a series finished 2-1 and ours now says 2-0, the change feed cannot tell you whether we corrected a published result or simply filled in a score we never had. Here, it is explicit.

A row

{
  "changed_at": "2026-09-21T19:42:07.318402Z",
  "changed": ["status", "score_a", "winner_team_id"],
  "changes": {
    "status": { "from": "live", "to": "completed" },
    "score_a": { "from": 1, "to": 2 },
    "winner_team_id": { "from": null, "to": "019f3d18-c15f-7319-81a7-343e8a80a578" }
  },
  "was_completed": false
}
FieldMeaning
changed_atThe instant the match moved. The same value changed_at held on the match at the time, so it lines up exactly with ?changed_since= and with the WebSocket.
changedThe fields that moved, in the same vocabulary the WebSocket's changed field uses.
changes{from, to} per field. from is an explicit null when the field had no value — not an omitted key.
was_completedWhether the match was already completed before this change. This is the line between a correction and ordinary progress.

was_completed is the field to filter on

A score climbing while a series is being played is not a correction — nobody was told it was final. A change to a match that was already completed is a different thing: we published a result and then revised it.

was_completed: false  →  lifecycle progress (scheduled → live → completed, scores filling in)
was_completed: true   →  we revised something we had already published as final

data_available entries

An entry whose changed is ["data_available"] and whose changes is {} means the match's own fields did not move — a sub-resource landed for the first time (maps, player stats, round stats, player depth or lineups). The match reappears in ?changed_since= for the same reason.

These are arrivals, not corrections. An empty changes is how you exclude them.

Parameters

ParameterDefaultMeaning
limit50Maximum entries to return. Capped at 200.
beforeRFC 3339 timestamp. Returns only entries strictly earlier than this.

Not cursor-paginated: next_cursor is always null. To walk further back, pass the changed_at of the last entry you received as before. It is exclusive, so you get the next page with no overlap and no gap.

curl -H "X-API-Key: $EO_API_KEY" \
  "https://api.esportsodds.gg/v1/cs2/matches/019f3d18-c15f-7319-81a7-343e8a80a578/history?limit=20"

Which fields are tracked

The match's own observable fields — the thirteen that move changed_at:

status · score_a · score_b · winner_team_id · scheduled_at · started_at · ended_at · stage · format · result_type · team_a_id · team_b_id · tournament_id

Changes inside a sub-resource are not tracked here. A corrected round stat or an adjusted map score does not produce an entry — only the first arrival of that layer does, as a data_available entry.

Nor is there another signal for one. Map-level and round-level records do carry their own updated_at, but it is a write stamp, not a change stamp: it advances every time a refresh pass touches the row, whether or not a value moved. So it cannot tell a correction from a no-op, in exactly the way updated_at can't on a match. If a sub-resource correction also changes something about the match — a map score that moves the series score, say — that lands here. One that doesn't is currently invisible, and re-fetching is the only way to find it.

History starts when the feature shipped

There is no backfill, and none is possible: before this existed, a prior value was overwritten in place and is simply gone. A match that finished before the ship date returns an empty list — which means "we have no record of changes", not "nothing changed". See the changelog for the date.

Retention

Entries are kept for 180 days and then removed. A match older than that returns an empty list even if it was corrected at the time.

Ids are as recorded

A winner_team_id, team_a_id or tournament_id inside changes is the value we published at that instant. If that team or tournament was later merged into another record, the id here still names the one we served then, and it may no longer resolve. Rewriting it would falsify the record, so we don't.

What this is not

  • Not a replay log. It covers the match's own fields, not odds movement — use odds history for prices — and not the contents of sub-resources.
  • Not an audit of why. It records what changed, not the reason or the trigger.
  • Not a substitute for the change feed. To find which matches moved, use ?changed_since= on the match list; come here for one match's detail.

On this page