Skip to content

Show top scorers, cards and assists

Verified on production12 min

Player rankings come from the same endpoint as the league table. Nothing new to learn about navigation: it is the three-step flow from recipe 02, with a different type selected. What is new is the row shape, and it inverts one thing you learned there.

GET /v2/standings-types?sport_id=5 lists every ranking type available for a sport, with its columns. Soccer has 19. These are the ones you will actually want:

type_idtype_nameWhat it ranksColumns
2League standingsteamsmp, won, draw, lost, goals_scored, goals_allowed, goals_diff, pts, form
6Top scorersplayersgoals_scored, mp, goals_per_match, manual_rank
7Cardsplayersyellow_cards, red_cards, cards, pts, mp, cards_per_match, yellow_cards_per_match, red_cards_per_match
176Assistsplayersassists, mp, assists_per_match, manual_rank
18Form tableteamsform
47Streaksteamswins, draws, losses, no_wins, no_draws, no_losses
39Most popular resultsresultsresult, amount, pct
244League standings liveteamsleague columns plus is_playing, home_result, away_result, event_id

The rest are variants of the league table (Home standings, Away standings, Halftime standings, Second half standings, Wide standings), two aggregate types (Overall stats with ~80 columns, Team overall stats with ~120) and two rankings (FIFA ranking, FIFA club ranking).

Same call as the league table, and the same three steps: catalogue, then pick, then rows.

Terminal window
curl "https://api.statscore.com/v2/standings?token=YOUR_TOKEN&object_type=season&object_id=65485&limit=50"

Each catalogue entry carries both identifiers, so match on whichever you prefer:

{
"api": {
"data": {
"standings_list": [
{
"id": 196367,
"name": "Top scorers - Poland, PKO Ekstraklasa 2025/26",
"type_id": 6,
"type_name": "Top scorers",
"subtype": "standings",
"object_id": 65485,
"object_type": "season",
"object_name": "PKO Ekstraklasa 2025/26",
"item_status": "active"
}
]
}
}
}

subtype is a coarser grouping than type_name and takes four values in practice: standings, standings_live, form, overall_stats.

Terminal window
curl "https://api.statscore.com/v2/standings/196367?token=YOUR_TOKEN"

Here is where player rankings stop looking like a league table:

{
"api": {
"data": {
"standings": {
"id": 196367,
"type_id": 6,
"type_name": "Top scorers",
"object_type": "season",
"object_id": 65485,
"groups": [
{
"id": "",
"name": "",
"zones": [],
"corrections": [],
"participants": [
{
"id": 1220697,
"name": "Tomas Bobcek",
"participant_type": "person",
"participant_acronym": "BOB",
"area_id": 164,
"area_name": "Slovakia",
"rank": 1,
"last_rank": 1,
"subparticipant_id": 136762,
"subparticipant_name": "Lechia Gdansk",
"columns": [
{ "id": 6, "code": "goals_scored", "short_name": "GS", "value": 20 },
{ "id": 1, "code": "mp", "short_name": "MP", "value": 30 },
{ "id": 393, "code": "goals_per_match", "short_name": "GPM", "value": "0.67" }
]
}
]
}
]
}
}
}
}

Two more things the sample shows:

  • The top-level columns array is empty. Stats live in columns on each participant, with a value. Read them from the rows, or from /v2/standings-types if you need the definitions before the data arrives.
  • value types are mixed. goals_scored is a number (20), goals_per_match is a string ("0.67"). Coerce on read; do not sort on the raw value.

rank and last_rank let you show movement. zones and corrections exist on the group but were empty arrays for both rankings we captured, so do not build a UI that requires them.

Top scorers, Ekstraklasa 2025/26, first five of 257 rows:

#PlayerClubGSMPGPM
1Tomas BobcekLechia Gdansk20300.67
2Karol CzubakMotor Lublin18320.56
3Jonatan Braut BrunesRakow Czestochowa16320.50
4Mikael IshakLech Poznan16310.52
5Afimico PululuJagiellonia15330.45

Cards, same season, 384 rows:

