guide

"Generate a CS2 match preview (head-to-head, form, rankings)"

Jul 10, 2026

Bottom line: A CS2 match preview is four reads stitched together: resolve both teams' slugs to IDs, then pull their head-to-head record, each side's recent form, and each side's ranking. Each call has one gotcha to know — detail routes need UUIDs, head-to-head requires an opponent parameter, form is a single page, and a team's own rank is opt-in via ?rank=1. Get those four right and the rest is formatting. Built on the EsportsOdds CS2 data API.

The shape of a preview

Two team slugs go in; a preview panel comes out. In between, the work fans out into parallel reads and comes back together.

Assembling a preview fans out from two resolved team IDs into head-to-head, form, and ranking calls, which combine into one preview.

The four calls, and their gotchas

Each of the four reads has a detail worth knowing before you write the code:

The four calls in order, each with its gotcha: resolve slugs to UUIDs, head-to-head needs an opponent parameter, form is not paginated, and own rank is opt-in via rank=1.

Let's build them one at a time.

1. Resolve slugs to IDs. Every detail and derived route works on UUIDs, so start by turning both slugs into IDs via the teams list:

import os
import requests

BASE = "https://api.esportsodds.gg"
HEADERS = {"Authorization": f"Bearer {os.environ['ESPORTSODDS_API_KEY']}"}


def team_id(slug: str) -> str:
    r = requests.get(f"{BASE}/v1/cs2/teams", params={"slug": slug},
                     headers=HEADERS, timeout=10)
    r.raise_for_status()
    rows = r.json()["data"]
    if not rows:
        raise LookupError(f"no team {slug!r}")
    return rows[0]["id"]

2. Head-to-head. GET /teams/{id}/h2h requires an opponent query parameter — omit it and you get a 400. It returns the completed-match record between the two:

def h2h(team: str, opponent: str) -> dict:
    r = requests.get(f"{BASE}/v1/cs2/teams/{team}/h2h",
                     params={"opponent": opponent}, headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["data"]   # {team_wins, opponent_wins, total, ...}

3. Recent form. GET /teams/{id}/form returns a team's recent results as a single page — it's not paginated, so no cursor loop. Each row has won, opponent_id, and the score:

def form(team: str, n: int = 5) -> list[dict]:
    r = requests.get(f"{BASE}/v1/cs2/teams/{team}/form",
                     headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["data"][:n]

4. Ranking. A team's position on our win-rate leaderboard is own_rank, and it's opt-in: request the team detail with ?rank=1 or own_rank comes back null:

def ranking(team: str) -> int | None:
    r = requests.get(f"{BASE}/v1/cs2/teams/{team}",
                     params={"rank": 1}, headers=HEADERS, timeout=10)
    r.raise_for_status()
    return r.json()["data"].get("own_rank")

Assemble the preview

With the four helpers in place, a preview is a matter of calling them and shaping the result:

def preview(slug_a: str, slug_b: str) -> dict:
    a, b = team_id(slug_a), team_id(slug_b)
    record = h2h(a, b)
    return {
        "teams": [slug_a, slug_b],
        "h2h": f"{record['team_wins']}{record['opponent_wins']} "
               f"(over {record['total']} meetings)",
        "form": {
            slug_a: ["W" if m["won"] else "L" for m in form(a)],
            slug_b: ["W" if m["won"] else "L" for m in form(b)],
        },
        "rank": {slug_a: ranking(a), slug_b: ranking(b)},
    }


p = preview("natus-vincere", "faze")
print(f"H2H: {p['h2h']}")
print(f"{p['teams'][0]}: form {''.join(p['form'][p['teams'][0]])}  rank #{p['rank'][p['teams'][0]]}")

The result is a compact, factual panel — the record between the teams, each side's recent W/L run, and where they sit in the rankings:

The assembled preview card: the head-to-head record, each team's recent form, and their rankings, shown as text.

Keep the framing neutral. A preview reports what happened — the record, the form, the standings — and lets the reader draw conclusions. It's context, not a tip.

Making it fast

The four reads for one team don't depend on each other, so run them concurrently. With asyncio and httpx, or a simple ThreadPoolExecutor around the requests calls, you can cut the wall-clock time to roughly a single request. If you're generating many previews in a batch, wrap the calls in the resilient client from the rate-limits guide so a burst doesn't trip the per-second limit.

Next steps

Resolve, fetch four ways, assemble. Mind the opponent parameter and the opt-in rank, and the endpoint reference covers the rest.