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.
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. |
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.
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. |
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.