Endpoint guide

Query fixtures and results.

The match collection covers scheduled fixtures and completed full-time results from the currently enabled datasets. Per-match team statistics — corners, shots, possession, cards, passes and expected goals — are available separately at /football/matches/{match_id}/stats for covered competitions. Each match also reports which of its details are stored, so you can check before you ask.

Available operations

Method Path Purpose
GET /api/v1/football/matches Filter and paginate fixtures or results. Each match reports which details are stored (details_available).
GET /api/v1/football/matches/{match_id} Retrieve one match by Stats API public ID, including details_available.

Match detail operations

A match resource is deliberately small. Richer detail lives on dedicated sub-resources so a client polling scores every thirty seconds is not also transferring lineups and per-player statistics it already has.

Each detail route reports its own coverage. A competition without statistics coverage returns the match with an explicit coverage flag and an empty collection rather than a 404 — an empty result and an unsupported competition are different answers, and the response says which one you received.

Method Path Purpose
GET /api/v1/football/matches/{match_id}/stats Team statistics: corners, shots, possession, cards, passes, expected goals.
GET /api/v1/football/matches/{match_id}/live-stats Same shape, cached briefly for matches in progress.
GET /api/v1/football/matches/{match_id}/lineups Starting eleven, bench, formation, coach.
GET /api/v1/football/matches/{match_id}/timeline Ordered goals, cards, substitutions, VAR decisions, each with a sequence number.
GET /api/v1/football/matches/{match_id}/live-timeline Same events, short cache, for in-play matches.
GET /api/v1/football/matches/{match_id}/player-stats Per-player minutes, rating, started and played flags, and canonical statistics.
GET /api/v1/football/matches/{match_id}/live-player-stats Same shape for matches in progress.
GET /api/v1/football/matches/{match_id}/referee Observed referee display name, or available=false with referee=null. No referee career statistics or stable referee identifier.
GET /api/v1/football/matches/{match_id}/odds Locally observed prematch prices with explicit availability and expiry.
GET /api/v1/football/matches/{match_id}/odds/live Locally observed in-play prices; bookmaker attribution is null.
GET /api/v1/football/matches/{match_id}/odds/players Supported prematch player-market selections; player IDs remain null without a verified mapping.

Check what a match has before you ask

Every match returned by the list and by match detail carries details_available: four booleans named timeline, lineups, statistics and player_statistics. A flag is true when Stats API currently holds that detail for that exact match, so a client can skip a request that would return nothing. Every admitted request counts toward your allowance, including one that returns an empty result.

False means nothing is stored right now. The match may not have been played, the competition may not be covered, or the detail may not have been collected yet; the flag does not say which. It is also different from the coverage block inside each detail response, which reports whether the data source covers the competition and season. Odds are not part of details_available because they expire; the odds endpoints report their own availability state.

Example
GET /api/v1/football/matches/match_a14c38236c0e
Authorization: Bearer sapi_live_...

{
  "data": {
    "id": "match_a14c38236c0e",
    "status": "finished",
    "details_available": {
      "timeline": true,
      "lineups": true,
      "statistics": true,
      "player_statistics": false
    }
  }
}

Who started, and who played

Each player in the per-player match statistics carries started and played next to minutes and is_substitute. started is always true or false. played is deliberately three-valued, because no minutes reported is not the same as did not play.

Field Value Meaning
started true In the starting eleven, including a starter who was later replaced. Taken from the lineup: where it names a team's eleven, a player outside it did not start. Only a team with no named eleven falls back to the provider's substitute flag, which is not always reliable.
started false Began on the bench.
played true Minutes above zero, or a recorded substitution brought the player on (a stoppage-time substitute can arrive with no minutes).
played false Only with clear evidence: the match is finished, the player began on the bench with no minutes reported, and substitutions are recorded but none brought this player on.
played null Cannot be told: a starter whose minutes are missing, an explicit 0, a match still in progress, or a match whose timeline records no substitutions.
Example
GET /api/v1/football/matches/match_a14c38236c0e/player-stats
Authorization: Bearer sapi_live_...

(illustrative values, trimmed to the fields that matter here)
{
  "data": {
    "teams": [
      {
        "players": [
          { "player": { "name": "Example Starter" },    "minutes": 90,   "is_substitute": false, "started": true,  "played": true },
          { "player": { "name": "Example Substitute" }, "minutes": 23,   "is_substitute": true,  "started": false, "played": true },
          { "player": { "name": "Example Reserve" },    "minutes": null, "is_substitute": true,  "started": false, "played": false }
        ]
      }
    ]
  }
}