#PlayerClubYCRCcardsPtsMP
1Juljan ShehuWidzew Lodz111121432
2Ivan ZhelizkoLechia Gdansk101111330
3Oskar WojcikCracovia101111331
import { apiGet } from "./client.js"; // z recipe 01
/**
* Zwraca ranking zawodnikow dla sezonu. `typeName` to np. "Top scorers",
* "Cards", "Assists" - pelna lista z /v2/standings-types.
*/
export async function getPlayerRanking(seasonId, typeName, { scope = "season" } = {}) {
const catalogue = await apiGet("/v2/standings", {
object_type: "season",
object_id: seasonId,
limit: 50,
});
// Pytanie o sezon zwraca TEZ rankingi etapow - filtrujemy po object_type sami.
const entry = (catalogue.standings_list ?? []).find(
(s) => s.type_name === typeName && s.object_type === scope && s.item_status === "active"
);
if (!entry) return null;
const data = await apiGet(`/v2/standings/${entry.id}`);
const standing = data.standings;
// Rankingi zawodnikow maja jedna grupe bez nazwy; tabele ligowe moga miec kilka.
const rows = (standing.groups ?? []).flatMap((g) => g.participants ?? []);
const num = (v) => (v === "" || v == null ? null : Number(v));
return {
id: standing.id,
typeName: standing.type_name,
scope: standing.object_type,
rows: rows.map((p) => ({
rank: p.rank,
lastRank: p.last_rank ?? null,
// W rankingu zawodnikow participant to OSOBA, a subparticipant to KLUB.
// W tabeli ligowej jest odwrotnie - participant to zespol.
player: p.participant_type === "person" ? { id: p.id, name: p.name, country: p.area_name } : null,
team: p.subparticipant_id
? { id: p.subparticipant_id, name: p.subparticipant_name }
: { id: p.id, name: p.name },
// wartosci sa mieszane: liczby i stringi. Klucz to `code`, nie pozycja.
stats: Object.fromEntries((p.columns ?? []).map((c) => [c.code, num(c.value)])),
})),
};
}
# Python - ten sam przeplyw
def get_player_ranking(season_id: int, type_name: str, scope: str = "season"):
catalogue = api_get("/v2/standings", object_type="season", object_id=season_id, limit=50)
# pytanie o sezon zwraca tez rankingi etapow - filtrujemy po object_type
entry = next(
(s for s in catalogue.get("standings_list", [])
if s.get("type_name") == type_name
and s.get("object_type") == scope
and s.get("item_status") == "active"),
None,
)
if not entry:
return None
standing = api_get(f"/v2/standings/{entry['id']}")["standings"]
def num(v):
return None if v in ("", None) else float(v)
rows = []
for g in standing.get("groups", []):
for p in g.get("participants", []):
is_person = p.get("participant_type") == "person"
rows.append({
"rank": p.get("rank"),
"last_rank": p.get("last_rank"),
# participant = osoba, subparticipant = klub (odwrotnie niz w tabeli ligowej)
"player": {"id": p["id"], "name": p.get("name"),
"country": p.get("area_name")} if is_person else None,
"team": {"id": p.get("subparticipant_id"), "name": p.get("subparticipant_name")}
if p.get("subparticipant_id") else {"id": p["id"], "name": p.get("name")},
"stats": {c["code"]: num(c.get("value")) for c in p.get("columns", [])},
})
return {"id": standing.get("id"), "type_name": standing.get("type_name"),
"scope": standing.get("object_type"), "rows": rows}

Player rankings are generated, not universal. Two fields on the competition tell you what to expect, both visible on /v2/competitions:

FieldEkstraklasaMeaning
stats_lvl"gold"depth of statistics: bronze / silver / gold
generate_season_statstruewhether season aggregates are produced at all

A bronze competition may have a league table and no top scorers. Branch on the catalogue being empty rather than assuming a ranking exists.

StatusCause
400limit outside the endpoint's enum, or a missing object_type / object_id pair
401missing, expired or malformed token
403the competition is outside your contract
404unknown standings id

Full reference: GET /v2/standings · GET /v2/standings-types · GET /v2/competitions