Football lineups API: formations and player ratings
Formations, starting elevens, substitutes, missing players and per-player statistics across six categories - plus the thing most implementations get wrong: ratings keep changing while the match is being played.
Updated 2026-10-26 8 min read
Line-ups are the richest per-match payload in the API and the one with the most ways to go subtly wrong. The data is straightforward; the timing is not.
Check the flag, then the confirmation
Two separate gates, and they mean different things:
-
has_statson the match — whether this fixture carries statistics at all. True only when the match has a stream and its competition supplies them. -
confirmedin the response — whether what you are looking at is the real line-up or a projection.
if (!match.has_stats) return; // no line-ups for this fixture
const { confirmed, data } = await api(`?type=lineups&id=${id}`);
if (!confirmed) showBadge('Predicted line-up'); Line-ups generally appear about an hour before kick-off. Rendering an unconfirmed eleven without saying so is how you end up with users insisting your app is wrong.
The shape
{
"success": true,
"confirmed": true,
"data": {
"home": {
"formation": "4-3-3",
"avg_rating": 7.1,
"kit_colors": { "player": "#EF0107", "goalkeeper": "#1D9E4B" },
"coach": { "name": "Mikel Arteta", "country": "Spain", "photo": "..." },
"start_xi": [ /* players, in positional order */ ],
"subs": [ /* same shape */ ],
"missing": [ { "name": "Gabriel Jesus", "reason": "Injury", "photo": "..." } ]
},
"away": { "formation": "4-2-3-1" }
}
} Drawing a formation without coordinates
There are no pitch coordinates, and you do not need them. The formation
string gives the shape, and start_xi arrives in positional
order — goalkeeper first, then each line:
function rows(formation, xi) {
const counts = formation.split('-').map(Number); // "4-3-3" -> [4,3,3]
const out = [[xi[0]]]; // goalkeeper
let i = 1;
for (const n of counts) { out.push(xi.slice(i, i + n)); i += n; }
return out; // [[GK],[4],[3],[3]]
}
Render each row as a flex line, defence at the bottom. Use
kit_colors for the shirts and you get a pitch view that matches
the real teams without shipping a single image.
Per-player statistics
Each player carries rating, minutes, and a
stats object in six categories:
| Category | Contains |
|---|---|
general | goals, assists, shots, shots_on, fouls, fouls_drawn, offsides, yellow_cards, red_cards |
passing | total, accurate, key_pass |
defense | tackles, interceptions, blocks, saves, conceded |
duels | total, won |
dribbles | attempts, success, past, won |
penalty | won, committed, scored, missed, saved |
Show two or three per position rather than all of them. A goalkeeper card wants saves and conceded; a forward wants goals, shots on target and key passes. Dumping thirty numbers per player is data, not a product.
The timing mistake
This is the one worth stating on its own, because it is easy to reason your way into it.
Line-ups look final once published, so caching them for the match seems safe. It is not: the names are final, the ratings are not. A rating is recalculated as the match proceeds.
Cache a line-up for the whole match and every player sits frozen at their kick-off rating for ninety minutes, on a tab labelled live. Cache on the match state instead:
const ttl = match.status === 'inprogress'
? 60_000 // ratings move; refresh while in play
: 600_000; // finished: nothing changes, hold it Substitutes and missing players
subs has the same shape as the starting eleven. A substitute
who came on carries minutes and real statistics; one who stayed
on the bench carries zeros. Use minutes > 0 to tell them
apart rather than cross-referencing the incident timeline.
missing is independent of both and often the most read part of
a pre-match page — each entry has a name, a photo and a
reason such as Injury or Suspended. Render it before kick-off,
when it is the thing people came for, and collapse it afterwards.
Average rating and coaches
avg_rating per side makes an easy comparison header, and
coach carries a name, country and photo — enough for a
small managers row without another request.
Summary
- Gate on
has_stats, then surfaceconfirmedhonestly. - Formation plus positional order is enough to draw a pitch.
- Six stat categories; show the two or three that fit the position.
- Ratings change in play — cache on match state, not once per match.
minutes > 0separates substitutes who played from those who did not.missingis the pre-match headline; give it room.
Frequently asked
When are line-ups published?
Usually about an hour before kick-off. Before that the endpoint returns nothing useful, and confirmed tells you which case you are in.
Do player ratings change during a match?
Yes. A rating is recalculated as the match proceeds, so a line-up fetched at kick-off and cached until full time will show stale ratings for ninety minutes. Cache line-ups briefly while the match is in play and for much longer once it is finished.
Which matches have line-ups?
Those where has_stats is true. That flag is true only when the match has a stream AND its competition supplies statistics, so check it before requesting.
What is in the missing array?
Players unavailable for the match, each with a reason such as Injury or Suspended. It is populated independently of the starting eleven.
Can I draw a formation pitch from this?
Yes. The formation string gives the shape and start_xi arrives in positional order, which is enough to lay out rows on a pitch without any coordinate data.