---
title: "Play-by-play / match events"
method: GET
path: "/v1/matches/{id}/events"
tags: ["matches"]
---

# Play-by-play / match events

`GET /v1/matches/{id}/events`

## Request

- Base URL: `https://api.bigballsdata.com`
- URL: `https://api.bigballsdata.com/v1/matches/{id}/events`
- Auth: one of:
  - API key in header `x-api-key`
  - HTTP bearer

Scoring events are included on every plan through the default view or
`?type=goals`. The full soccer minute-by-minute stream (`?type=all`,
including cards, substitutions, shots and VAR) requires Pro, matching
the NBA, NHL and NFL play-by-play floor. Where stored soccer event coverage answers
the request, `?type=goals` excludes penalties that were missed or saved
instead of mislabelling them as scored goals. Those events, and
VAR-disallowed goals, stay visible with their minute and player under
the Pro `?type=all` view. In the measured stored-event corpus, this
improves agreement with the free `score` / `linescore` fields from
22,482 of 22,755 matches (98.80%) to 22,685 of 22,755 (99.69%); 70 of
22,755 matches still require separate reconciliation.
For NBA, NHL and NFL matches, `?type=all` returns the full play log,
which is the same product as each league's dedicated plays route and
likewise requires Pro. Lower tiers receive a 403 naming the tier.
The match's sport must also be in your plan.

On `?type=all` only, NFL rows are projected from the stored NFL play
log through the match's canonical UUID. They include down, distance,
field position, yards gained, EPA and win probability when the feed
supplies them. `player_id` remains null on these rows: the upstream
player identifiers do not yet map one-to-one onto canonical player
UUIDs, so the API does not guess. The default scope and `?type=goals`
return the scoring summary without those play-level fields, which is
what keeps them available below Pro.

