Resolve a name to an id
Going from "NAVI" to a UUID — or skipping the lookup entirely.
You rarely start with a UUID. You start with a name.
Search by name
When you have a name and no slug, search for it with q:
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
"https://api.esportsodds.gg/v1/cs2/teams?q=navi"q is on /teams, /players and /tournaments. It finds your text anywhere in the name and ignores
case, so q=navi finds NAVI and q=cologne finds every Cologne event. What it looks at:
| List | q matches |
|---|---|
/teams | name, short name, slug and alternate names |
/players | nickname and slug (never a real name) |
/tournaments | name and slug |
- Two to 100 characters. One character, or more than 100, is a
400. A blankqis ignored. - Not ranked. Rows come back in the list's usual order (by name for teams, by nickname for players,
newest first for tournaments), so the best match is not always first, and a stem such as
spiritalso finds the academy squads. Pick the row whosename,short_nameorslugis what you meant. - Combined with the other filters, like any filter:
?q=spirit®ion=Europe. Paging works as usual.
from urllib.parse import quote
def find_team(name: str) -> dict | None:
rows = get(f"/v1/cs2/teams?q={quote(name)}")["data"]
wanted = name.lower()
exact = [t for t in rows if wanted in (t["name"].lower(), t["short_name"].lower(), t["slug"])]
return (exact or rows or [None])[0]With the Python client (0.4.0 and later) it is client.teams.list(q="navi"), and the same q works on
players and tournaments. Cache what you find: a team's id does not change.
Skip the lookup
Teams, players and tournaments accept a slug wherever a path id is expected, so if you know the slug you don't need a lookup at all:
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
"https://api.esportsodds.gg/v1/cs2/teams/natus-vincere"
curl -H "Authorization: Bearer $ESPORTSODDS_API_KEY" \
"https://api.esportsodds.gg/v1/cs2/teams/natus-vincere/h2h?opponent=faze"Sub-resources and the ?opponent= parameter accept slugs too. Matches have no slug — a match id
is always a UUID.
When you know the slug
Filter the list by slug and cache the result:
import functools
@functools.lru_cache(maxsize=512)
def team_id(slug: str) -> str | None:
rows = get(f"/v1/cs2/teams?slug={slug}")["data"]
return rows[0]["id"] if rows else None?slug= returns the single matching row — or an empty page — in the normal list envelope, and never
depends on paging position.
Cache aggressively: teams and players change far more slowly than anything else in the API, and a cached id costs you nothing.
Matching many names
Slugs are lowercase and hyphenated, but not always a mechanical transform of the display name
(Natus Vincere → natus-vincere, but FaZe → faze). Guessing works often enough to be
dangerous and fails silently, returning an empty page: search with q instead.
If you're matching a lot of user input, walk the list once and build your own index:
index = {}
for team in paginate("/v1/cs2/teams", limit=500):
index[team["name"].lower()] = team["id"]
index[team["slug"]] = team["id"]
if team["short_name"]:
index.setdefault(team["short_name"].lower(), team["id"])At limit=500 that's a handful of requests for the whole roster, refreshable weekly.
Store the UUID, display the slug
Slugs are stable but not guaranteed permanent — an organisation rename can change one. A team's
old slug keeps resolving to it wherever a team slug is accepted, as long as no other team comes to
hold that slug, and the team you get back carries its current slug. That keeps an old link
working, but it is a courtesy, not an identifier: for anything you persist, store the UUID and treat
the slug as a display convenience. A UUID never changes.