Skip to content

List a team squad

Verified on production8 min

The squad endpoint returns a team's personnel for a season. It is one call and needs no navigation, but the response mixes three things you probably want to separate: current players, players who have left, and the coach.

Terminal window
curl "https://api.statscore.com/v2/participants/136761/squad?token=YOUR_TOKEN&competition_id=1498&season_id=65485"
ParameterWhereRequiredWhy you want it
idpathyesthe team
season_idquerynoscopes the squad to one season
competition_idquerynodisambiguates when a team plays several competitions

Without season_id you get the team's personnel as the provider currently holds it, which is not the same as "the squad in season X". Pass both when you are rendering a historical page.

Everything arrives in api.data.participants[], and every entry has type: "person". What distinguishes them are two fields that are easy to miss.

{
"api": {
"data": {
"participants": [
{
"id": 271441,
"name": "Mikael Ishak",
"short_name": "Mikael Ishak",
"acronym": "ISH",
"type": "person",
"area_name": "Sweden",
"shirt_nr": "9",
"team_connection": "current",
"details": {
"position_name": "Attacker",
"position_id": "34",
"birthdate": "1993-03-31",
"born_place": "Stockholm",
"height": 185,
"weight": 81,
"is_retired": "no",
"subtype": "athlete",
"absence": null
}
}
]
}
}
}

Four things in details that break naive rendering:

FieldTrap
shirt_nrcan be null, and is a string when present ("9", "41"). Do not sort numerically without coercing, and do not assume it exists.
shirt_nrnot unique across the array. Two entries shared "17" and two shared "7" in our sample - because one of each pair is transfer_outer. Unique only within current.
height / weighteither a number (185) or an empty string ("") for players without data. Number("") is 0, so an unguarded coercion produces a 0 cm player.
born_placeoften "". Fine to show conditionally, never as a required field.

Positions come as a pair, and the ids are stable:

position_idposition_name
31Goalkeeper
32Defender
33Midfielder
34Attacker

Group by position_id rather than by the localized position_name - the name changes with the lang parameter, the id does not.

details.absence was null on every row we captured. It presumably carries injuries or suspensions, but we have not seen it populated, so do not build a UI that depends on its shape.

import { apiGet } from "./client.js"; // z recipe 01
const POSITION_ORDER = { 31: 0, 32: 1, 33: 2, 34: 3 }; // GK, DEF, MID, ATT
/**
* Zwraca sklad podzielony na obecnych zawodnikow, odeszlych i sztab.
* Jedna tablica z API zawiera wszystkie trzy grupy naraz.
*/
export async function getSquad(teamId, { seasonId, competitionId } = {}) {
const data = await apiGet(`/v2/participants/${teamId}/squad`, {
season_id: seasonId,
competition_id: competitionId,
});
// "" i null znacza "brak danych" - Number("") to 0, wiec pilnujemy tego jawnie
const num = (v) => (v === "" || v == null ? null : Number(v));
const people = (data.participants ?? []).map((p) => {
const d = p.details ?? {};
return {
id: p.id,
name: p.name,
shortName: p.short_name,
country: p.area_name,
// shirt_nr bywa null i jest stringiem, gdy jest
shirtNr: num(p.shirt_nr),
// "current" albo "transfer_outer" - dopasowujemy do "current", nie wykluczamy reszty
isCurrent: p.team_connection === "current",
// trener siedzi w tej samej tablicy, rozni sie tylko subtype
isCoach: d.subtype === "coach",
position: d.position_id ? { id: Number(d.position_id), name: d.position_name } : null,
birthdate: d.birthdate || null,
heightCm: num(d.height),
weightKg: num(d.weight),
absence: d.absence ?? null,
};
});
const byPosition = (a, b) =>
(POSITION_ORDER[a.position?.id] ?? 99) - (POSITION_ORDER[b.position?.id] ?? 99) ||
(a.shirtNr ?? 999) - (b.shirtNr ?? 999);
return {
players: people.filter((p) => p.isCurrent && !p.isCoach).sort(byPosition),
departed: people.filter((p) => !p.isCurrent && !p.isCoach).sort(byPosition),
staff: people.filter((p) => p.isCoach),
};
}
# Python - ten sam podzial
POSITION_ORDER = {31: 0, 32: 1, 33: 2, 34: 3} # GK, DEF, MID, ATT
def get_squad(team_id: int, season_id: int | None = None, competition_id: int | None = None):
data = api_get(f"/v2/participants/{team_id}/squad",
season_id=season_id, competition_id=competition_id)
def num(v):
# "" i None znacza brak danych; int("") rzuca, a Number("") w JS dawalo 0
return None if v in ("", None) else float(v)
people = []
for p in data.get("participants", []):
d = p.get("details") or {}
people.append({
"id": p["id"],
"name": p.get("name"),
"country": p.get("area_name"),
"shirt_nr": num(p.get("shirt_nr")),
# dopasowujemy do "current", nie wykluczamy znanych innych wartosci
"is_current": p.get("team_connection") == "current",
# trener jest w tej samej tablicy
"is_coach": d.get("subtype") == "coach",
"position": {"id": int(d["position_id"]), "name": d.get("position_name")}
if d.get("position_id") else None,
"birthdate": d.get("birthdate") or None,
"height_cm": num(d.get("height")),
"weight_kg": num(d.get("weight")),
"absence": d.get("absence"),
})
def key(p):
pos = POSITION_ORDER.get((p["position"] or {}).get("id"), 99)
return (pos, p["shirt_nr"] if p["shirt_nr"] is not None else 999)
return {
"players": sorted([p for p in people if p["is_current"] and not p["is_coach"]], key=key),
"departed": sorted([p for p in people if not p["is_current"] and not p["is_coach"]], key=key),
"staff": [p for p in people if p["is_coach"]],
}

Per-match appearances are a different endpoint

Section titled “Per-match appearances are a different endpoint”

The squad tells you who belongs to the team, not who played. For minutes, lineups and per-match appearances you need:

QuestionEndpoint
who was in the lineup for this match, and their positionGET /v2/events/{id} -> lineups[] (lineups and formations)
minutes and bench for this matchGET /v2/events/{id}/participants
goals and cards across the seasonrankings, not the squad (recipe 08)
StatusCause
400malformed season_id or competition_id
401missing, expired or malformed token
403the competition is outside your contract
404unknown participant id

Full reference: GET /v2/participants/{id}/squad · GET /v2/events/{id}/participants