Available operations
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/football/matches | Filter and paginate fixtures or results. |
| GET | /api/v1/football/matches/{match_id} | Retrieve one match by Stats API public ID. |
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. |
| 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 and canonical statistics. |
| GET | /api/v1/football/matches/{match_id}/live-player-stats | Same shape for matches in progress. |
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. Live incidents remain outside the beta contract.