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:
kind | Meaning |
|---|---|
enemy | An ordinary duel against the opposition. |
team | A kill on a teammate. |
self | A 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 withIt 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_names | Actually |
|---|---|
elite | Dual Berettas |
deagle | Desert Eagle |
glock | Glock-18 |
hkp2000 | P2000 |
cz75a | CZ75 Auto |
usp_silencer, usp_silencer_off | USP-S |
m4a1_silencer | M4A1 |
taser | Zeus x27 |
sg556, sg-556 | SG 553, under an older name |
knife_gypsy_jackknife | A knife |
World | Not 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:
headshotsdoes not followkills's netting one-for-one, so a weapon row can report more headshots than kills — 5,948 rows do, 741 of them with a negativekills. A per-rowheadshots / killsis 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 positivekills; 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 positivedamage. For "weapons this player used", filter onshots > 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.