Broadcast streams
Where a match in progress is being shown — channel, language and embed URL. A live pointer, not a history.
GET /v1/{game}/matches/{id}/streams returns the channels broadcasting a match right now. The
same rows embed via include=streams on GET /v1/{game}/matches/{id} and on the
bulk endpoint — which is how you ask "what is on air?" in one
request instead of one per match.
This is a live pointer, not a record. A match's whole channel list is replaced on every
refresh, and it empties when the match ends. An empty data: [] is the normal, correct answer
for every match that is not currently being played — it does not mean the match was never
streamed.
The shape
| Field | Meaning |
|---|---|
id | The row's id (UUIDv7). It changes when the set is replaced — don't store it as a stable key for a channel. |
match_id | The match being broadcast. |
platform | twitch, kick, youtube or other, derived from the URL's host. |
channel | The channel's display name (falls back to the last path segment of the URL). |
url | The page a viewer would open. Unique per match. |
embed_url | A player URL suitable for an <iframe>, when one is published. Absent otherwise. |
language | Lowercase ISO 639-1 where stated (en, pt, ru, …). Absent otherwise. |
official | Whether this is the organiser's own broadcast rather than a co-stream. |
viewers | Concurrent viewers as last observed, or null when not reported. Always a point-in-time number — see below. |
observed_at | When we last saw this channel on this match (RFC 3339, UTC). |
Rows come back official first, then by viewers descending (null last), then by url — so
data[0] is the broadcast most people are watching.
Freshness
An in-progress match's channel list is re-read on each collection pass — in practice every few
minutes. viewers carries the same freshness, so read it as "roughly this many, a few minutes ago":
it is useful for ranking co-streams against each other, not as a counter. observed_at is the
honest timestamp for both, and it is per row.
Channels are picked up once a match is in progress, not while it is still scheduled. A match whose start time has passed but that has not gone live yet will usually have an empty list.
Getting the live slate in one request
include= is accepted on the match detail route and on /matches/bulk — not on the plain
/matches list. Bulk takes the same filters, so one call gets the whole live slate with its
channels:
live = get("/v1/cs2/matches/bulk", params={"status": "live", "include": "streams"})["data"]
for m in live:
if not m["streams"]:
continue
top = m["streams"][0]
print(f'{m["team_a_name"]} vs {m["team_b_name"]}: {top["channel"]} ({top.get("language", "?")})')include=streams is present-but-empty when you ask for it and the match has none, and absent when
you don't — so "streams" in m tells you whether you asked, and m["streams"] == [] tells you
nothing is on air.
What it is not
- Not a VOD or highlight archive. Nothing is kept after the broadcast ends. If you need the history of who streamed what, snapshot it yourself while the match is live.
- Not in
data_availableor?has=. Every flag there is monotonic — once a match has stats it always has stats — and streams are not: they appear and then go away. Filter live matches with?status=liveand read the list. - Not an endorsement, and not a paid placement. We receive nothing for listing them; the URLs are the public channel pages the tournament itself points at.