List matches for a game
Matches for the game, newest scheduled first. An unknown `{game}` 404s cleanly. Listing hygiene: by default, `cancelled` fixtures and `scheduled` rows more than 6 hours past their slot are excluded (a cancelled match is a known non-event; a long-overdue scheduled row means the upstream fixture no longer exists as listed). Pass `?status=cancelled` to list cancelled fixtures explicitly; `?status=scheduled` is the upcoming list and keeps the 6-hour exclusion. Any match remains fetchable by id regardless of status.
Matches for the game, newest scheduled first. An unknown {game} 404s cleanly. Listing hygiene: by default, cancelled fixtures and scheduled rows more than 6 hours past their slot are excluded (a cancelled match is a known non-event; a long-overdue scheduled row means the upstream fixture no longer exists as listed). Pass ?status=cancelled to list cancelled fixtures explicitly; ?status=scheduled is the upcoming list and keeps the 6-hour exclusion. Any match remains fetchable by id regardless of status.
Primary auth (live). Authorization: Bearer <api-key> — the header takes precedence over the query fallback. The raw key is hashed (SHA-256, hex) and looked up in api.keys.key_hash; the raw value is never stored.
In: header
Path Parameters
Game slug. cs2 is the only populated title today; an un-onboarded game 404s cleanly.
Query Parameters
Filter by match status. scheduled excludes rows more than 6 hours past their slot; cancelled is the explicit way to list called-off fixtures.
Value in
- "scheduled"
- "live"
- "completed"
- "cancelled"
Filter by tournament (UUID).
uuidFilter by team (UUID).
uuidOnly matches scheduled on or after this day, as YYYY-MM-DD (e.g. 2026-07-01). Compared against scheduled_at in UTC and inclusive of the whole day. A malformed value is a 400, not a silent no-op.
dateOnly matches scheduled on or before this day, as YYYY-MM-DD. Inclusive — date_to=2026-07-01 includes everything on 1 July, not just midnight.
dateKeep only matches that actually carry the named sub-resources. Comma-separated and AND-ed, e.g. has=rounds,depth returns only matches with BOTH round data and the per-player depth layer. Values are the same keys as data_available. An unknown name is a 400, never a silently-ignored filter.
has=map_stats is narrower than has=stats on the same endpoint: it keeps only matches whose /stats carries the per-map rows, not just the whole-match aggregate.
has=odds is the one value that depends on your access rather than only on what is stored: it keeps matches for which we hold at least one odds line you can retrieve, so it never returns a match whose /odds would come back empty for you.
Only matches whose changed_at is at or after this instant (RFC 3339, inclusive) — the change feed. Pair it with sort=changed_at and page to the end, then store the newest changed_at you saw and pass it back next time.
Three things to know. It sees the match's own fields — status, scores, winner, times, stage, format, teams and tournament — and the first arrival of each sub-resource: the instant data_available gains a true, because maps, stats, per-map stats, vetoes, rounds, depth or lineups landed for the first time. Later corrections to those rows do not move it again; only the transition from "we hold none" to "we hold some" does. It includes cancelled and long-overdue rows that the default list hides, because a cancellation is precisely a change you need to hear about. And ingestion commits in batches, so a row can become visible a few minutes after the changed_at it carries: overlap your window by 10 minutes and de-duplicate on id rather than resuming from the exact last value. changed_at starts on 2026-09-18; rows untouched since then all carry that date. A malformed value is a 400.
date-timeOrdering. -scheduled_at (default, newest fixture first), scheduled_at, changed_at or -changed_at. An unrecognised key returns 400 rather than being ignored. A cursor is bound to the sort it was minted under, so change sort and start again from the first page.
Page size. The default and maximum vary per endpoint — this shared definition documents the common case (default 100, max 500); each operation that differs states its own numbers. cursor carries the page position, not limit.
A value above the endpoint's maximum is silently clamped, not rejected. A missing, zero, negative or non-numeric value silently falls back to the default — so ?limit=abc and ?limit=0 both return the default page size rather than a 400. Read meta.count if you need to know how many rows you actually got.
1 <= value100Opaque forward pagination cursor — pass the previous page's meta.next_cursor verbatim. Omit for the first page. Never construct one by hand.
Response Body
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/v1/cs2/matches"{ "data": [ { "id": "01a0a3e2-96a5-7a9f-8af2-fae0f1a7d678", "game_id": "019f23d1-fb5b-7d24-935d-e4d68f6f565a", "tournament_id": "01a09164-2d84-7c6e-9d80-a42c8c70ce12", "team_a_id": "019f2858-e255-78e4-9e7c-32f307dfac91", "team_b_id": "01a0a3e2-96a0-7bf7-986c-6a68e151de5c", "format": "bo3", "status": "completed", "stage": "Circuit X Curitiba 3 Group D", "scheduled_at": "2026-09-18T02:25:00Z", "started_at": "2026-09-17T19:56:50.826573Z", "ended_at": "2026-09-18T03:44:28Z", "score_a": 2, "score_b": 0, "winner_team_id": "019f2858-e255-78e4-9e7c-32f307dfac91", "result_type": "played", "live_state": null, "created_at": "2026-09-15T07:05:50.158384Z", "updated_at": "2026-09-18T10:59:10.806164Z", "changed_at": "2026-09-18T03:49:12.004518Z", "tournament_tier": "B", "team_a_name": "BESTIA", "team_a_short": "BST", "team_b_name": "CLANSBR", "team_b_short": null, "tournament_name": "Circuit X Curitiba 3", "data_available": { "maps": true, "stats": true, "map_stats": true, "vetoes": true, "rounds": true, "depth": true, "lineups": true, "odds": false } } ], "meta": { "count": 1, "next_cursor": "eyJ2IjoiMjAyNi0wOS0xOFQwMjoyNTowMFoiLCJpZCI6IjAxYTBhM2UyLTk2YTUtN2E5Zi04YWYyLWZhZTBmMWE3ZDY3OCJ9" }}{ "error": { "code": "not_found", "message": "not found", "request_id": "019f3d18-c15f-7319-81a7-343e8a80a578" }}{ "error": { "code": "not_found", "message": "not found", "request_id": "019f3d18-c15f-7319-81a7-343e8a80a578" }}{ "error": { "code": "not_found", "message": "not found", "request_id": "019f3d18-c15f-7319-81a7-343e8a80a578" }}{ "error": { "code": "rate_limited", "message": "rate limit exceeded", "request_id": "019f3d18-c15f-7319-81a7-343e8a80a578" }}Matches with their sub-resources embedded GET
The match list with each match's data embedded, so a historical pull costs one request per **page of 25 matches** instead of up to seven requests per match. It takes exactly the filters, ordering and cursors of `GET /v1/{game}/matches` — including `changed_since` and `sort=changed_at` — and each embedded array is byte-for-byte what the matching per-match route returns. **One request, whatever it carries.** A bulk page counts as a single request against your monthly allowance. During a free trial this endpoint has its own ceiling of 20 requests (a 500-match sample); see the rate-limits guide. **Page size is capped at 25** and reported back as `meta.max_limit`, so a larger `limit` is visibly clamped rather than silently ignored. A full page of recent matches with everything included is around 7 MB before compression and can reach about 10 MB — send `Accept-Encoding: gzip` (most HTTP clients do; with curl pass `--compressed`) and it travels at roughly a tenth of that. **At most two bulk pages are built at once, server-wide.** If both slots are taken you get `503` with `Retry-After: 2` and the code `unavailable`. That refusal is not counted against your quota. Page sequentially, honour `Retry-After`, and you will rarely see it. Ask only for what you need: an `include` you omit is absent from the response and costs nothing to build. With no `include` at all this is the plain match list at a smaller page size. Check `data_available` on each row to tell "this match has no rounds on record" from "you did not ask for rounds".
Data & model coverage matrix (honest reliability) GET
The capability × status matrix behind the dashboard Coverage page and the docs coverage doc: per capability, its status and cadence. `docs/data-sourcing/reconciliation-policy.md`.