**Soccer shot coordinates and xG.** Where we hold shot-level data for a
match, `?type=all` also returns shot events carrying `coordinate_x` /
`coordinate_y` (the shooter's position, 0-100 normalised), `xg` and
`xgot`. Coverage is narrow and documented rather than implied: these
fields are populated for FIFA World Cup 2026 matches only, and are
`null` everywhere else. They are absent, not zero, when unknown. The
same rows are also served by
`GET /v1/stored/matches/{id}/plays`.

**`data` is ALWAYS an array, including when we hold nothing.** Until
2026-09-20 the live-router fallback arm broke that promise: a match we
do not cover returned `data: {"events": null}`, and a match served from
the live fan-out returned `data: {"events": {"value": [...]}}`, so
`data.map(...)` threw on two of the three success bodies this one path
could produce. Both now return the array, and the no-coverage signal
moved into `meta` rather than being dropped:

- `meta.coverage: false` plus a human-readable `meta.message` mean
  **we hold no events for this match**. These are the same keys
  `GET /v1/nfl/games/{gameId}/plays` uses for its empty page.
- `meta.coverage` absent with `data: []` means the match IS covered and
  the feed reports no events (for example `?type=goals` on a goalless
  match).
- `meta.count` is the length of `data`, derived from the rows sent.

An empty array and an uncovered match are different answers, so read
`meta.coverage` before concluding a match has no events.

## Path parameters

- `id` string, uuid, required: A stored BigBalls id. This is the identifier every match, team, player and league endpoint returns, and the one every `{id}` path segment accepts. Take it from the `id` field of the corresponding list endpoint and use it as-is.

## Query parameters

- `sport` string
- `type` 'goals' | 'all'

## Response `200`

ok. `data` is an array on every 200, empty when we hold no events; see `meta.coverage` / `meta.message` for why it is empty.

- object: The standard response wrapper. Only `data` is guaranteed. `error` is absent (not null) on 16 of 40 live 200 responses measured 2026-09-18, so test for the KEY before reading it; this block declared it required until then. On a non-2xx, `error` is present and `data` is absent.
  - `data` Event[], required
    - `sequence_number` integer: Ordering key within the match.
    - `elapsed` integer: Soccer minute from the live adapter.
    - `elapsed_extra` integer, nullable
    - `team` string, nullable
    - `player_name` string, nullable
    - `player_id` string, nullable
    - `headshot_url` string, uri, nullable
    - `assist_name` string, nullable
    - `assist_id` string, nullable
    - `assist_headshot_url` string, uri, nullable
    - `event_type` string, nullable
    - `event_detail` string, nullable
    - `card_class` 'second_yellow' | 'straight_red' | 'yellow' | 'null', nullable: Football only. Whether a dismissal was a SECOND YELLOW or a DIRECT (straight) RED, derived from the player's card sequence in this match, because the upstream feed labels every card either `Yellow Card` or `Red Card` and states nothing more. `event_detail` is unchanged and still carries the upstream label verbatim. Definitions, applied consistently: `second_yellow` means the player has exactly two yellows and exactly one red in this match; `straight_red` means exactly one red and no yellow; `yellow` is any booking. `null` means WE WILL NOT SAY, and for a red card that is a deliberate verdict rather than missing data: it covers a red following exactly one booking (which cannot be told apart from a second yellow with a missing first booking), any player-match where VAR reviewed, upgraded or cancelled a card, more than one dismissal for one player, and events whose player cannot be identified. An unlabelled red is still returned in full.
    - `period` integer, nullable
    - `period_display` string, nullable
    - `clock` Clock
      - `elapsed_seconds` integer, required
      - `period` integer, required
      - `added_seconds` integer
    - `type` string
    - `team_id` string, uuid: A stored BigBalls id. This is the identifier every match, team, player and league endpoint returns, and the one every `{id}` path segment accepts. Take it from the `id` field of the corresponding list endpoint and use it as-is.
    - `related_player_id` string, uuid: A stored BigBalls id. This is the identifier every match, team, player and league endpoint returns, and the one every `{id}` path segment accepts. Take it from the `id` field of the corresponding list endpoint and use it as-is.
    - `description` string
    - `scoring_play` boolean
    - `score_value` integer, nullable: Points/runs this event added.
    - `home_score` integer, nullable: Running home score AFTER this event.
    - `away_score` integer, nullable: Running away score AFTER this event.
    - `coordinate_x` number, nullable: Soccer shot location: the SHOOTER's position on a 0-100 normalised pitch. `null` on non-shot events and on any event from a feed that carries no positions.
    - `coordinate_y` number, nullable
    - `xg` number, nullable: Expected goals for a shot, 0-1. `null` means the source does not model xG for this event, never "zero chance".
    - `xgot` number, nullable: Expected goals on target. Present only for shots that were on target; `null` for misses and blocks by design.
    - `down` integer, nullable: NFL down; null for plays without a down and for non-NFL events.
    - `distance` integer, nullable: NFL yards to gain; null for non-NFL events.
    - `yards_to_endzone` integer, nullable: NFL field position measured in yards from the opponent end zone.
    - `yards_gained` integer, nullable: NFL yards gained on the play; null for non-NFL events.
    - `epa` number, nullable: NFL expected points added; null when unavailable or on non-NFL events.
    - `win_probability` number, nullable: NFL possession-team win probability after the play.
    - `defending_team_id` string, uuid: A stored BigBalls id. This is the identifier every match, team, player and league endpoint returns, and the one every `{id}` path segment accepts. Take it from the `id` field of the corresponding list endpoint and use it as-is.
  - `meta` object: Response metadata. NOTHING here is guaranteed: measured over 38 live envelopes on 2026-09-18, `cached` and `request_id` appeared in 29, `source` in 28, `cache_age_ms` in 9 and `confidence` in 6. This block previously declared all five REQUIRED, which no response has ever satisfied. Treat every field as optional and branch on its absence. Routes add their own keys here (`count`, `limit`, `offset`, `note`, `total`, and per-endpoint ones), so do not assume a closed set.
    - `date_window` object: Present when ?date= was applied. The exact window the filter used, so an omission at a day boundary is visible. start_utc is inclusive, end_utc exclusive. timezone is UTC, or the zone named by ?tz=, in which case the window is that local day. Boxing card dates are published event dates, so timezone and bounds are null there.
      - `date` string
      - `timezone` string, nullable
      - `start_utc` string, date-time, nullable
      - `end_utc` string, date-time, nullable
      - `note` string
    - `source` 'official-league' | 'aggregator-paid' | 'aggregator-free' | 'community-scraper' | 'forecast' | 'mixed' | 'cache' | 'none' | 'unknown-source' | 'stored' | 'catalogue' | 'gateway' | 'coverage-snapshot': Opaque source-tier label. Customers never see the vendor identity. `forecast` marks a weather forecast (GET /v1/matches/{id}/weather).
    - `confidence` number, float
    - `cached` boolean
    - `cache_age_ms` integer
    - `request_id` string
    - `fields_missing` string[]
    - `note` string: Short factual disclosure about something this response deliberately withheld, present only when there is something to disclose. On live scores it appears when `match_id` could not be resolved to a stored match and was therefore omitted: it gives the counts and points at `/v1/stored/matches`, whose ids you can call back into. Absent when every served id resolves. Distinct from `message`, which explains `coverage: false` or a stale response. This one is attached to a response that DID serve data and names the part of it that is deliberately missing.
    - `count` integer: Length of `data`.
    - `coverage` boolean: Present and `false` only when we hold no events for this match. Absent means the match is covered.
    - `message` string: Human-readable reason, present with `coverage: false`.
  - `error` ApiError
    - `code` string, required
    - `message` string, required
    - `details` unknown

## Other responses

- `403`: plan_required (Pro or higher for `type=all`)

---

[API](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api.md) · [All operations](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/llms.txt) · [OpenAPI document](https://skmtc.dev/bigballsdata/apis/big-ball-sports-api/revisions/31fb54ac414e?raw)
