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
}| Field | Meaning |
|---|---|
changed_at | The 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. |
changed | The 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_completed | Whether 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 finaldata_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
| Parameter | Default | Meaning |
|---|---|---|
limit | 50 | Maximum entries to return. Capped at 200. |
before | — | RFC 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.
Related
- Match lifecycle — how a match moves between states.
- Recovering from disconnects — what to do after a gap.
- Limitations — what the platform does not do.