Matches

Matches with their sub-resources embedded

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".

GET
/v1/{game}/matches/bulk

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".

Authorization

AuthorizationBearer <token>

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*string

Game slug. cs2 is the only populated title today; an un-onboarded game 404s cleanly.

Query Parameters

include?string

Comma-separated sub-resources to embed on every match: maps, stats, vetoes, rounds, depth, lineups, streams, players. Each one requested is always present — an empty array (or, for depth, five empty arrays) when the match has none — and each one not requested is absent. Unknown names are ignored; meta.includes echoes the ones that were understood, so a typo is visible.

status?string

Filter by match status. completed is the one a historical pull wants.

Value in

  • "scheduled"
  • "live"
  • "completed"
  • "cancelled"
tournament?string

Filter by tournament (UUID).

Formatuuid
team?string

Filter by team (UUID).

Formatuuid
date_from?string

Only matches scheduled on or after this day (YYYY-MM-DD, UTC, inclusive).

Formatdate
date_to?string

Only matches scheduled on or before this day (YYYY-MM-DD, UTC, inclusive of the whole day).

Formatdate
has?string

Keep only matches that carry the named sub-resources (comma-separated, AND-ed; the same keys as data_available). has=rounds,depth skips the matches that would embed empty arrays, so every request you spend returns data.

changed_since?string

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.

Formatdate-time
sort?string

Ordering. -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.

limit?integer

Matches per page. Default and maximum are both 25; a larger value is clamped and meta.max_limit says so.

Range1 <= value <= 25
Default25
cursor?string

Opaque 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

application/json

curl -X GET "https://example.com/v1/cs2/matches/bulk"
{  "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      },      "maps": [        {          "id": "01a0b16f-3db8-730c-b5a9-b71b7d6a057d",          "match_id": "01a0a3e2-96a5-7a9f-8af2-fae0f1a7d678",          "map_number": 1,          "map_name": "de_ancient",          "score_a": 13,          "score_b": 7,          "first_half_a": 9,          "first_half_b": 3,          "second_half_a": 4,          "second_half_b": 4,          "overtime_a": null,          "overtime_b": null,          "first_half_side_a": "ct",          "status": "played",          "rounds_played": 20,          "created_at": "2026-09-17T22:14:36.269238Z",          "updated_at": "2026-09-18T09:47:13.853948Z"        },        {          "id": "01a0b2b1-b3f2-77ca-b5aa-cfad1447e816",          "match_id": "01a0a3e2-96a5-7a9f-8af2-fae0f1a7d678",          "map_number": 2,          "map_name": "de_mirage",          "score_a": 13,          "score_b": 2,          "first_half_a": 10,          "first_half_b": 2,          "second_half_a": 3,          "second_half_b": 0,          "overtime_a": null,          "overtime_b": null,          "first_half_side_a": "t",          "status": "played",          "rounds_played": 15,          "created_at": "2026-09-18T04:06:50.042061Z",          "updated_at": "2026-09-18T09:47:13.853948Z"        }      ],      "lineups": [        {          "id": "01a0a6a9-4c97-7277-a184-d9a9089da520",          "match_id": "01a0a3e2-96a5-7a9f-8af2-fae0f1a7d678",          "team_id": "019f2858-e255-78e4-9e7c-32f307dfac91",          "player_ids": [            "019f28e2-22cb-74ea-af0f-0408f360cec2",            "019f28e2-22cb-7de3-b94b-7652094f468c",            "019f28e2-76af-7029-97b5-70686851cd14",            "019f28e2-76af-707f-95ed-e47bae9e065f",            "019f28e2-76af-70fb-96fe-9452dbabbdf5",            "019f28e2-76af-7611-8aea-7794acc0c0bc",            "019f2b8f-1e66-78cf-bc47-73b061cf0337"          ],          "captured_at": "2026-09-15T20:00:31.524108Z"        },        {          "id": "01a0a6a9-4c97-7dad-ab66-a95f39035e05",          "match_id": "01a0a3e2-96a5-7a9f-8af2-fae0f1a7d678",          "team_id": "01a0a3e2-96a0-7bf7-986c-6a68e151de5c",          "player_ids": [            "019f28e1-e6af-7c9e-9dbf-9f83509caa47",            "019f2bda-1afe-7645-b9dd-b05c9a89b19e",            "01a0a3e2-96a0-7b37-8172-f2fae5cb4679",            "01a0a583-31b0-7603-9de3-48bbbae8b4f2",            "01a0a58a-0fb1-7520-b152-6649c8738d94"          ],          "captured_at": "2026-09-15T20:00:31.524108Z"        }      ]    }  ],  "meta": {    "count": 1,    "next_cursor": "eyJ2IjoiMjAyNi0wOS0xOFQwMjoyNTowMFoiLCJpZCI6IjAxYTBhM2UyLTk2YTUtN2E5Zi04YWYyLWZhZTBmMWE3ZDY3OCJ9",    "max_limit": 25,    "includes": [      "maps",      "lineups"    ]  }}
{  "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"  }}

{  "error": {    "code": "not_found",    "message": "not found",    "request_id": "019f3d18-c15f-7319-81a7-343e8a80a578"  }}