guide

CS2 API quickstart: your first call in five minutes

Jul 10, 20265 min read

Bottom line: You can go from zero to a live CS2 match feed in about five minutes. Sign up, put your key in an Authorization: Bearer header, and call GET /v1/cs2/matches. Every response is a JSON envelope: a data array or object plus a meta block. The one thing that trips people up first is that list rows and detail rows return different shapes. This guide covers all of it, with runnable code in three languages, against the EsportsOdds CS2 data API.

What you'll build

By the end you'll have made an authenticated request, pulled the Counter-Strike 2 matches list, and read the fields out. It's the foundation every other guide builds on, from a live-scores dashboard to a Discord score bot.

The path from signing up to your first successful response: get a key, add it as a Bearer header, call GET /v1/cs2/matches, and read back a JSON envelope of data plus meta.

Step 1: Get an API key

Create an account on the dashboard and generate a key. Keys look like eo_live_… and are shown once, so store yours somewhere safe. Every example below reads it from an environment variable so you never paste a secret into code:

export ESPORTSODDS_API_KEY="eo_live_your_key_here"

There's a single plan, $99/month for 20,000 requests, and no separate keys to juggle for different endpoints. One key reaches the whole API. The 7-day trial that comes before it carries an allowance of 1,000 requests.

The ratings and odds this key unlocks are computed in-house, not relayed from a third party; the methodology documents how.

Step 2: Make your first request

The base URL is https://api.esportsodds.gg; every path is versioned and game-namespaced, so CS2 lives under /v1/cs2/…. Send your key as a Bearer token:

curl -s https://api.esportsodds.gg/v1/cs2/matches \
  -H "Authorization: Bearer $ESPORTSODDS_API_KEY"

If the key is valid you'll get back a block of JSON. If it isn't, the response is a 401 whose body names which of two things went wrong. No key reached the server:

{ "error": "missing API key" }

A key that's present but unknown, revoked, or paused returns the other:

{ "error": "invalid API key" }

Every error the API returns is this same flat {"error": "…"} shape, never nested, so a client can read resp.json()["error"] on any failure without special-casing it.

Three outcomes: no key returns a 401 with error missing API key; an unknown or revoked key returns a 401 with error invalid API key; a valid key returns 200 with the data envelope.

Two ways to send the key

The Authorization: Bearer header above is the one to use: keys stay out of URLs, proxy logs, and browser history. For a client that can't set headers, the API also accepts the key as an ?apiKey= query parameter (/v1/cs2/matches?apiKey=$ESPORTSODDS_API_KEY), and if a request carries both, the header wins.

The key goes two ways: a preferred Authorization Bearer header that wins when both are present, or an apiKey query-string fallback; either resolves through one hashed lookup.

Step 3: Read the envelope

Here's the part worth slowing down on. A list endpoint wraps its results in an envelope: the rows go in data (up to 100 by default, or as many as ?limit= asks for, to a maximum of 500), and a meta object tells you how many came back and whether there's another page.

{
  "data": [
    {
      "id": "0191f2c8-8a1e-7c3a-9f5e-2b6d4e8a1c00",
      "status": "completed",
      "team_a_name": "Natus Vincere",
      "team_b_name": "FaZe Clan",
      "score_a": 2,
      "score_b": 1,
      "tournament_name": "IEM Cologne 2026",
      "scheduled_at": "2026-07-08T17:00:00Z"
    }
  ],
  "meta": { "count": 1, "next_cursor": null }
}

The response envelope has two parts: a data array holding the item rows, and a meta object carrying count and next_cursor, which is null on the last page.

A single-resource endpoint (like one match by ID) returns {"data": { … }}: the same data key, but an object instead of an array, and no meta. Write your response handling around that split once and every endpoint behaves predictably.

Step 4: The one gotcha: list rows vs detail rows

The matches list you just called is an enriched projection. Each row already carries team_a_name, team_b_name, and tournament_name, so you can render a scoreboard straight from it with no extra lookups.

The single-match detail endpoint is deliberately leaner. GET /v1/cs2/matches/{id} returns a bare match with team_a_id and team_b_id: IDs, not names. If you need the names on a detail view, resolve the IDs against /v1/cs2/teams.

The matches list returns team and tournament names on every row; the single-match detail endpoint returns only IDs, which you resolve against the teams endpoint.

So: reach for the list when you want names, and hydrate IDs from /teams only on detail views. It saves a surprising number of redundant calls.

The same call in Python and JavaScript

Nothing above is curl-specific. In Python with requests:

import os
import requests

BASE = "https://api.esportsodds.gg"
key = os.environ["ESPORTSODDS_API_KEY"]

resp = requests.get(
    f"{BASE}/v1/cs2/matches",
    headers={"Authorization": f"Bearer {key}"},
    timeout=10,
)
resp.raise_for_status()
payload = resp.json()

for match in payload["data"]:
    a, b = match.get("team_a_name"), match.get("team_b_name")
    print(f"{a} vs {b}{match['status']}")

And in JavaScript with fetch:

const BASE = "https://api.esportsodds.gg";
const key = process.env.ESPORTSODDS_API_KEY;

const res = await fetch(`${BASE}/v1/cs2/matches`, {
  headers: { Authorization: `Bearer ${key}` },
});
if (!res.ok) throw new Error(`API error: ${res.status}`);

const { data } = await res.json();
for (const m of data) {
  console.log(`${m.team_a_name} vs ${m.team_b_name}${m.status}`);
}

Both read data the same way, because the envelope is the same everywhere.

Where to go next

You've got a working key and you understand the envelope:

Start with a match list, confirm the shape, and build outward from there.

Full API reference: Your first integration in the developer docs.