How-to

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:

Listq matches
/teamsname, short name, slug and alternate names
/playersnickname and slug (never a real name)
/tournamentsname and slug
  • Two to 100 characters. One character, or more than 100, is a 400. A blank q is 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 spirit also finds the academy squads. Pick the row whose name, short_name or slug is what you meant.
  • Combined with the other filters, like any filter: ?q=spirit&region=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.

On this page