CS2 data reference

Player depth

Weapons, grenades, hitgroups, duels and flashes — the deepest layer, and the least universal.

GET /v1/{game}/matches/{id}/depth returns a single object containing five arrays. Each is [] when unavailable, never null.

This is the least universally available layer — check data_available.depth on the match, or filter the match list with ?has=depth, before requesting it.

weapons

One row per player per weapon per side they held it on, so a player who used an AK-47 on both sides appears twice.

weapon_slug (ak-47) is the stable key to group on; weapon_name (AK-47) is for display; weapon_class buckets it (Rifle, Pistols, SMG, Heavy, Grenade, Equipment, or an empty string where the source didn't classify it).

Counters: shots, hits, kills, headshots, damage, wall_bangs, trade_kills. hits/shots is an accuracy rate — guard the zero denominator, since a weapon can be equipped without being fired.

grenades

One row per player per grenade type per side: throws, hits, kills, owns. grenade_name is one of Flashbang, Smoke Grenade, HE Grenade, Molotov, Incendiary Grenade or Decoy Grenade.

hitgroups

Where shots landed: hit_group — one of Head, Chest, Stomach, LeftArm, RightArm, LeftLeg, RightLeg, Gear or Generic — with hits, damage and kills whose finishing shot landed there.

duels

One row per ordered (killer, victim) pair, match-level. A mutual rivalry is two rows — swap the ids to find the reverse direction.

kills is how many times the killer killed the victim. weapon_names is the ordered list of weapons behind those kills — see the weapon sequence below.

kind says what the pair is, and the vocabulary is closed:

kindMeaning
enemyAn ordinary duel against the opposition.
teamA kill on a teammate.
selfA self-inflicted death — world damage, own grenade, a fall. killer_player_id and victim_player_id are the same player.

Filter on kind before you total anything. kills is a magnitude for all three kinds, so summing a player's rows without filtering to enemy counts their team kills and their own deaths as kills on the opposition.

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

kills_on_enemies = sum(d["kills"] for d in depth["duels"] if d["kind"] == "enemy")
team_kills       = sum(d["kills"] for d in depth["duels"] if d["kind"] == "team")

The weapon sequence

weapon_names is one entry per kill, oldest first, joined with ", ". Split on the comma and trim:

seq = [w.strip() for w in duel["weapon_names"].split(",")]
opener = seq[0]          # what they first killed this opponent with

It is not a kill feed. There are no timestamps on a duel row, and — like everything else on this endpoint — no map or round attribution, so the sequence runs across the whole match with no way to place any entry in it. Relative order is the entire signal.

One entry per kill holds for 99.97% of rows, over a corpus of about 1.12 million. Where it doesn't, the list is longer than kills, and the disagreements are concentrated in 16 matches out of the 22,000-odd that carry duel data. Treat kills as the count and the sequence as the ordering; don't derive either from the other.

The spellings are not normalised

Three vocabularies coexist in this one field: display names (AK-47), slugs (ak-47) and raw engine names (ak47, m4a1_silencer). Non-display spellings are about 1.4% of entries — few, but spread widely enough to touch roughly 6% of duel rows. Lower-casing and stripping punctuation folds most of them onto the weapon_name used by the weapons rows. Thirteen values don't fold, so a raw string join silently drops those kills:

In weapon_namesActually
eliteDual Berettas
deagleDesert Eagle
glockGlock-18
hkp2000P2000
cz75aCZ75 Auto
usp_silencer, usp_silencer_offUSP-S
m4a1_silencerM4A1
taserZeus x27
sg556, sg-556SG 553, under an older name
knife_gypsy_jackknifeA knife
WorldNot a weapon — the environment

World is a fall, world damage or the bomb, and it lands almost entirely where you'd expect: all but a handful of the rows carrying it are kind: "self". Build your own lookup rather than switching on the raw string, and expect a spelling you haven't seen — this field is an open vocabulary, the same as end_reason.

Kill counts elsewhere are net of team kills

This matrix is the only place a team kill is separable. Everywhere else a kill count is the net figure: a team kill subtracts one. That holds for kills on match stats and for kills on the weapon rows above, which is why a weapon row can carry a negative kills — a player whose only molotov kill was a teammate has kills: -1 for molotov. Around 3,400 weapon rows across the corpus are negative this way, most of them grenades.

Don't clamp those to zero. The negative is what keeps a player's weapon rows summing to their match kills, and the duel matrix is where you go to recover the team-kill count itself. damage nets the same way, for the same reason — about 630 weapon rows are negative on damage.

Two consequences worth coding around:

  • headshots does not follow kills's netting one-for-one, so a weapon row can report more headshots than kills — 5,948 rows do, 741 of them with a negative kills. A per-row headshots / kills is not a rate. Sum a player's weapon rows first, or take the headshot percentage from match stats.
  • Don't filter a weapon list on kills > 0. Only 1.97M of the corpus's 5.05M weapon rows have a positive kills; 3.08M sit at exactly zero and 3,428 are negative. Those are still weapons the player used — all but 15 rows in the whole corpus record at least one shot, and 2,685 of the negative rows carry positive damage. For "weapons this player used", filter on shots > 0 OR damage <> 0 OR kills <> 0.

flashes

One row per ordered (flasher, flashed) pair. Self-flashes are real rows where flasher_player_id == flashed_player_id — they are recorded, not filtered, and dropping them is a decision you should make deliberately.

count is how many times; duration_ns is total blind time in nanoseconds — divide by 1e9 for seconds. It's an int64 because a match total exceeds 32-bit range.

count counts blinds on an opponent only. Blinding a teammate is recorded with count: 0 and a real duration_ns, so a row with no count is not an empty row. Corpus-wide, 99.8% of same-team rows carry count: 0 against 0.05% of opposing rows, and every zero-count row has a non-zero duration. Use duration_ns when you want blind time inflicted on anyone, and count when you want blinds that cost the opposition.

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

for f in depth["flashes"]:
    if f["flasher_player_id"] == f["flashed_player_id"]:
        continue  # self-flash
    print(f"{f['flasher_player_id']} blinded {f['flashed_player_id']} "
          f"{f['count']}× for {f['duration_ns'] / 1e9:.1f}s")

Everything here is match-level

None of the five arrays carry map_number — the upstream exposes them per match, not per map. If you need per-map weapon usage, that isn't available.

On this page