CS2 data reference

Player match stats

The per-player stat line — which fields are always present, which are nullable, and the map_number trap.

GET /v1/{game}/matches/{id}/stats returns a whole-match aggregate row per player, plus — where we have them — one row per player per map. Read the next section before you depend on the per-map rows: they are not present on every match, and data_available.map_stats tells you which is which.

Check per-map coverage before you rely on it

The aggregate rows are there for every match we have stats for. The per-map rows are not yet there for every match, and a match with only aggregate rows is indistinguishable from a full one until you look.

data_available.map_stats on the match tells you in advance, and it is narrower than data_available.stats on purpose — both describe this same endpoint:

flagswhat you get from /stats
stats: falsenothing; the endpoint returns an empty list
stats: true, map_stats: falsethe whole-match aggregate rows only
stats: true, map_stats: trueaggregate rows and one row per player per map
match = get(f"/v1/cs2/matches/{match_id}")["data"]
if not match["data_available"]["map_stats"]:
    ...  # aggregate only — don't build a per-map view from this match

To page only over matches that carry the per-map layer, filter the list with ?has=map_stats:

GET /v1/cs2/matches?status=completed&has=map_stats

Coverage is improving

Per-map rows land automatically on matches as they finish. Older matches are being filled in continuously, so a match that reads map_stats: false today may well carry per-map rows later — re-check the flag rather than caching the answer.

Maps that were never played out carry no per-map rows at all, and that is correct rather than missing: see status on Map results for forfeits, abandons and walkovers.

The map_number trap

map_number is null on the whole-match aggregate row. Every player therefore appears maps + 1 times: once per map, once in total.

rows = get(f"/v1/cs2/matches/{match_id}/stats")["data"]

per_map   = [r for r in rows if r["map_number"] is not None]
aggregate = [r for r in rows if r["map_number"] is None]

Summing all rows double-counts every player. This is the single most common mistake against this endpoint.

Always present

kills is net of team kills — a kill on a teammate subtracts one, which is how upstream counts it. The duel matrix is the only place the two are separable, via its kind field.

kills, assists, deaths, adr and rating are on every row. rating is our own computed rating, not a third party's — don't compare it numerically against a rating from elsewhere.

Nullable

The richer detail comes from a source that doesn't cover every match, so these are null rather than 0 where unavailable — an honest absence, not a claim that nothing happened:

kast, headshots, first_kills, first_deaths, trade_kills, trade_deaths, clutches, multikills_2k/3k/4k/5k, damage, utility_value.

KAST units differ by endpoint

kast here is a fraction between 0 and 1 — multiply by 100 for the percentage usually shown. On a round row it is a count of 0–5 players, and on a team profile the *_pct fields are already 0–100. Three different units for related ideas; check which endpoint you're reading.

team_id

team_id is the team the player played for in this match, which is not always their current team — rosters change, and stand-ins happen. It is nullable where attribution couldn't be resolved. Use it rather than the player's own team_id when attributing historical performance.

Field reference

Prop

Type

For what ADR, KAST and rating mean as concepts, the marketing site has explainers at /learn/cs2-adr and /learn/cs2-kast. This page documents the fields as served.

On this page