What's inside a match: lineups, events and ratings
The match list gives you a score. The match detail gives you the game. This is what is actually in one, taken from a real response rather than a schema.
The top level
{
"id": 4741712,
"competition": "premier-league",
"season": "2026-27",
"stage": null,
"round": 1,
"kickoff": "2026-08-21T19:00:00.000Z",
"status": "finished",
"minute": 90,
"homeTeam": { "id": 104, "slug": "arsenal", "name": "Arsenal", "code": "ARS", "crestUrl": "..." },
"awayTeam": { "id": 24, "slug": "coventry", "name": "Coventry", "code": "COV", "crestUrl": "..." },
"score": { "home": 3, "away": 0 },
"venue": "Emirates Stadium",
"referee": "Thomas Bramall",
"playedMinutes": 97,
"lineups": { "home": { ... }, "away": { ... } },
"events": [ ... ]
}
Two fields are worth pausing on. minute is the clock, which stops at 90 for a
finished match. playedMinutes is how long the game actually lasted including added
time, 97 here. If you are placing events on a timeline, the second one is your axis.
stage is null in a domestic league and carries a value in a cup, where the round
number alone does not tell you whether you are in the league phase or a knockout.
Lineups
Each side has a formation and two arrays:
"lineups": {
"home": {
"formation": "4-2-3-1",
"starting": [ ... 11 entries ... ],
"bench": [ ... ]
}
}
Every entry, on the pitch or on the bench, looks like this:
{
"player": { "id": 3941, "slug": "david-raya", "name": "David Raya", "photoUrl": "..." },
"shirtNumber": 1,
"position": "Goalkeeper",
"minutes": 90,
"rating": 7.2,
"goals": 0,
"assists": 0,
"stats": { "Goals": "0", "Assists": "0", "Minutes": "90'", ... }
}
The stats object is the per-match detail the source publishes, and it varies by
position: a goalkeeper carries saves, a forward carries shots. Treat it as display material rather
than something to compute on. The typed fields beside it, minutes,
rating, goals, assists, are the ones that are always there
and always numbers.
You rarely need to add it up yourself. A player's season stats already total the useful
ones as numbers: xG, expected assists, shots, key passes, passes, tackles, interceptions, and
saves for goalkeepers. /stats/expected-goals and its neighbours rank them, and a
team's /stats carries shots and xG for and against.
A substitute who did not play has minutes: 0 and a null rating. That is how you tell
an unused substitute from one who came on late.
Events
One flat array in chronological order. This match had 14:
{
"minute": 15,
"addedTime": 0,
"type": "goal",
"subtype": null,
"label": "Goal",
"teamId": 104,
"player": { "id": 45579, "name": "Kai Havertz", ... },
"secondaryPlayer": { "id": 79301, "name": "Riccardo Calafiori", ... }
}
secondaryPlayer carries the second person in the event, and what that means depends
on the type. On a goal it is the assist. On a substitution it is the player going the other way. It
is null when there is nobody, as on Bukayo Saka's unassisted goal eight minutes later.
subtype qualifies the type: a goal can be a penalty or an own goal. Check it before
you credit a striker with a tap-in past his own keeper.
minute and addedTime are separate. An event in the third minute of
stoppage time at the end of the first half is minute 45, added time 3. Adding them together gives 48
and puts it after a real 46th-minute event from the second half, so sort on the pair rather than
the sum.
Rebuilding the scoreline
A goal-by-goal summary is a filter and a running count:
const goals = match.events.filter((e) => e.type === 'goal');
let home = 0, away = 0;
for (const g of goals) {
const own = g.subtype === 'own-goal';
const forHome = (g.teamId === match.homeTeam.id) !== own;
forHome ? home++ : away++;
console.log(
g.minute + "' " + g.player.name +
(g.subtype ? ' (' + g.subtype + ')' : '') +
' — ' + home + '-' + away
);
}
Note the own-goal flip. The event carries the team the scorer plays for, not the team that benefited, so an own goal counts the other way. Getting this wrong is the most common bug in a match summary, and it only shows up on the rare match that has one.
The cheap version
A match detail is the largest response in the API. If you only need the score and the scorers,
/matches?status=finished gives you the scoreline without the lineups, and it is a
fraction of the size. Fetch the detail when somebody clicks.