Display a league table
Standings work in two steps that are easy to confuse: one endpoint returns a catalogue of tables that exist, another returns the rows of one table. A single group stage typically has eight different tables, and only one of them is the league table you probably want.
/v2/standings → catalogue: which tables exist for this scope/v2/standings/{id} → rows: the actual table/v2/standings-types → which columns can appear, and in what order1. Find the scope
Section titled “1. Find the scope”A standings table is attached to an object: a sport, a competition, a season, a stage or an event. For a league table you almost always want a stage, so start by walking down to one.
# competition → season → stagecurl "https://api.statscore.com/v2/seasons?token=YOUR_TOKEN&competition_id=13245&limit=5"curl "https://api.statscore.com/v2/stages?token=YOUR_TOKEN&season_id=71198&limit=5"Two flags on a stage decide what to do next, and they are not interchangeable:
| Flag | Meaning | Next call |
|---|---|---|
show_standings: "yes" | this stage has tables | /v2/standings (this recipe) |
has_brackets: "yes" | this stage is a knockout tree | /v2/stages/{id}/bracket |
2. List the tables that exist
Section titled “2. List the tables that exist”curl "https://api.statscore.com/v2/standings?token=YOUR_TOKEN&object_type=stage&object_id=153098"This returns the catalogue, not the rows:
{ "api": { "data": { "standings_list": [ { "id": 219301, "type_name": "League standings", "subtype": "standings" }, { "id": 219302, "type_name": "Form table", "subtype": "form" }, { "id": 219303, "type_name": "Halftime standings", "subtype": "standings" }, { "id": 219304, "type_name": "Most popular results", "subtype": "standings" }, { "id": 219305, "type_name": "Overall stats", "subtype": "overall_stats" }, { "id": 219306, "type_name": "Second half standings", "subtype": "standings" }, { "id": 219307, "type_name": "Streaks", "subtype": "standings" }, { "id": 219308, "type_name": "Team overall stats", "subtype": "overall_stats" } ] } }}Eight tables for one group stage. Select by type_name - for a normal league table that is
"League standings". Do not select by array position; the order is not guaranteed.
3. Fetch the rows
Section titled “3. Fetch the rows”curl "https://api.statscore.com/v2/standings/219301?token=YOUR_TOKEN"The shape is standings → groups[] → participants[]:
{ "api": { "data": { "standings": { "id": 219301, "name": "League standings - Africa, Africa CON (W) 2026, stage: Group Stage", "type_name": "League standings", "reset_group_rank": "yes", "groups": [ { "id": "…", "name": "Group A", "zones": [], "corrections": [], "participants": [ { "id": 950684, "name": "Morocco (W)", "participant_acronym": "MOR", "participant_logo": "…", "rank": 1, "last_rank": 3, "zone_id": null, "zone_name": null, "columns": [ { "id": 1, "name": "Matches played", "short_name": "MP", "code": "mp", "value": 3 }, { "id": 2, "name": "Won", "short_name": "W", "code": "won", "value": 2 }, { "id": 3, "name": "Draw", "short_name": "D", "code": "draw", "value": 1 }, { "id": 4, "name": "Lost", "short_name": "L", "code": "lost", "value": 0 }, { "id": 6, "name": "Goals scored", "short_name": "GS", "code": "goals_scored", "value": 5 }, { "id": 7, "name": "Goals allowed", "short_name": "GA", "code": "goals_allowed", "value": 0 }, { "id": 5, "name": "Goals difference", "short_name": "GD", "code": "goals_diff", "value": 5 }, { "id": 8, "name": "Points", "short_name": "Pts", "code": "pts", "value": 7 } ] } ] } ] } } }}columns[] carries the label and the value together. Each entry has name, short_name,
code and value, so you can render a full table from this response alone - you do not need a
second call just to learn what the numbers mean.
Always read values by code, never by array index. Column sets differ between sports and
between table types, and the order is not part of the contract.
4. Render it
Section titled “4. Render it”import { apiGet } from "./client.js"; // z recipe 01
/** Zwraca gotową do wyrenderowania tabelę dla etapu. */export async function getLeagueTable(stageId, { typeName = "League standings" } = {}) { // 1 - katalog tabel dla etapu const { standings_list } = await apiGet("/v2/standings", { object_type: "stage", object_id: stageId, });
const table = standings_list.find((s) => s.type_name === typeName); if (!table) { const available = standings_list.map((s) => s.type_name).join(", "); throw new Error(`No "${typeName}" for stage ${stageId}. Available: ${available}`); }
// 2 - wiersze wybranej tabeli const { standings } = await apiGet(`/v2/standings/${table.id}`);
// 3 - nagłówki z pierwszego wiersza; kolumny są identyczne dla całej tabeli const first = standings.groups?.[0]?.participants?.[0]; const headers = (first?.columns ?? []).map((c) => ({ code: c.code, label: c.short_name, title: c.name, }));
return { id: standings.id, name: standings.name, headers, groups: (standings.groups ?? []).map((g) => ({ name: g.name, zones: g.zones ?? [], corrections: g.corrections ?? [], rows: (g.participants ?? []) .slice() .sort((a, b) => a.rank - b.rank) .map((p) => ({ rank: p.rank, movement: p.last_rank ? p.last_rank - p.rank : 0, // + = awans w tabeli team: p.name, acronym: p.participant_acronym, logo: p.participant_logo, zone: p.zone_name, // wartości po code, nie po pozycji values: Object.fromEntries((p.columns ?? []).map((c) => [c.code, c.value])), })), })), };}# Python - ten sam przepływdef get_league_table(stage_id: int, type_name: str = "League standings"): catalogue = api_get("/v2/standings", object_type="stage", object_id=stage_id) tables = catalogue["standings_list"]
table = next((t for t in tables if t["type_name"] == type_name), None) if table is None: available = ", ".join(t["type_name"] for t in tables) raise LookupError(f'No "{type_name}" for stage {stage_id}. Available: {available}')
standings = api_get(f"/v2/standings/{table['id']}")["standings"]
groups = [] for g in standings.get("groups", []): rows = [] for p in sorted(g.get("participants", []), key=lambda x: x["rank"]): rows.append({ "rank": p["rank"], "movement": (p["last_rank"] - p["rank"]) if p.get("last_rank") else 0, "team": p["name"], "zone": p.get("zone_name"), "values": {c["code"]: c["value"] for c in p.get("columns", [])}, }) groups.append({"name": g.get("name"), "rows": rows, "zones": g.get("zones", []), "corrections": g.get("corrections", [])})
return {"id": standings["id"], "name": standings["name"], "groups": groups}Things that will bite you
Section titled “Things that will bite you”Groups, not a flat list. Even a single-table league arrives wrapped in groups[]. A cup group
stage has four. Never assume groups[0] is the whole competition.
reset_group_rank. When "yes", rank restarts at 1 in each group. If you flatten groups into
one list without checking this, you get four teams all ranked 1.
corrections[]. Point deductions and administrative adjustments live here, separate from the
computed columns. Ignoring them means your table disagrees with the official one.
zones[] and zone_name. Promotion, relegation and qualification bands. This is what drives
the coloured stripes in a table UI - there is no need to infer them from rank.
last_rank. Previous position, for up/down arrows. It can be 0 or absent early in a season,
so guard before subtracting.
Variations
Section titled “Variations”The same three steps give you other tables - only type_name changes:
| Goal | type_name |
|---|---|
| League table | League standings |
| Recent form | Form table |
| Home-only / away-only | Home standings / Away standings |
| Table at half-time | Halftime standings |
| Top scorers | Top scorers |
| Cards | Cards |
To discover the complete column vocabulary for a sport - including columns that only appear in some
table types - call GET /v2/standings-types. It returns each type with
its full columns[] definition, which is useful for building a fixed table header before any data
arrives.
- Get one match with score and stats
- Display a knockout bracket - for stages where
has_brackets: "yes"
Full reference: GET /v2/standings ·
GET /v2/standings/{id} ·
GET /v2/standings-types