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_stats on the match — whether this fixture carries statistics at all. True only when the match has a stream and its competition supplies them.
  • confirmed in 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:

CategoryContains
generalgoals, assists, shots, shots_on, fouls, fouls_drawn, offsides, yellow_cards, red_cards
passingtotal, accurate, key_pass
defensetackles, interceptions, blocks, saves, conceded
duelstotal, won
dribblesattempts, success, past, won
penaltywon, 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 surface confirmed honestly.
  • 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 > 0 separates substitutes who played from those who did not.
  • missing is 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.