Football data API
Football data,
already shaped.
Fixtures, live scores, tables, match stats with xG, lineups, incidents, odds and model predictions — one consistent JSON object per match, across 53 competitions. No key, no account, no SDK.
Live · 304,140 objects stored · last write 33 min ago
{
"event_id": 209549,
"stats": {
"home": {
"duels": 51,
"fouls": 8,
"passes": 281,
"crosses": {
"value": 5,
"total": 18,
"pct": 28
},
"punches": 1,
"tackles": 32,
"dribbles": {
"value": 8,
"total": 15,
"pct": 53
},
"…": "… 50 more"
},
"away": {
"duels": 49,
"fouls": 10,
"passes": 779,
"crosses": {
"value": 5,
"total": 15,
"pct": 33
},
"punches": 0,
"tackles": 15,
"dribbles": {
"value": 17,
"total": 35,
"pct": 49
},
"…": "… 50 more"
},
"first_half": {
"home": {
"duels": 55,
"fouls": 4,
"passes": 153,
"crosses": {
"value": 4,
"total": 13,
"pct": 31
},
"tackles": 18,
"dribbles": {
"value": 5,
"total": 7,
"pct": 71
},
"offsides": 2,
"…": "… 35 more"
},
"away": {
"duels": 45,
"fouls": 6,
"passes": 323,
"crosses": {
"value": 2,
"total": 9,
"pct": 22
},
"tackles": 5,
"dribbles": {
"value": 8,
"total": 17,
"pct": 47
},
"offsides": 0,
"…": "… 35 more"
}
},
"second_half": {
"home": {
"duels": 48,
"fouls": 4,
"passes": 128,
"crosses": {
"value": 1,
"total": 5,
"pct": 20
},
"tackles": 14,
"dribbles": {
"value": 3,
"total": 8,
"pct": 38
},
"offsides": 1,
"…": "… 35 more"
},
"away": {
"duels": 52,
"fouls": 4,
"passes": 456,
"crosses": {
"value": 3,
"total": 6,
"pct": 50
},
"tackles": 10,
"dribbles": {
"value": 9,
"total": 18,
"pct": 50
},
"offsides": 0,
"…": "… 35 more"
}
}
},
"xg_estimated": true,
"shotmap": [
{
"gm": {
"x": 0,
"y": 53.2,
"z": 26
},
"xg": 0.0741,
"gml": "high-left",
"min": 84,
"pos": {
"x": 6.3,
"y": 38.7,
"z": 0
},
"sit": "assisted",
"body": "left-foot",
"…": "… 6 more"
},
{
"gm": {
"x": 0,
"y": 64.7,
"z": 16.7
},
"xg": 0.0659,
"gml": "left",
"min": 83,
"pos": {
"x": 20.4,
"y": 61.8,
"z": 0
},
"sit": "fast-break",
"body": "left-foot",
"…": "… 5 more"
},
"… 26 more"
],
"momentum": [
{
"m": 1,
"v": 2
},
{
"m": 2,
"v": 12
},
"… 90 more"
],
"average_positions": {
"away": [
{
"n": 27,
"x": 60.1,
"y": 52.8,
"pos": "F",
"name": "E. Haaland",
"player_id": 852
},
{
"n": 130,
"x": 50.2,
"y": 51,
"pos": "M",
"name": "E. Anderson",
"player_id": 548
},
"… 14 more"
],
"home": [
{
"n": 49,
"x": 42.9,
"y": 85.9,
"pos": "M",
"name": "T. Mitchell",
"player_id": 429
},
{
"n": 43,
"x": 26.9,
"y": 46.7,
"pos": "D",
"name": "C. Richards",
"player_id": 423
},
"… 14 more"
]
},
"xg_per_minute": [
{
"m": 2,
"xg_home": 0.062,
"xg_away": 0,
"cum_home": 0.062,
"cum_away": 0,
"estimated": false
},
{
"m": 3,
"xg_home": 0.029,
"xg_away": 0,
"cum_home": 0.091,
"cum_away": 0,
"estimated": false
},
"… 22 more"
]
}
read live from the API, cached 5 min
Why it's different
A cache you can hammer, not a feed you can exhaust.
Every route reads a store that crons already filled. Your request never reaches the upstream provider, so it can't spend a quota, can't rate-limit someone else, and can't fail because a third party is having an afternoon.
No key, no account
Open CORS, GET-only by contract. Paste a URL in a browser and you have the data. Middleware refuses anything but GET ahead of routing, so a write route can never be added by accident.
Honest timestamps
Every response carries fetched_at. You always know how old the object is, rather than guessing from a cache header someone set once.
Coverage you can see
GET /leagues reports event_count and covered per competition — so you learn the shape of the store up front, not one 404 at a time.
Match stats with xG
Shotmaps with coordinates and per-shot xG, momentum series, average positions, duels, tackles, big chances — the deep object, not a scoreline.
Live, tables, odds
In-play fixtures, league standings, consensus odds plus a bookmaker comparison, Polymarket lines and stored model predictions.
Raw store access
When the shaped endpoints aren't the shape you want, /datasets/{resource} hands back the stored payload untouched, keyed and paginated.
What's inside
23 resources, live counts.
Pulled from GET /resources as you loaded this page — these are the real row counts, not a screenshot from launch day.
Core
Event
League
The fixture shape
One object per match. The same one, everywhere.
Ids are strings, dates are ISO-8601 with an offset, scores are integers or null — never an empty string standing in for a missing number. The list endpoints and the detail endpoints agree.
[
{
"id": "209561",
"date": "2026-09-04T19:00:00+00:00",
"status": "notstarted",
"league_id": "1",
"season_id": "1058",
"round": 3,
"home_team": "Ipswich Town",
"away_team": "Liverpool FC",
"home_id": "200",
"away_id": "1",
"home_score": null,
"away_score": null,
"venue_id": "195"
},
{
"id": "209563",
"date": "2026-09-05T11:30:00+00:00",
"status": "notstarted",
"league_id": "1",
"season_id": "1058",
"round": 3,
"home_team": "Newcastle United",
"away_team": "Bournemouth",
"home_id": "4",
"away_id": "2",
"home_score": null,
"away_score": null,
"venue_id": "4"
}
]
Pricing
Free to explore. Paid when it's load-bearing.
The free tier is the whole store at 120 requests a minute. Paid tiers raise the ceiling and add bulk export, webhooks and a redistribution licence.