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
opponentparameter, 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.
The four calls, and their gotchas
Each of the four reads has a detail worth knowing before you write the code:
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:
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
- Add fantasy projections — layer per-player numbers onto the team preview.
- Show it on a dashboard — render the preview alongside the live score.
Resolve, fetch four ways, assemble. Mind the opponent parameter and the opt-in rank, and the endpoint reference covers the rest.