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 88 competitions. No key, no account, no SDK.
Live · 377,512 objects stored · last write 33 min ago
{
"event_id": 209549,
"stats": {
"home": {
"duels": 53,
"fouls": 8,
"passes": 291,
"crosses": {
"value": 4,
"total": 18,
"pct": 22
},
"punches": 1,
"tackles": 34,
"dribbles": {
"value": 8,
"total": 14,
"pct": 57
},
"…": "… 51 more"
},
"away": {
"duels": 47,
"fouls": 11,
"passes": 788,
"crosses": {
"value": 5,
"total": 15,
"pct": 33
},
"punches": 0,
"tackles": 14,
"dribbles": {
"value": 17,
"total": 37,
"pct": 46
},
"…": "… 51 more"
},
"first_half": {
"home": {
"duels": 56,
"fouls": 4,
"passes": 159,
"crosses": {
"value": 4,
"total": 13,
"pct": 31
},
"tackles": 18,
"dribbles": {
"value": 5,
"total": 7,
"pct": 71
},
"offsides": 2,
"…": "… 38 more"
},
"away": {
"duels": 44,
"fouls": 6,
"passes": 324,
"crosses": {
"value": 2,
"total": 9,
"pct": 22
},
"tackles": 4,
"dribbles": {
"value": 8,
"total": 17,
"pct": 47
},
"offsides": 0,
"…": "… 38 more"
}
},
"second_half": {
"home": {
"duels": 51,
"fouls": 4,
"passes": 132,
"crosses": {
"value": 1,
"total": 5,
"pct": 0
},
"tackles": 16,
"dribbles": {
"value": 3,
"total": 7,
"pct": 43
},
"offsides": 1,
"…": "… 38 more"
},
"away": {
"duels": 49,
"fouls": 5,
"passes": 464,
"crosses": {
"value": 3,
"total": 6,
"pct": 50
},
"tackles": 10,
"dribbles": {
"value": 9,
"total": 20,
"pct": 45
},
"offsides": 0,
"…": "… 38 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": "209584",
"date": "2026-09-19T11:30:00+00:00",
"status": "2nd_half",
"league_id": "1",
"season_id": "1058",
"round": 5,
"home_team": "Tottenham Hotspur",
"away_team": "Aston Villa",
"home_id": "9",
"away_id": "3",
"home_score": 0,
"away_score": 1,
"venue_id": "9"
},
{
"id": "209582",
"date": "2026-09-19T14:00:00+00:00",
"status": "notstarted",
"league_id": "1",
"season_id": "1058",
"round": 5,
"home_team": "Newcastle United",
"away_team": "Hull City",
"home_id": "4",
"away_id": "204",
"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 support and a redistribution licence.