Keep the provider's order of events

Each timeline event carries sequence, its position in the provider's current event list for the match, starting at 1. Sorting by sequence keeps the provider's order between events in the same minute, such as two events at 45+2.

sequence is an ordinal, not an identifier. While a match is live, or after the provider corrects its feed, events can be added or removed and the numbering can change, so do not store it as a key.

Use odds only within their observed availability

Odds requests read local snapshots and never trigger a provider call. A known match without a snapshot returns not_collected; this is different from an unknown match, which returns 404. Inspect availability, stale, source_updated_at, expires_at, flags and selection suspension before using a price.

Prematch observations expire at kickoff or six hours from the earlier source/ingestion time. Live observations expire after 120 seconds from the earlier source/ingestion time. Missing timestamps, failed collection and suspended prices are explicit. These checks describe stored evidence and do not promise that collection will arrive within that window.

Odds responses bypass the response cache so a previously available snapshot cannot remain available just because a cache entry survived. Stale observations may remain in the response for diagnostics. Bookmaker and market coverage varies; the live feed has no verified bookmaker attribution, and the player view filters supported prematch goal-scorer markets rather than promising every player prop.

Worked example: reading corners

Request team statistics for a single match. Every documented field is always present; a statistic the source did not supply is null rather than absent, so a client never has to distinguish a missing key from a missing value.

Example
GET /api/v1/football/matches/match_a14c38236c0e/stats
Authorization: Bearer sapi_live_...

{
  "data": {
    "match_id": "match_a14c38236c0e",
    "status": "finished",
    "coverage": { "statistics_available": true },
    "teams": [
      {
        "team": { "id": "team_f2c85c3c8cb2", "name": "Colorado Rapids" },
        "statistics": {
          "corners": 7,
          "shots_total": 14,
          "shots_on_target": 4,
          "possession_percent": 64,
          "passes_accuracy_percent": 86,
          "expected_goals": 1.22,
          "red_cards": null
        }
      }
    ]
  }
}

Statistic field semantics

Field names are canonical to Stats API, not inherited from any upstream provider. That is deliberate: a provider change must not rename a field your code reads.

Field Type Meaning
corners integer Corner kicks awarded to the team.
shots_total integer All goal attempts, including blocked and off target.
shots_on_target integer Attempts that would enter the goal without intervention.
shots_inside_box integer Attempts taken inside the penalty area.
possession_percent number Share of possession, 0-100. Both teams sum to roughly 100.
passes_accuracy_percent number Completed passes as a percentage of attempted.
expected_goals number Cumulative xG. Available where the source supplies it.
yellow_cards / red_cards integer Cards shown. Null means not reported, not zero.

Error cases and how to tell them apart

Three outcomes look similar from the outside and mean different things. Handling them identically is the most common integration mistake.

Situation Response Correct handling
Match ID does not exist 404 not_found The identifier is wrong. Do not retry.
Competition has no statistics coverage 200 with coverage.statistics_available false and empty teams Show "not covered". Do not treat as an error or retry.
Match has not been played 200 with empty statistics Poll again after full time.
Key lacks an active entitlement 403 subscription_inactive Renew the plan; retrying will not help.
Per-minute limit exceeded 429 rate_limit with Retry-After Back off for the stated interval.

Filters

Parameter Format
competition_id Stats API competition ID.
season_id Stats API season ID.
team_id Matches where the team is home or away.
status scheduled, live, finished, postponed, canceled, or unknown.
date_from Inclusive YYYY-MM-DD UTC date.
date_to Inclusive YYYY-MM-DD UTC date.
page / limit Positive page; limit 1–100.
Example
GET /api/v1/football/matches?team_id=tm_example&date_from=2026-08-01&date_to=2026-08-31

Freshness boundary

OpenFootball is a community-maintained historical fixtures and results source, not a guaranteed live-score feed. A match updated_at value records when that normalized row last changed during Stats API ingestion; it is not a timestamp published by the source, and an identical re-import does not advance it.

The beta serves each dataset as Stats API observed it at the latest successful ingestion, with no promised maximum age or refresh interval. GET /api/v1/coverage/summary reports dataset-level last_success_at, artifact_retrieved_at, and records_seen. For OpenFootball, source_updated_at is null and freshness_guaranteed is false.

Each ingestion run separately records the retrieved artifact SHA-256, byte count, and retrieval time for audit. Retrieval and success times prove only what Stats API imported and when, not when the football facts became current. Match timeline and live-view endpoints are available where covered; endpoint availability is not a guarantee of sub-minute freshness.