Soccer fixture results now have populated `events`.
Soccer Timeline Events
We've added a new events array to the /v3/fixtures/results response for soccer fixtures. Each entry describes a discrete in-match event — currently goals, yellow cards, red cards, and substitutions — with the player(s) involved, when it happened, and which side scored/was penalised/swapped.
Where to find it
GET /v3/fixtures/results
→ data[].events → TimelineEvent[]
The field exists on every fixture result row. It's an empty array ([]) for non-soccer sports, for soccer fixtures with no harvested timeline, and for fixtures whose timeline contains no goal/card/sub events.
TimelineEvent shape
| Field | Type | When present | Description |
|---|---|---|---|
event_type | string | always | "goal" | "yellow_card" | "red_card" | "substitution" |
period | string | when known | Display period code matching the rest of the v3 contract: "1H", "HALF", "2H", "END-REG", "ET 1H", "HALF-ET", "ET 2H", "END-ET", "PENS", "END-PENS" |
period_number | int | when known | 1 = first half, 2 = second half, 3 = extra-time first half, 4 = extra-time second half, 5 = penalty shootout |
clock | string | when known | Display clock at the moment of the event. Stoppage-time events encoded as "90+3". |
seconds_elapsed | float | when known | Seconds elapsed in the match — useful for sorting / grading. Consistent within a single fixture. |
team | string | when known | "home" or "away" — which side the event belongs to |
player | object | goal, card | { "id": string, "name": string } — primary player (scorer or carded player) |
goal_type | string | goal only | "goal" (regular open-play) | "penalty" | "header" | "own_goal" |
assists | object[] | goal only, when present | List of { "id": string, "name": string } for the assisting player(s) |
player_in | object | substitution only | { "id": string, "name": string } — player entering |
player_out | object | substitution only | { "id": string, "name": string } — player leaving |
Field absence is meaningful. Keys that don't apply to an event type (e.g. assists on a yellow card) are omitted rather than emitted as null. Keys we don't have data for (e.g. an unmapped player) are also omitted.
Player IDs. player.id is the canonical OpticOdds player ID — the same ID returned by /v3/players and other v3 endpoints. You can join/cross-reference on it directly. When we can't map a player to our database, the ID is omitted; name is still best-effort.
Player names. Names are pulled from our canonical player record, so you'll get the full name (e.g. "Riyad Mahrez") rather than any abbreviated form. Players we don't have in our system fall back to whatever name we have available.
Example payloads
Goal (open play, with assist)
{
"event_type": "goal",
"period": "1H",
"period_number": 1,
"clock": "36",
"seconds_elapsed": 2141.0,
"team": "home",
"player": {
"id": "FBBFCD9D2402DAE7",
"name": "Nizar Mahmoud Ahmed Al Rashdan"
},
"goal_type": "goal",
"assists": [
{
"id": "BA6C626928973C9E",
"name": "Mousa Mohammad Mousa Sulaiman Al Ta'mari"
}
]
}Goal (penalty, no assist)
{
"event_type": "goal",
"period": "2H",
"period_number": 2,
"clock": "72",
"seconds_elapsed": 4320.0,
"team": "away",
"player": { "id": "ABCD1234", "name": "Erling Haaland" },
"goal_type": "penalty"
}Goal (header)
{
"event_type": "goal",
"period": "2H",
"period_number": 2,
"clock": "69",
"seconds_elapsed": 4083.0,
"team": "away",
"player": { "id": "A6624D374BE613FA", "name": "Ahmed Benbouali" },
"goal_type": "header"
}Goal (own goal)
{
"event_type": "goal",
"period": "1H",
"period_number": 1,
"clock": "23",
"seconds_elapsed": 1380.0,
"team": "home",
"player": { "id": "DEAD0001", "name": "John Stones" },
"goal_type": "own_goal"
}Note: team is the team that benefits from the goal (i.e. the scoring side). The player is the one who scored it into their own net.
Yellow card
{
"event_type": "yellow_card",
"period": "1H",
"period_number": 1,
"clock": "44",
"seconds_elapsed": 2595.0,
"team": "away",
"player": {
"id": "D845A75C55FCC491",
"name": "Ramiz Zerrouki"
}
}Red card
{
"event_type": "red_card",
"period": "2H",
"period_number": 2,
"clock": "88",
"seconds_elapsed": 5280.0,
"team": "home",
"player": { "id": "FACEC0DE", "name": "Casemiro" }
}Substitution
{
"event_type": "substitution",
"period": "2H",
"period_number": 2,
"clock": "46",
"seconds_elapsed": 2700.0,
"team": "away",
"player_in": {
"id": "F6D384301FFFECAD",
"name": "Nabil Bentaleb"
},
"player_out": {
"id": "D845A75C55FCC491",
"name": "Ramiz Zerrouki"
}
}Stoppage-time event
The clock string carries injury time directly:
{
"event_type": "goal",
"period": "2H",
"period_number": 2,
"clock": "90+3",
"seconds_elapsed": 5580.0,
"team": "home",
"player": { "id": "...", "name": "..." },
"goal_type": "goal"
}Ordering
Events are sorted by (period_number, seconds_elapsed) — chronological within a fixture. Two events at the same seconds_elapsed retain their source order.
Non-soccer sports
The events array is currently soccer-only. For other sports it's always []. We plan to extend this to basketball, hockey, and football in future releases — the shape will stay the same, with sport-specific event_type and goal_type values added.
Migration notes
This replaces a previously-undocumented events field that was always [] in practice. If you were ignoring it before, no action needed. If you were reading it, you'll now start seeing real data for soccer fixtures.