Get match timeline
Get the events of a played match in the order they happened: goals, cards, substitutions, shots, VAR checks and more. Use it to build a match report, a goal list or a minute-by-minute feed. Filter by event type, period or team to get just what you need.
https://api.soccer-data.co/v1/matches/{match_id}/timeline
- Returns 409 while the match is live. Use the live timeline endpoint until full-time.
- Goals come in three types. Penalties are penalty_scored and own goals are own_goal, so send event_type=goal,penalty_scored,own_goal to get every goal.
- Shoot-out kicks are also penalty_scored, with period set to penalties. Use the period filter to leave them out.
- Stoppage time is always split out. A card at 45+3 has minute 45 and extra_time 3.
- If a match has no timeline, you still get a 200 with an empty events list. meta.coverage is none and meta.reason tells you why.
- An unknown event_type returns 400.
The match's id, like match_…. Find one with the ID guide.
Comma-separated list of event types to keep. Leave it out to get every event.
goalown_goalshot_on_targetshot_off_targetshot_blockedshot_savedshot_woodworkyellow_cardred_cardyellow_red_cardfouloffsidecorner_kicksubstitutionpenalty_awardedpenalty_scoredpenalty_missedpenalty_savedvarperiod_startperiod_endadded_timeComma-separated list of periods to keep, for example only the second half.
first_halfsecond_halfextra_time_first_halfextra_time_second_halfpenaltiesOnly return events for this team.
The match's id.
full when a timeline exists for this match, none when it doesn't.
Events in the order they happened. Empty when there is no timeline. Read the list as it comes; it is already sorted.
Position of the event in the match, starting at 1.
Minute of the event within normal time for that period. Stoppage minutes go in extra_time.
Stoppage minutes on top of minute. 0 for events in normal time.
first_half, second_half, extra_time_first_half, extra_time_second_half or penalties.
What happened. One of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork (hit the post or bar), yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end or added_time. For own_goal, player is the one who put it in their own net and team is their side, so the goal counts for the other team.
The team involved.
Team's id. Null when only the name is known.
Team's name.
URL-friendly name, handy for building links.
The player involved. Same fields as team: id (null when only the name is known), name and slug.
Number of events returned, after filters.
Same as data.coverage: full or none.
Only present when coverage is none. match_not_started, no_events, or not_supported when we don't cover events for this competition.
When the timeline last changed.
curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/timeline" \ -H "X-Api-Key: $SOCCER_DATA_KEY"
const res = await fetch("https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/timeline", {
headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY },
});
const { data } = await res.json();
import os, requests
res = requests.get(
"https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/timeline",
headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]},
)
data = res.json()["data"]
$data = Http::withHeaders(['X-Api-Key' => env('SOCCER_DATA_KEY')])
->get('https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/timeline')
->json('data');
{
"data": {
"match_id": "match_9pd0ck6g2m9i",
"coverage": "full",
"events": [
{
"sequence": 3,
"minute": 45,
"extra_time": 3,
"period": "first_half",
"type": "yellow_card",
"team": {
"id": "team_4mimn5fmya2v",
"name": "Aston Villa",
"slug": "aston-villa-team_4mimn5fmya2v"
},
"player": {
"id": "player_85xcr2xm5vxo",
"name": "Ollie Watkins",
"slug": "ollie-watkins-player_85xcr2xm5vxo"
}
}
]
},
"meta": {
"total": 10,
"coverage": "full",
"last_updated": "2026-05-15T21:05:49.659Z"
}
}
### GET /matches/{match_id}/timeline
Get match timeline
Get the events of a played match in the order they happened: goals, cards, substitutions, shots, VAR checks and more. Use it to build a match report, a goal list or a minute-by-minute feed. Filter by event type, period or team to get just what you need.
- Returns 409 while the match is live. Use the live timeline endpoint until full-time.
- Goals come in three types. Penalties are penalty_scored and own goals are own_goal, so send event_type=goal,penalty_scored,own_goal to get every goal.
- Shoot-out kicks are also penalty_scored, with period set to penalties. Use the period filter to leave them out.
- Stoppage time is always split out. A card at 45+3 has minute 45 and extra_time 3.
- If a match has no timeline, you still get a 200 with an empty events list. meta.coverage is none and meta.reason tells you why.
- An unknown event_type returns 400.
Query parameters:
- `event_type` (string, one of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork, yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end, added_time): Comma-separated list of event types to keep. Leave it out to get every event.
- `period` (string, one of first_half, second_half, extra_time_first_half, extra_time_second_half, penalties): Comma-separated list of periods to keep, for example only the second half.
- `team_id` (string): Only return events for this team.
Response fields:
- `data.match_id` (string): The match's id.
- `data.coverage` (string): full when a timeline exists for this match, none when it doesn't.
- `data.events[]` (array): Events in the order they happened. Empty when there is no timeline. Read the list as it comes; it is already sorted.
- `data.events[].sequence` (integer): Position of the event in the match, starting at 1.
- `data.events[].minute` (integer): Minute of the event within normal time for that period. Stoppage minutes go in extra_time.
- `data.events[].extra_time` (integer): Stoppage minutes on top of minute. 0 for events in normal time.
- `data.events[].period` (string): first_half, second_half, extra_time_first_half, extra_time_second_half or penalties.
- `data.events[].type` (string): What happened. One of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork (hit the post or bar), yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end or added_time. For own_goal, player is the one who put it in their own net and team is their side, so the goal counts for the other team.
- `data.events[].team` (object|null): The team involved.
- `data.events[].team.id` (string|null): Team's id. Null when only the name is known.
- `data.events[].team.name` (string): Team's name.
- `data.events[].team.slug` (string|null): URL-friendly name, handy for building links.
- `data.events[].player` (object|null): The player involved. Same fields as team: id (null when only the name is known), name and slug.
- `meta.total` (integer): Number of events returned, after filters.
- `meta.coverage` (string): Same as data.coverage: full or none.
- `meta.reason` (string): Only present when coverage is none. match_not_started, no_events, or not_supported when we don't cover events for this competition.
- `meta.last_updated` (string|null): When the timeline last changed.
Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/timeline" -H "X-Api-Key: $SOCCER_DATA_KEY"