# Soccer Data API > Football data over REST: competitions, teams, players, fixtures, live scores and stats, lineups, events, xG shot maps, standings and bookmaker odds. --- ## Soccer Data API Football data over a simple REST API, from fixtures and live scores to xG and bookmaker odds. Soccer Data gives you competitions, teams, players, fixtures, results, live scores, league tables, season and match stats, xG shot maps, heatmaps and betting odds. You ask with a plain HTTPS `GET` request and get JSON back. > **Tip:** Building with an AI coding tool? Start with [Build with AI](/docs/build-with-ai). It shows you how to hand your assistant the whole API reference in one file. ### Start here | Page | What you'll find | |---|---| | [Quickstart](/docs/quickstart) | Your first requests in about five minutes. | | [Introduction](/docs/introduction) | What the API covers and how requests and responses look. | | [Authentication](/docs/authentication) | How to send your key and keep it safe. | | [API reference](/docs/reference) | Every endpoint, with parameters, fields and examples. | | [Coverage](/docs/concepts/coverage) | Which competitions and seasons have which data. | ### Guides - [Find any ID](/docs/guides/finding-ids): look up competitions, seasons, teams and players by name. - [Fixtures and results](/docs/guides/fixtures-and-results): upcoming matches, recent scores and matchdays. - [Live matches](/docs/guides/live-matches): scores, events and stats while the ball is rolling. - [League tables](/docs/guides/standings): current and past tables, including tournament groups. - [Player and team stats](/docs/guides/player-and-team-stats): season totals, match stats, shot maps and heatmaps. - [Odds](/docs/guides/odds): pre-match and in-play prices from several bookmakers. ### How it works 1. [Create an account](/register). Your first API key is made for you when you sign up; you can add more on the [API keys](/dashboard/keys) page of your dashboard. 2. Send the key in the `X-Api-Key` header with every request. 3. Call any endpoint under `https://api.soccer-data.co/v1` and read the JSON that comes back. ### Try it This request asks for Arsenal's next scheduled match: ```bash curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` Prefer clicking to typing? The [API tester](/dashboard/tester) in your dashboard runs any endpoint as your signed-in account, with no key to paste, and shows the response. --- ## Introduction What Soccer Data covers, and the conventions every endpoint shares. Soccer Data is a read-only REST API for football. You send `GET` requests over HTTPS with your API key, and every answer is JSON. This page covers what's inside and the few rules that apply everywhere. ### What you can get | Area | What's included | |---|---| | Competitions | Leagues, cups and international tournaments, with their seasons, groups and league tables. | | Teams | Profiles, squads, season records, standings history, injuries and suspensions. | | Players | Profiles, season totals, injuries and suspensions, season heatmaps. | | Matches | Fixtures, results, live scores, lineups, event timelines, team and player stats, referees. | | xG | Shot maps with an xG value for every shot, plus per-match player heatmaps. | | Odds | Pre-match and in-play prices from Bet365, Paddy Power, BetMGM UK, Pinnacle and Betfair Exchange, where they exist. | | Coverage | Which competitions and seasons have which kinds of data. | Not every competition has every kind of data, and how far back the history goes depends on the competition. Competitions and matches carry flags such as `xg_available` and `odds_available` so you can check before you ask. The [Coverage](/docs/concepts/coverage) page explains how to see exactly what exists. ### Base URL Every path in these docs sits under this base URL: ```text https://api.soccer-data.co/v1 ``` ### Authentication Put your API key in the `X-Api-Key` header of each request. Keys belong on your server, never in code that runs in a browser or an app you ship to users. See [Authentication](/docs/authentication) for the details. ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Requests All endpoints use `GET`. Filters go in the query string: ```bash curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=finished" \ -H "X-Api-Key: YOUR_API_KEY" ``` If you send a parameter an endpoint doesn't know, including a typo, you get a `400` with the code `UNKNOWN_PARAMETER`. The message suggests the closest valid parameter, when one is close, and lists the ones the endpoint accepts, so mistakes are quick to fix. ### IDs Each thing in the API has an id that starts with a word saying what it is, such as `competition_`, `season_`, `team_`, `player_` or `match_`, followed by 12 letters and digits. For example, the Premier League is `competition_705uv6jm8myn` and Arsenal is `team_f0ibhxdw7hs5`. Ids never change, so you can store them in your own database. Treat them as plain strings and use them exactly as you received them. [Data model](/docs/concepts/data-model) lists every kind, and [Find any ID](/docs/guides/finding-ids) shows how to look them up. ### Dates and times - Date filters such as `date_from` and `date_to` take `YYYY-MM-DD`. There is no single `date` parameter; to get one day, set both to the same date. - Kickoff times in responses are in UTC, in ISO 8601 format (for example `2026-10-10T11:30:00.000Z`). Convert them to local time in your app. ### Responses List endpoints return a `data` array plus a `meta` object for paging: ```json { "data": [ { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "short_name": "Arsenal", "country": "England", "is_mens_team": true, "primary_competition": { "id": "competition_705uv6jm8myn", "name": "Premier League" } } ], "meta": { "page": 1, "per_page": 1, "total": 13, "total_pages": 13 } } ``` Endpoints that return a single thing put one object in `data`: ```json { "data": { "id": "competition_705uv6jm8myn", "name": "Premier League", "country": "England", "type": "league", "xg_available": true, "current_season_id": "season_hr1ie7yrsdmt", "total_teams": 20 } } ``` [Pagination](/docs/concepts/pagination) explains `meta` and how to walk through every page. ### Errors Every error has the same shape, so one handler covers them all: ```json { "error": { "code": "NOT_FOUND", "message": "Match not found", "status_code": 404 } } ``` Write your logic against `code`, which is stable, rather than the message. [Errors](/docs/concepts/errors) lists every code and what to do about it. ### Limits and freshness Each plan has a per-minute limit and a daily limit, and every response tells you how much you have left in its headers. Answers that change during a match are cached briefly: normally for 15 seconds on paid plans and 5 minutes on Free. If a fresh answer can't be fetched, you may get an older copy marked `X-Cache: STALE`. [Rate limits](/docs/concepts/rate-limits) has the numbers. ### Getting new data The API works by pulling: there are no webhooks or streaming connections. To follow a live match, call the live endpoints at a steady pace. [Live matches](/docs/guides/live-matches) shows how to do that without wasting requests. ### Changes over time We add endpoints, parameters and response fields as the API grows. Write your client so it ignores fields it doesn't recognise, and it won't break when something new appears. ### Next steps - [Quickstart](/docs/quickstart): make your first requests. - [Build with AI](/docs/build-with-ai): let an AI coding tool write the integration. - [Authentication](/docs/authentication): set up your key properly. --- ## Quickstart Make your first requests in five minutes, from finding ids to a team's next match and a league table. By the end of this page you'll have looked up a competition and a team, fetched Arsenal's next match and printed the Premier League table. The examples come in curl, JavaScript and Python. The JavaScript needs Node 18 or later (save it as a `.mjs` file so top-level `await` works), and the Python needs the `requests` package. > **Tip:** Want to see responses before writing any code? The [API tester](/dashboard/tester) in your dashboard runs every endpoint as your signed-in account, so it needs no key. ### 1. Get an API key [Create an account](/register). Your first key is made for you when you sign up, and shown on the dashboard page you land on. Keys start with `sd_`. We show the full key only once, so copy it straight away. If you lose it, or want another, create one on the [API keys](/dashboard/keys) page. Keep the key in an environment variable rather than in your code: ```bash export SOCCER_DATA_KEY="YOUR_API_KEY" ``` > **Warning:** Your key should only live on a server. Anything that runs in a browser or ships inside an app can be read by anyone. [Authentication](/docs/authentication) shows a safe setup. ### 2. Find the competition Search competitions by name. The best match comes first. ```bash curl "https://api.soccer-data.co/v1/competitions?search=Premier%20League&per_page=3" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const params = new URLSearchParams({ search: "Premier League", per_page: "3" }); const res = await fetch(`https://api.soccer-data.co/v1/competitions?${params}`, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); const body = await res.json(); console.log(body.data[0].id); // the Premier League's id ``` ```python import os, requests res = requests.get( "https://api.soccer-data.co/v1/competitions", params={"search": "Premier League", "per_page": 3}, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) print(res.json()["data"][0]["id"]) ``` ```json { "data": [ { "id": "competition_705uv6jm8myn", "name": "Premier League", "country": "England", "type": "league", "xg_available": true }, { "id": "competition_ebdxduk29wtn", "name": "Egyptian Premier League", "country": "Egypt", "type": "league" }, { "id": "competition_zbmq20gkyy8b", "name": "Russian Premier League", "country": "Russia", "type": "league" } ], "meta": { "page": 1, "per_page": 3, "total": 6, "total_pages": 2 } } ``` The Premier League is `competition_705uv6jm8myn`. Ids never change, so you only need to look one up once. ### 3. Find the team Search teams the same way. Add `is_mens_team=true` so women's sides with the same name drop out. ```bash curl "https://api.soccer-data.co/v1/teams?search=Arsenal&is_mens_team=true&per_page=3" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const params = new URLSearchParams({ search: "Arsenal", is_mens_team: "true", per_page: "3" }); const res = await fetch(`https://api.soccer-data.co/v1/teams?${params}`, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); const { data } = await res.json(); for (const team of data) console.log(team.id, team.name); ``` ```python import os, requests res = requests.get( "https://api.soccer-data.co/v1/teams", params={"search": "Arsenal", "is_mens_team": "true", "per_page": 3}, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) for team in res.json()["data"]: print(team["id"], team["name"]) ``` ```json { "data": [ { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "country": "England", "is_mens_team": true }, { "id": "team_oejc59y73fs9", "name": "Arsenal U21", "country": "England", "is_mens_team": true }, { "id": "team_fg5femf0es8u", "name": "Arsenal Tula", "country": "Russian Federation", "is_mens_team": true } ], "meta": { "page": 1, "per_page": 3, "total": 13, "total_pages": 5 } } ``` Arsenal's first team is `team_f0ibhxdw7hs5`. ### 4. Get the next match Ask for the team's scheduled matches, sorted by kickoff, one per page. ```bash curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const params = new URLSearchParams({ team_id: "team_f0ibhxdw7hs5", status: "scheduled", sort: "utc_date", per_page: "1", }); const res = await fetch(`https://api.soccer-data.co/v1/matches?${params}`, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); const [next] = (await res.json()).data; console.log(`${next.home_team.name} v ${next.away_team.name}, ${next.utc_date}`); ``` ```python import os, requests res = requests.get( "https://api.soccer-data.co/v1/matches", params={"team_id": "team_f0ibhxdw7hs5", "status": "scheduled", "sort": "utc_date", "per_page": 1}, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) match = res.json()["data"][0] print(f'{match["home_team"]["name"]} v {match["away_team"]["name"]}, {match["utc_date"]}') ``` ```json { "data": [ { "id": "match_yoeml0e6ae37", "competition_id": "competition_705uv6jm8myn", "season_id": "season_hr1ie7yrsdmt", "matchday": 6, "status": "scheduled", "utc_date": "2026-10-10T11:30:00.000Z", "home_team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal" }, "away_team": { "id": "team_w63vzmngfyz9", "name": "Leeds United" }, "score": { "home": null, "away": null, "winner": null }, "odds_available": true, "xg_available": false } ], "meta": { "page": 1, "per_page": 1, "total": 9, "total_pages": 9 } } ``` Two things to know about `/matches`: - Without `sort`, it lists the latest kickoff first, so for upcoming games `sort=utc_date` matters. - `utc_date` is always in UTC. For the most recent result instead, use `status=finished` and leave `sort` out. ### 5. Get the league table A table belongs to a season, so first read the competition's `current_season_id`, then ask for that season's standings. ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ## data.current_season_id is season_hr1ie7yrsdmt curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons/season_hr1ie7yrsdmt/standings" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const headers = { "X-Api-Key": process.env.SOCCER_DATA_KEY }; const base = "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn"; const competition = (await (await fetch(base, { headers })).json()).data; const table = (await (await fetch( `${base}/seasons/${competition.current_season_id}/standings`, { headers }, )).json()).data; for (const row of table) console.log(row.position, row.team.name, row.points); ``` ```python import os, requests headers = {"X-Api-Key": os.environ["SOCCER_DATA_KEY"]} base = "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" season_id = requests.get(base, headers=headers).json()["data"]["current_season_id"] table = requests.get(f"{base}/seasons/{season_id}/standings", headers=headers).json()["data"] for row in table: print(row["position"], row["team"]["name"], row["points"]) ``` ```json { "data": [ { "team": { "id": "team_hof12pbei216", "name": "Manchester City" }, "position": 1, "matches_played": 5, "wins": 5, "draws": 0, "losses": 0, "goals_for": 13, "goals_against": 5, "goal_difference": 8, "points": 15, "group_label": null }, { "team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal" }, "position": 2, "matches_played": 5, "wins": 4, "draws": 0, "losses": 1, "goals_for": 8, "goals_against": 4, "goal_difference": 4, "points": 12, "group_label": null } ] } ``` ### Common mistakes | What went wrong | What you see | How to fix it | |---|---|---| | Sending `date=2026-10-10` | `400 UNKNOWN_PARAMETER` | Use `date_from` and `date_to`, both set to the same day. | | Leaving out `sort=utc_date` for upcoming matches | The match furthest in the future comes first | Add `sort=utc_date`. | | Missing or mistyped key header | `401 UNAUTHORIZED` | Send `X-Api-Key: YOUR_API_KEY`. | | Guessing or typing ids by hand | `404 NOT_FOUND` | Look ids up with `search`. | ### Next steps - [Build with AI](/docs/build-with-ai): let your coding assistant take it from here. - [Find any ID](/docs/guides/finding-ids): every way to look up ids. - [Fixtures and results](/docs/guides/fixtures-and-results) and [League tables](/docs/guides/standings): more of what you just did. - [Live matches](/docs/guides/live-matches): follow games in play. - [Pagination](/docs/concepts/pagination): read lists longer than one page. - [API reference](/docs/reference): every endpoint. --- ## Build with AI Give your AI coding tool the full Soccer Data reference and a good starting prompt, and let it write the integration. AI coding tools such as Claude Code, Codex, Cursor and Lovable can build a working Soccer Data integration in minutes, as long as they know the real endpoints. Without a reference they guess, and guessed parameter names fail. This page shows how to give them what they need. ### Why this works We publish the whole documentation, including every endpoint with its parameters, response fields and an example, as one plain-text file: ```text https://soccer-data.co/llms.txt ``` An assistant that reads this file uses the exact paths and parameter names. And when it does slip, the API helps it recover: an unknown parameter returns `400 UNKNOWN_PARAMETER` with the closest valid name and the full list of accepted parameters in the message, which is enough for most tools to fix the call themselves. ### Set up in three steps #### 1. Create a key and store it safely [Create an account](/register) and copy the key you're given at sign-up (you can create more on the [API keys](/dashboard/keys) page). Put it in a `.env` file at the root of your project, and make sure git ignores that file: ```bash echo 'SOCCER_DATA_KEY=YOUR_API_KEY' >> .env echo '.env' >> .gitignore ``` > **Warning:** Never paste your key into the chat with your assistant, and never let it end up in browser code. Keep it in `.env` or your tool's secrets settings, and have only your server read it. #### 2. Give the assistant the reference Either paste the URL `https://soccer-data.co/llms.txt` into your prompt, or save a copy inside your project so the assistant can read it offline: ```bash mkdir -p docs curl -o docs/soccer-data-llms.txt https://soccer-data.co/llms.txt ``` #### 3. Start with a clear prompt The [Build with AI](/dashboard/build-with-ai) page in your dashboard has a starter prompt with your base URL and the reference link already filled in, plus ideas to build. Copy it, describe what you want, and send it. If you'd rather write your own, these are the rules worth giving the assistant: ```text I'm building with the Soccer Data football API. Read the reference first: https://soccer-data.co/llms.txt Rules: - Base URL: https://api.soccer-data.co/v1 - Send the key in the X-Api-Key header. Read it from the SOCCER_DATA_KEY environment variable on the server. Never put it in browser code, in NEXT_PUBLIC_ or VITE_ variables, or in git. - The frontend calls my own server route; only that route calls Soccer Data. - Ids are strings like team_... or match_.... Look them up with the search parameter on /competitions, /teams and /players. Never invent them. - Dates use date_from and date_to (YYYY-MM-DD). There is no date parameter. - /matches lists the latest kickoff first. For upcoming matches use status=scheduled and sort=utc_date. - Lists are paged with page and per_page (up to 100). Stop at meta.total_pages. - Errors look like {"error": {"code", "message", "status_code"}}. Branch on code. - 400 UNKNOWN_PARAMETER means a wrong parameter name: use the one the message suggests. - On 429 or 503, wait for the seconds in the Retry-After header, then retry. - Cache responses. There are no webhooks, so poll live data only while a match is live. - Check flags such as has_player_stats, xg_available, shotmap_available and odds_available before calling endpoints that depend on them. - History depth differs by competition: check /coverage/leagues. First test: fetch Arsenal's next match (team_id team_f0ibhxdw7hs5). What I want to build: [describe it here] ``` ### Tool by tool Most tools read a notes file from your project on every session. Put the rules above in that file so you don't have to repeat them. | Tool | Setup | |---|---| | Claude Code | Save the reference and `.env` in the project first, put the rules in `CLAUDE.md` at the root, then run `claude`. You can also block it from reading `.env` by adding deny rules for `./.env` and `./.env.*` in `.claude/settings.json`. | | Codex | Download the reference and create `.env` before you start, because the default sandbox has no network access. Put the rules in `AGENTS.md`. To let it make test calls, approve the network request when it asks, or set `network_access = true` under `[sandbox_workspace_write]` in `~/.codex/config.toml`. | | Cursor | Save the reference, create `.env` and put the rules in `AGENTS.md`. Mention `@docs/soccer-data-llms.txt` in chat to point at the reference. Don't add the reference file to `.gitignore`: Cursor leaves ignored files out of what it can see. | | Lovable | Paste the rules into Project settings, under Knowledge. Store the key with the Add secret form, not in chat, and call the API from a server function or Edge Function. | | v0, Replit and other builders | Use the tool's secrets or environment settings for the key. Avoid `NEXT_PUBLIC_` and `VITE_` variables, since those end up in browser code. | ### Ideas to try - A live scores widget for today's Premier League matches that only refreshes while games are in play. - A league table page built from the current season's standings. - A fantasy points calculator that scores players from a finished match's player stats. - An xG shot map for a match, shown only when `shotmap_available` is true. - An odds comparison for the weekend's fixtures that skips matches without prices. ### Tips - **Paste errors back.** If a call fails, give the assistant the full error JSON. The `message` usually says exactly what to change. - **Use real ids.** Get them from a `search` request, and include them in your prompt. - **Watch your limits.** A retry loop or a tight polling loop can use up a day's requests quickly. Ask the assistant to cache responses and to respect `Retry-After`. Your [usage page](/dashboard/usage) shows what's been spent. - **Check coverage first.** Before building something that needs xG, player stats or odds, make sure the competition has them. See [Coverage](/docs/concepts/coverage). ### Next steps - [Quickstart](/docs/quickstart): the same first requests, by hand. - [Find any ID](/docs/guides/finding-ids): every way to look up ids. - [Errors](/docs/concepts/errors): what each error code means. - [API reference](/docs/reference): every endpoint. --- ## Authentication Send your API key with every request, and keep it on your server. Every request needs an API key. You send it in a header, we check it, and the request counts against your plan. ### Sending your key Put the key in the `X-Api-Key` header: ```bash curl "https://api.soccer-data.co/v1/competitions?per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const res = await fetch("https://api.soccer-data.co/v1/competitions?per_page=1", { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); console.log(res.status, await res.json()); ``` ```python import os, requests res = requests.get( "https://api.soccer-data.co/v1/competitions", params={"per_page": 1}, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) print(res.status_code, res.json()) ``` ```php $response = Http::withHeaders(['X-Api-Key' => env('SOCCER_DATA_KEY')]) ->get('https://api.soccer-data.co/v1/competitions', ['per_page' => 1]); ``` If your HTTP client is already set up for bearer tokens, `Authorization: Bearer YOUR_API_KEY` works too. Use one or the other. > **Note:** Keys only work in a header. A key added to the URL as a query parameter is ignored, and the request fails with `401`. ### Managing keys Your first key is made for you when you sign up. Create more, and revoke them, on the [API keys](/dashboard/keys) page of your dashboard. - Keys start with `sd_`. - Give each key a name, such as the app or environment that uses it, so you can tell them apart later. - We show a new key in full only once. Copy it before you leave the page. - You can hold up to 10 active keys at a time. - All your keys share your account's per-minute and daily limits. See [Rate limits](/docs/concepts/rate-limits). #### Revoking and rotating Revoking a key stops it at once; any request that still uses it gets `401`. If a key might have leaked, rotate it without downtime: 1. Create a new key. 2. Put the new key on your server and deploy. 3. Revoke the old key. ### Keeping your key safe Your key is tied to your account and your plan. Anyone who has it can spend your requests. - **Call the API from your server.** If you have a website or a mobile app, have it call a route on your own server, and let only that route talk to Soccer Data. Anything that runs in a browser can be read by visitors in the page source or the network tab. - **Read the key from an environment variable.** Don't hard-code it, and add `.env` to `.gitignore`. - **Watch out for public variables.** Variables that start with `NEXT_PUBLIC_` or `VITE_` are bundled into browser code, so they are no place for a key. - **Use your platform's secrets store.** In tools like Lovable, use the Add secret form and call the API from a server function, since the project's `.env` file may be committed. - **Keep it out of sight.** Don't paste the key into logs, screenshots, support tickets or AI chats. #### Environment variables ```bash ## .env (keep this file out of git) SOCCER_DATA_KEY=YOUR_API_KEY ``` ```javascript // config.js export const apiKey = process.env.SOCCER_DATA_KEY; if (!apiKey) throw new Error("SOCCER_DATA_KEY is not set"); ``` In Node.js 20.6 or later, `node --env-file=.env app.js` loads the file for you; on older versions use the `dotenv` package. In Python, `python-dotenv` does the same job before you read `os.environ["SOCCER_DATA_KEY"]`. #### A server route in Next.js This route handler keeps the key on the server. Your page calls `/api/next-fixture`, and only the server talks to Soccer Data: ```javascript // app/api/next-fixture/route.js export async function GET() { const params = new URLSearchParams({ team_id: "team_f0ibhxdw7hs5", status: "scheduled", sort: "utc_date", per_page: "1", }); const res = await fetch(`https://api.soccer-data.co/v1/matches?${params}`, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); return Response.json(await res.json(), { status: res.status }); } ``` ### When something goes wrong Authentication problems use the same error shape as everything else: ```json { "error": { "code": "UNAUTHORIZED", "message": "Send your API key in the X-Api-Key header. You can create one in your dashboard.", "status_code": 401 } } ``` | Status | Code | What it means | What to do | |---|---|---|---| | 401 | `UNAUTHORIZED` | No key was sent. | Add the `X-Api-Key` header. | | 401 | `UNAUTHORIZED` | The key is wrong or has been revoked. | Check for missing characters or extra spaces, or create a new key. | | 403 | `PLAN_UPGRADE_REQUIRED` | The endpoint isn't part of your plan. Odds need a paid plan. | Upgrade on the [plans page](/dashboard/subscription), or use another endpoint. | | 429 | `RATE_LIMITED` or `USAGE_LIMIT_EXCEEDED` | You've hit the per-minute or daily limit. | Wait the number of seconds in `Retry-After`. | > **Note:** Too many requests with invalid keys from the same address within a minute get a `429 RATE_LIMITED` for a short while. Fix the key, then wait for `Retry-After`. Once your key is accepted, every response, errors included, carries your rate limit headers. [Rate limits](/docs/concepts/rate-limits) explains them. ### Next steps - [Quickstart](/docs/quickstart): your first requests. - [Errors](/docs/concepts/errors): every error code. - [Build with AI](/docs/build-with-ai): set up an AI coding tool without exposing your key. --- ## Find any ID Look up competition, season, team, player and match ids by name or filter. Almost every endpoint asks for an id: which competition, which season, which team. This guide shows how to find each one. You only need to do it once per thing, because ids never change and are safe to store. All examples send your key in the `X-Api-Key` header. Keep that key in server-side code. ### The kinds of id | Starts with | Points to | Example | |---|---|---| | `competition_` | A league, cup or tournament | `competition_705uv6jm8myn` (Premier League) | | `season_` | One season of a competition | `season_hr1ie7yrsdmt` (Premier League 26/27) | | `team_` | A club or national team | `team_f0ibhxdw7hs5` (Arsenal men) | | `player_` | A player | `player_ul3vc6om97u5` (Bukayo Saka) | | `match_` | A single match | `match_yoeml0e6ae37` (Arsenal v Leeds United) | Ids are strings. The word at the start tells you what kind of thing the id names, and the 12 characters after it identify which one. [Data model](/docs/concepts/data-model) lists a few more kinds, such as shots, managers and referees. ### Competitions Search by name. The closest match comes first; add `sort=name` if you'd rather have them A to Z. ```bash curl "https://api.soccer-data.co/v1/competitions?search=Premier&per_page=3" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "id": "competition_705uv6jm8myn", "name": "Premier League", "country": "England", "country_code": "GB", "subdivision_code": "GB-ENG", "type": "league", "xg_available": true } ], "meta": { "page": 1, "per_page": 3, "total": 6, "total_pages": 2 } } ``` You can also filter instead of searching, for example `country_code=GB-ENG&type=league` for English leagues, or `country=Spain` for everything in Spain. ### Seasons To get the season that's running now, fetch the competition and read `current_season_id`: ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "id": "competition_705uv6jm8myn", "name": "Premier League", "current_season_id": "season_hr1ie7yrsdmt" } } ``` For older seasons, list them all. The newest comes first, and the current one has `is_current: true`. ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "id": "season_hr1ie7yrsdmt", "name": "Premier League 26/27", "year": "26/27", "is_current": true }, { "id": "season_98l95mxugqs9", "name": "Premier League 25/26", "year": "25/26", "is_current": false } ] } ``` > **Warning:** A season id belongs to one competition. If you pair it with a different `competition_id`, you get a `400` saying the season doesn't belong to that competition. ### Teams ```bash curl "https://api.soccer-data.co/v1/teams?search=Arsenal&per_page=5" \ -H "X-Api-Key: YOUR_API_KEY" ``` A search like this can return several teams with the same name: ```json { "data": [ { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "is_mens_team": true, "primary_competition": { "id": "competition_705uv6jm8myn", "name": "Premier League" } }, { "id": "team_co5mh4glxs4y", "name": "Arsenal", "is_mens_team": false, "primary_competition": { "id": "competition_5y6lo1sgiuz9", "name": "Women's Super League" } }, { "id": "team_oejc59y73fs9", "name": "Arsenal U21", "is_mens_team": true, "primary_competition": { "id": "competition_9z5r9s59u6ch", "name": "Football League Trophy" } } ], "meta": { "page": 1, "per_page": 5, "total": 13, "total_pages": 3 } } ``` To tell them apart: - **Men's or women's:** add `is_mens_team=true` or `is_mens_team=false`. The field is `null` when we don't know. - **First team or youth side:** look at `primary_competition`. Arsenal's first team plays in the Premier League; the U21 side doesn't. To get every team in one season, filter by both competition and season. This returns the 20 Premier League clubs: ```bash curl "https://api.soccer-data.co/v1/teams?competition_id=competition_705uv6jm8myn&season_id=season_hr1ie7yrsdmt" \ -H "X-Api-Key: YOUR_API_KEY" ``` ### Players Search by name. You need at least three characters, and the search ignores case and accents, so `saka` works. ```bash curl "https://api.soccer-data.co/v1/players?search=saka&per_page=2" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka", "position": "F", "date_of_birth": "2001-09-05", "nationality": "England", "current_team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "jersey_number": 7 } } ], "meta": { "page": 1, "per_page": 2, "total": 1, "total_pages": 1 } } ``` Other ways to find players: - **By team:** `/players?team_id=team_f0ibhxdw7hs5`. Add `position=F` (or `G`, `D`, `M`) to narrow it down. - **A full squad:** `/teams/team_f0ibhxdw7hs5/players` returns the current squad in one response. - **Several at once:** `/players?player_ids=player_ul3vc6om97u5,player_zpfc5xvtigeo` with comma-separated ids. ### Matches Matches are found by filtering rather than searching. For example, a team's next match: ```bash curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` You can also filter by competition, season, date range, matchday and more. [Fixtures and results](/docs/guides/fixtures-and-results) covers them all. ### Country codes `country_code` takes ISO codes such as `DE` or `ES`. For the UK, `GB` covers all four nations; use `GB-ENG`, `GB-SCT`, `GB-WLS` or `GB-NIR` for just one. If you'd rather use names, the `country` parameter takes them, as in `country=Spain`. ### Common mistakes - **Typing ids by hand or guessing them.** An id we don't recognise returns `404 NOT_FOUND`. Copy ids from a response. - **Mixing up seasons and competitions.** A season id only works with its own competition. - **Picking the wrong Arsenal.** Check `is_mens_team` and `primary_competition` before you store a team id. - **Searching players with fewer than three characters.** Use a longer name. ### Next steps - [Fixtures and results](/docs/guides/fixtures-and-results) - [League tables](/docs/guides/standings) - [Data model](/docs/concepts/data-model) --- ## Fixtures and results Upcoming matches, recent scores, a day's games, matchdays and tournament rounds, all from one endpoint. Fixtures and results both come from [`/matches`](/docs/reference/matches/list). You choose what you get with filters: a team, a competition, a date range, a status. This guide walks through the questions people ask most. Every example sends your key in the `X-Api-Key` header. Keep that key on your server. ### A team's next match Ask for scheduled matches, soonest first, one per page: ```bash curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "id": "match_yoeml0e6ae37", "competition_id": "competition_705uv6jm8myn", "season_id": "season_hr1ie7yrsdmt", "matchday": 6, "status": "scheduled", "utc_date": "2026-10-10T11:30:00.000Z", "home_team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal" }, "away_team": { "id": "team_w63vzmngfyz9", "name": "Leeds United" }, "score": { "home": null, "away": null, "winner": null }, "odds_available": true, "live_odds_available": false } ], "meta": { "page": 1, "per_page": 1, "total": 9, "total_pages": 9 } } ``` Change `per_page=1` to `per_page=5` for the next five. `meta.total` tells you how many scheduled matches the team has in the data. ### A team's latest results Ask for finished matches and leave `sort` out. The default order already puts the most recent kickoff first. ```bash curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=finished&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "id": "match_nbqbxh7kv0q8", "status": "finished", "utc_date": "2026-09-19T14:00:00.000Z", "home_team": { "id": "team_v70dqyg1h8b0", "name": "Brighton & Hove Albion" }, "away_team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal" }, "score": { "home": 3, "away": 0, "regulation": { "home": 3, "away": 0 }, "went_to_extra_time": false, "went_to_penalties": false, "winner": "home" }, "xg_available": true, "shotmap_available": true } ], "meta": { "page": 1, "per_page": 1, "total": 561, "total_pages": 561 } } ``` For the venue, referee and half-time score of a single match, call [`/matches/{match_id}`](/docs/reference/matches/show). ### One day's matches, in your time zone Set `date_from` and `date_to` to the same day. Add `utc_offset` when "the day" should mean your local day rather than the UTC one: ```bash curl "https://api.soccer-data.co/v1/matches?competition_id=competition_705uv6jm8myn&date_from=2026-10-10&date_to=2026-10-10&utc_offset=%2B10:00&sort=utc_date" \ -H "X-Api-Key: YOUR_API_KEY" ``` The offset changes which matches fall inside the day. In UTC, 10 October 2026 has six Premier League matches. At `+10:00`, the local day runs from 14:00 UTC on 9 October to 14:00 UTC on 10 October, so only Arsenal v Leeds United (11:30 UTC) is included. > **Warning:** In a URL, a bare `+` is read as a space, and the request fails with `400 BAD_REQUEST`. Write `+` as `%2B`, as above, or build the query with `URLSearchParams` or your HTTP library's params option. Negative offsets like `-05:00` need no encoding. Kickoff times in the response stay in UTC either way. Convert them for display in your app. ### A league matchday ```bash curl "https://api.soccer-data.co/v1/matches?competition_id=competition_705uv6jm8myn&season_id=season_hr1ie7yrsdmt&matchday=6" \ -H "X-Api-Key: YOUR_API_KEY" ``` This returns the ten matches of Premier League matchday 6. ### Tournament groups and knockouts For a group in a tournament, pass `group` along with the competition and season. This returns the six Group A matches of the 2022 World Cup: ```bash curl "https://api.soccer-data.co/v1/matches?competition_id=competition_7vpwx91c7j7h&season_id=season_st2tyewgtq2j&group=A&sort=utc_date" \ -H "X-Api-Key: YOUR_API_KEY" ``` Use `stage` to split a season into parts: - `stage=playoff`: knockout rounds and play-offs only (16 matches for the 2022 World Cup). - `stage=regular`: league and group matches only. [`/competitions/{competition_id}/seasons/{season_id}/groups`](/docs/reference/competitions/groups) lists the group labels a season has. ### How far ahead fixtures go Later rounds of a season aren't always in the data yet. To see how far it reaches, ask for the season's scheduled matches without a `sort`: ```bash curl "https://api.soccer-data.co/v1/matches?competition_id=competition_705uv6jm8myn&season_id=season_hr1ie7yrsdmt&status=scheduled&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` The first row is the furthest fixture we have, and `meta.total` is how many scheduled matches are loaded. [Coverage](/docs/concepts/coverage) gives the same picture for every season. ### Getting every page Lists come 20 to a page by default. To read everything, ask for `per_page=100` and keep increasing `page` until it reaches `meta.total_pages`. [Pagination](/docs/concepts/pagination) has ready-made loops. Each page is one request, so filter as tightly as you can first. ### Filters at a glance | Parameter | What it does | |---|---| | `team_id` | Matches the team plays in, at home or away. | | `competition_id`, `season_id` | One competition, one season. The season must belong to the competition. | | `status` | `scheduled`, `live`, `finished`, `postponed` or `cancelled`. | | `date_from`, `date_to` | Kickoff date range, `YYYY-MM-DD`, both ends included. There's no `date` parameter. | | `utc_offset` | The time zone the dates are read in, such as `-05:00`. Defaults to UTC. | | `sort` | `-utc_date` (latest first, the default) or `utc_date` (soonest first). | | `matchday` | A league round. | | `stage` | `regular`, `playoff` or `all`. | | `group` | A tournament group, such as `A`. Needs `competition_id`. | | `page`, `per_page` | Paging. 20 per page by default, 100 at most. | The [`/matches` reference](/docs/reference/matches/list) has the full details. ### Reading the score - `score.home` and `score.away` are the goals scored in normal time. They're `null` before kickoff. - `score.winner` is `home`, `away` or `draw` once the match is over, and takes extra time and penalties into account. - `score.regulation` repeats the normal-time score; `went_to_extra_time` and `went_to_penalties` say how far the match went. - `score.after_extra_time` and `score.penalty_shootout` only appear when a match got that far. ### Good to know - `utc_date` is always UTC. - Matches that kick off at the same moment always come back in the same order, so paging through them is stable. - Every request counts against your limits, including each page. Cache what doesn't need to be fresh, such as finished results. ### Common mistakes - **Leaving out `sort=utc_date` when you want the next match.** You get the match furthest in the future instead. - **A bare `+` in `utc_offset`.** Encode it as `%2B`. - **Sending `date`.** It returns `400 UNKNOWN_PARAMETER`. The message suggests the closest valid parameter and lists the ones the endpoint takes, `date_from` and `date_to` among them. - **Sending `group` without `competition_id`, or `per_page` above 100.** Both return `400`. - **Assuming the whole season is loaded.** Check how far fixtures go before you promise them to users. ### Related endpoints - [List matches](/docs/reference/matches/list) - [Get a match](/docs/reference/matches/show) - [List groups in a season](/docs/reference/competitions/groups) - [Coverage by competition](/docs/reference/coverage/league) --- ## Live matches Find the matches in play, then follow their score, events and stats with a steady polling loop. Following a live match takes two steps: find out which matches are in play, then poll the live endpoints for the ones you care about. There are no webhooks or streaming connections, so your code asks at a steady pace and stops at full time. Every example sends your key in the `X-Api-Key` header. Keep that key on your server. ### Which matches are live Ask [`/matches`](/docs/reference/matches/list) for `status=live`. On a busy day there can be more than the default 20, so raise `per_page`: ```bash curl "https://api.soccer-data.co/v1/matches?status=live&per_page=100" \ -H "X-Api-Key: YOUR_API_KEY" ``` Add `competition_id` or `team_id` to watch only the games you need. Each live match carries a `live` object with the clock: ```json { "id": "match_0i2m68v7oaxa", "competition_id": "competition_3pcc7lp44661", "status": "live", "utc_date": "2026-10-09T19:00:00.000Z", "home_team": { "id": "team_7p28lyz7ql3i", "name": "West Ham United" }, "away_team": { "id": "team_v3nsjaq0vsxs", "name": "Queens Park Rangers" }, "score": { "home": 0, "away": 1 }, "live": { "match_status": "first_half", "elapsed_minutes": 29, "period": "first_half", "updated_at": "2026-10-09T19:29:14.093Z" }, "live_odds_available": true } ``` `live` is `null` for any match that isn't in play. In the first moments after kickoff, the fields inside it can also be `null` until the clock first updates, so check before you display them. Refresh this list every minute or two to pick up new kickoffs and final whistles. ### The live endpoints | Endpoint | What it gives you | |---|---| | [`/matches/{match_id}/live-stats`](/docs/reference/matches/live-stats) | Score, clock and running team stats. | | [`/matches/{match_id}/live-timeline`](/docs/reference/matches/live-timeline) | Goals, cards and substitutions so far. Filter with `event_type`, `period` or `team_id`. | | [`/matches/{match_id}/live-player-stats`](/docs/reference/matches/live-player-stats) | Each player's numbers so far. Filter with `player_ids` (comma-separated). | | [`/matches/{match_id}/odds/live`](/docs/reference/odds/live) | In-play prices, when `live_odds_available` is `true`. Needs a paid plan. | #### Score and stats ```bash curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-stats" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "match_id": "match_0i2m68v7oaxa", "meta": { "match_status": "first_half", "elapsed_minutes": 29, "home_goals": 0, "away_goals": 1, "period": "first_half" }, "stats": { "ball_possession": { "all": { "home": 55, "away": 45 } }, "total_shots": { "all": { "home": 1, "away": 2 } }, "shots_on_target": { "all": { "home": 0, "away": 1 } }, "expected_goals": { "all": { "home": 0.06, "away": 0.85 } } } } } ``` Which stats appear depends on the match. Check that a key exists before you read it. #### Events ```bash curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-timeline" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "match_id": "match_0i2m68v7oaxa", "coverage": "full", "events": [ { "sequence": 1, "minute": 17, "extra_time": 0, "period": "first_half", "type": "yellow_card", "team": { "id": "team_7p28lyz7ql3i", "name": "West Ham United" }, "player": { "id": "player_hjikl8w59an8", "name": "Arne Engels" } }, { "sequence": 2, "minute": 18, "extra_time": 0, "period": "first_half", "type": "penalty_scored", "team": { "id": "team_v3nsjaq0vsxs", "name": "Queens Park Rangers" }, "player": { "id": "player_zqov38ls8d5o", "name": "Nicolas Madsen" } } ] }, "meta": { "total": 6, "coverage": "full", "last_updated": "2026-10-09T20:30:03.372Z", "match_status": "live" } } ``` A few things to watch for: - **Penalties are their own event type.** A goal from the spot is `penalty_scored`, not `goal`. To get every goal, ask for `event_type=goal,penalty_scored`. - **Shoot-outs use the same type.** Kicks in a penalty shoot-out are also `penalty_scored`, but with `period` set to `penalties`. Leave them out when you count goals in the match. - **Stoppage time is split in two.** A goal in the 45+3rd minute has `minute: 45` and `extra_time: 3`. #### Player stats ```bash curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-player-stats?player_ids=player_5lxcg0mhwqx3" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "player_id": "player_5lxcg0mhwqx3", "player_name": "Ronnie Edwards", "team_id": "team_v3nsjaq0vsxs", "position": "M", "rating": 6.84, "started": true, "minutes_played": 25, "passing": { "total_passes": 7, "accurate_passes": 7, "key_passes": 0 }, "shooting": { "goals": 0, "total_shots": 0, "expected_assists": 0.01 } } ] } ``` Each entry also has a `general` group with touches, fouls and cards. ### How often to poll Live answers are cached on our side, and how long depends on your plan: | Plan | Live data normally refreshed every | |---|---| | Free | 5 minutes | | Pro and Business | 15 seconds | Polling faster than your plan's refresh returns the same answer again, and each call still counts as a request. The `X-Cache` header tells you whether an answer came from the cache (`HIT`) or was fetched fresh (`MISS`). If a fresh answer can't be fetched, you may get an older copy marked `STALE`, with its age in `X-Cache-Age`: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). Polling every 15 to 30 seconds is plenty on a paid plan. Plan your budget before you build. On Pro, polling one endpoint every 15 seconds through a two-hour match uses about 480 requests; polling every 30 seconds halves that. On Free, one endpoint every 5 minutes uses about 24. See [Rate limits](/docs/concepts/rate-limits) for your plan's numbers. To keep the count down: - Poll only the matches someone is actually looking at. - Choose the endpoint you need. If you only show the score, `live-stats` alone is enough. - When you get a `429`, wait the number of seconds in the `Retry-After` header before trying again. ### Before kickoff and after full time The live endpoints only answer while a match is in play. | When | `live-stats` | `live-timeline` and `live-player-stats` | `odds/live` | |---|---|---|---| | Before kickoff | `409` | `409` | `404` | | In play | `200` | `200` | `200` when prices exist | | After full time | `200` with `match_status: "finished"` for up to 5 minutes, then `409` | `409` | `404` | The `409` has the code `CONFLICT` and the message "Match is not live". Treat it as a signal to switch endpoints, not as a failure. Once the match is over, use the full-match endpoints instead: [`/stats`](/docs/reference/matches/stats), [`/player-stats`](/docs/reference/matches/player-stats) and [`/timeline`](/docs/reference/matches/timeline). These work the other way round: while the match is still live they return `409`, with the code `MATCH_IS_LIVE` on `/stats`. #### When to stop Stop polling a match when either of these happens: - `meta.match_status` in `live-stats` is `finished`. - A live endpoint returns `409` after the match has started. ### A simple polling loop ```javascript const headers = { "X-Api-Key": process.env.SOCCER_DATA_KEY }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function follow(matchId) { while (true) { const res = await fetch(`https://api.soccer-data.co/v1/matches/${matchId}/live-stats`, { headers }); if (res.status === 409) return; // not live (any more) if (res.status === 429 || res.status === 503) { await sleep(Number(res.headers.get("Retry-After") ?? 30) * 1000); continue; } if (!res.ok) throw new Error(`Live stats failed with ${res.status}`); const { data } = await res.json(); console.log(`${data.meta.elapsed_minutes}' ${data.meta.home_goals}-${data.meta.away_goals}`); if (data.meta.match_status === "finished") return; await sleep(30_000); } } await follow("match_0i2m68v7oaxa"); ``` ### Lineups You don't have to wait for kickoff for lineups. [`/matches/{match_id}/lineups`](/docs/reference/matches/lineups) has a predicted lineup before the match, and the official team sheet usually arrives about an hour before kickoff. The `type` and `confirmed` fields tell you which one you're looking at. ### Common mistakes - **Counting only `goal` events.** Penalties are `penalty_scored`. - **Counting shoot-out kicks as match goals.** Skip events with `period: "penalties"`. - **Assuming every stat is there.** Check that a key exists first. - **Not handling `null` in `live`** in the first moments of a match. - **Polling faster than your plan's refresh.** You get the same data and spend more requests. - **Ignoring `429` and `Retry-After`.** - **Treating `409` as an error.** It means the match isn't live; stop or switch endpoints. - **Polling from the browser.** That exposes your key. Poll from your server and pass the results on. ### Related - [Fixtures and results](/docs/guides/fixtures-and-results) - [Player and team stats](/docs/guides/player-and-team-stats) - [Odds](/docs/guides/odds) - [Rate limits](/docs/concepts/rate-limits) --- ## League tables Current and past league tables, tournament group tables, and where a team has a table entry. A league table belongs to one season of one competition, so you need both ids. This guide shows how to get them, then how to read league tables, past seasons and tournament groups. Every example sends your key in the `X-Api-Key` header. Keep that key on your server. ### Find the competition and its season Search for the competition, then read `current_season_id` from it: ```bash curl "https://api.soccer-data.co/v1/competitions?search=Premier" \ -H "X-Api-Key: YOUR_API_KEY" ## The Premier League is competition_705uv6jm8myn curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ## data.current_season_id is season_hr1ie7yrsdmt ``` For earlier seasons, [`/competitions/{competition_id}/seasons`](/docs/reference/competitions/seasons) lists them all. > **Tip:** Read `current_season_id` from the competition instead of hard-coding a season id. Your table then moves on to the new season by itself. ### The current table ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons/season_hr1ie7yrsdmt/standings" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "team": { "id": "team_hof12pbei216", "name": "Manchester City" }, "position": 1, "matches_played": 5, "wins": 5, "draws": 0, "losses": 0, "goals_for": 13, "goals_against": 5, "goal_difference": 8, "points": 15, "group_label": null }, { "team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal" }, "position": 2, "matches_played": 5, "wins": 4, "draws": 0, "losses": 1, "goals_for": 8, "goals_against": 4, "goal_difference": 4, "points": 12, "group_label": null } ] } ``` The example shows the first two of 20 rows. Rows come in `position` order, and the whole table arrives in one response: there are no pages to walk through. Tables move during matches, so they follow your plan's live refresh: normally every 15 seconds on paid plans and every 5 minutes on Free. #### Row fields | Field | Meaning | |---|---| | `team` | The team's `id` and `name`. | | `position` | Place in the table. | | `matches_played`, `wins`, `draws`, `losses` | The team's record. | | `goals_for`, `goals_against`, `goal_difference` | Goals scored, conceded and the difference. | | `points` | Points so far. | | `group_label` | The group, such as `"A"`, in a tournament. `null` in a league. | ### A past season Swap in an older season id. Premier League 25/26 is `season_98l95mxugqs9`: ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons/season_98l95mxugqs9/standings" \ -H "X-Api-Key: YOUR_API_KEY" ``` Arsenal top that table, with 85 points from 26 wins in 38 matches. How many past seasons exist depends on the competition. Check [Coverage](/docs/concepts/coverage) before you promise users a particular season. > **Warning:** The season must belong to the competition in the path. If it doesn't, you get a `400` saying the season doesn't belong to that competition. ### Tournament groups Tournaments with a group stage have one table per group. Start by listing the groups. This is the 2022 World Cup: ```bash curl "https://api.soccer-data.co/v1/competitions/competition_7vpwx91c7j7h/seasons/season_st2tyewgtq2j/groups" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "group_label": "A", "name": "Group A" }, { "group_label": "B", "name": "Group B" } ] } ``` The real response runs from Group A to Group H. Then ask for one group's table with `group`: ```bash curl "https://api.soccer-data.co/v1/competitions/competition_7vpwx91c7j7h/seasons/season_st2tyewgtq2j/standings?group=A" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "team": { "id": "team_z5p9xh26ys6y", "name": "Netherlands" }, "position": 1, "matches_played": 3, "wins": 2, "draws": 1, "losses": 0, "goals_for": 5, "goals_against": 1, "goal_difference": 4, "points": 7, "group_label": "A" } ] } ``` Leave `group` out and you get every group's table in one list, sorted by `group_label` and then by `position`. A league without groups returns an empty list from `/groups`. ### Cups Competitions played only as knockouts, like the FA Cup, have no table. Their standings come back as an empty list. ### Where a team has tables [`/teams/{team_id}/standings`](/docs/reference/teams/standings) lists every competition and season in which a team has a table entry: ```bash curl "https://api.soccer-data.co/v1/teams/team_f0ibhxdw7hs5/standings" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "competition": { "id": "competition_705uv6jm8myn", "name": "Premier League" }, "seasons": [ { "id": "season_hr1ie7yrsdmt", "name": "Premier League 26/27", "group_label": null }, { "id": "season_98l95mxugqs9", "name": "Premier League 25/26", "group_label": null } ] }, { "competition": { "id": "competition_b25me9a3vyf7", "name": "UEFA Europa League" }, "seasons": [ { "id": "season_dgf6gghi4mf8", "name": "UEFA Europa League 22/23", "group_label": "A" } ] } ] } ``` This is a list of places to look, not a list of positions. To find where the team finished, fetch that season's table. When the season has a `group_label`, pass it as `group` to get just that group's table. If you only need one team's position, points and recent form for a season, [`/teams/{team_id}/stats`](/docs/reference/teams/stats) gives you all three in a single call. ### Common mistakes - **Pairing a season with the wrong competition.** You get a `400`. - **Expecting positions from `/teams/{team_id}/standings`.** It only lists seasons; fetch the table for positions. - **Sending `page` or `per_page` to the standings endpoint.** Tables aren't paged, so the parameter is rejected. - **Expecting a table for a knockout cup.** There isn't one; the list is empty. - **Forgetting `group` in a tournament.** Without it you get every group mixed into one list. ### Related endpoints - [Standings for a season](/docs/reference/competitions/standings) - [Groups in a season](/docs/reference/competitions/groups) - [Seasons of a competition](/docs/reference/competitions/seasons) - [Team standings history](/docs/reference/teams/standings) - [Team season stats](/docs/reference/teams/stats) --- ## Player and team stats Season totals for players and teams, match stats, xG shot maps and heatmaps. Stats come at two levels: season totals for a player or a team, and the numbers for a single finished match. On top of that, shot maps and heatmaps show where things happened on the pitch. This guide covers each one. Every example sends your key in the `X-Api-Key` header. Keep that key on your server. The examples use Bukayo Saka (`player_ul3vc6om97u5`), Arsenal (`team_f0ibhxdw7hs5`), the 25/26 Premier League season (`season_98l95mxugqs9` in `competition_705uv6jm8myn`) and Brighton 3-0 Arsenal (`match_nbqbxh7kv0q8`). ### Which endpoint do I need? | You want | Endpoint | |---|---| | A player's season totals | [`/players/{player_id}/stats`](/docs/reference/players/stats) | | A team's season record | [`/teams/{team_id}/stats`](/docs/reference/teams/stats) | | Team stats for one match, with xG | [`/matches/{match_id}/stats`](/docs/reference/matches/stats) | | Player stats for one match | [`/matches/{match_id}/player-stats`](/docs/reference/matches/player-stats) | | Every shot in a match, with xG | [`/matches/{match_id}/shotmap`](/docs/reference/matches/shotmap) | | Where a player touched the ball in one match | [`/matches/{match_id}/players/{player_id}/heatmap`](/docs/reference/matches/player-heatmap) | | Where a player touched the ball over a season | [`/players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap`](/docs/reference/players/season-heatmap) | The match endpoints are for finished matches. While a match is still being played, use the live endpoints from [Live matches](/docs/guides/live-matches). ### A player's season `season_id` is required. Add `competition_id` to count only one competition, and `stage=regular` or `stage=playoff` to split the regular season from play-offs. ```bash curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/stats?season_id=season_98l95mxugqs9&competition_id=competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "player_id": "player_ul3vc6om97u5", "season_id": "season_98l95mxugqs9", "competition_id": "competition_705uv6jm8myn", "team_id": "team_f0ibhxdw7hs5", "rating": 7.64, "appearances": 31, "starts": 21, "minutes_played": 2225, "scoring": { "goals": 7, "assists": 5, "big_chances_created": 12 }, "shooting": { "total_shots": 71, "shots_on_target": 27 }, "passing": { "total_passes": 773, "pass_accuracy": 76.7, "key_passes": 63 }, "defending": { "tackles": 41, "interceptions": 16 }, "discipline": { "yellow_cards": 2, "red_cards": 0 } } } ``` The numbers are grouped into `scoring`, `shooting`, `passing`, `defending` and `discipline`; the example shows a few from each. Without `season_id` you get `400 BAD_REQUEST`. > **Tip:** Want per-90 figures? Divide a total by `minutes_played` and multiply by 90. ### A team's season ```bash curl "https://api.soccer-data.co/v1/teams/team_f0ibhxdw7hs5/stats?season_id=season_98l95mxugqs9" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "team_id": "team_f0ibhxdw7hs5", "season_id": "season_98l95mxugqs9", "competition_id": "competition_705uv6jm8myn", "matches_played": 38, "wins": 26, "draws": 7, "losses": 5, "points": 85, "position": 1, "goals_for": 71, "goals_against": 27, "goal_difference": 44, "form": "WWWWW" } } ``` This is the quickest way to get one team's position, points and recent form. If the team has no stats for that season, you get a `404`. ### Team stats for a match ```bash curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/stats" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "match_id": "match_nbqbxh7kv0q8", "overview": { "ball_possession": { "all": { "home": 40, "away": 60 } }, "expected_goals": { "all": { "home": 1.31, "away": 1.63 }, "first_half": { "home": 0.36, "away": 0.39 }, "second_half": { "home": 0.95, "away": 1.24 } }, "total_shots": { "all": { "home": 17, "away": 11 } } } } } ``` Each stat has an `all` value for the whole match, and `first_half` and `second_half` where they exist, each split into `home` and `away`. Besides `overview`, you'll find groups for `shots`, `attack`, `passes`, `duels`, `defending` and `goalkeeping`, plus `np_expected_goals` (xG without penalties). When a match has no xG, `expected_goals` is `null`. ### Player stats for a match Leave out `player_ids` for everyone who played, or pass one or more ids, comma-separated: ```bash curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/player-stats?player_ids=player_ul3vc6om97u5" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": [ { "player_id": "player_ul3vc6om97u5", "player_name": "Bukayo Saka", "team_id": "team_f0ibhxdw7hs5", "position": "F", "rating": 7.06, "minutes_played": 81, "shooting": { "total_shots": 2, "shots_on_target": 1, "expected_goals": 0.35, "expected_assists": 0.53 } } ] } ``` Match player stats and season stats are shaped differently, so don't reuse the same parser for both. ### Shot maps ```bash curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/shotmap" \ -H "X-Api-Key: YOUR_API_KEY" ``` Add `player_id` to get one player's shots only. Each shot looks like this: ```json { "id": "shot_as8bbsa4f1mq", "player_id": "player_ul3vc6om97u5", "player_name": "Bukayo Saka", "team_id": "team_f0ibhxdw7hs5", "x": 7.9, "y": 45.3, "minute": 45, "result": "save", "expected_goals": 0.303, "situation": "fast_break", "body_part": "right_foot", "is_goal": false, "is_on_target": true, "is_penalty": false } ``` #### Drawing shots on a pitch Every shot is placed as if the team were attacking the same goal, so you can draw both teams on one half. - `x` runs from 0 at the goal line being attacked to 100 at the shooter's own goal line. The edge of the penalty area is at 16.5. - `y` runs across the pitch, with 50 in the middle of the goal. ### Heatmaps A match heatmap has one point for every touch the player made. This one has 46: ```bash curl "https://api.soccer-data.co/v1/matches/match_nbqbxh7kv0q8/players/player_ul3vc6om97u5/heatmap" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "match_id": "match_nbqbxh7kv0q8", "player": { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka" }, "team": { "id": "team_f0ibhxdw7hs5", "name": "Arsenal", "side": "away" }, "points": [{ "x": 69, "y": 9 }, { "x": 31, "y": 7 }, { "x": 55, "y": 13 }] } } ``` A season heatmap groups touches into cells on the pitch and gives each cell a `count`: ```bash curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/competitions/competition_705uv6jm8myn/seasons/season_98l95mxugqs9/heatmap" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "player": { "id": "player_ul3vc6om97u5", "name": "Bukayo Saka" }, "competition": { "id": "competition_705uv6jm8myn", "name": "Premier League" }, "season": { "id": "season_98l95mxugqs9", "name": "Premier League 25/26" }, "points": [{ "x": 59, "y": 3, "count": 1 }, { "x": 59, "y": 8, "count": 3 }] } } ``` Both use `x` and `y` from 0 to 100. Both return `404` when there's nothing to show, for example for a substitute who never came on, or when there's no positional data for that match or competition. ### Check before you call Not every competition or match has every kind of stat. Flags tell you in advance: | Flag | Where | Means | |---|---|---| | `has_team_stats` | Competition | Team stats exist in this competition. | | `has_player_stats` | Competition | Player stats exist in this competition. | | `xg_available` | Competition and match | Some xG exists: team totals, per-shot values or both. | | `shotmap_available` | Match | The shot map has shots with xG values. | | `xg_quality` | Match | `full` when there's xG for every shot, otherwise `none`. Treat any other value as `none`. | `xg_available` can be `true` while `shotmap_available` is `false`: the match has team xG, but no shot-by-shot data. Check `shotmap_available` before drawing a shot map. To find competitions with a kind of data, use [`/coverage/leagues`](/docs/reference/coverage/leagues) with `data_type=player_stats` or `data_type=xg`. [Coverage](/docs/concepts/coverage) explains how. ### When stats aren't there `/stats` and `/player-stats` can return `404` for two different reasons, and the `code` tells you which: - **`STATS_NOT_AVAILABLE`:** the match only has a score and lineups. Detailed stats will never exist for it, so don't retry. - **`NOT_FOUND`:** the stats aren't in yet, or the match doesn't exist. Check the id, and try again later if the match has only just finished. ### Common mistakes - **Leaving out `season_id`** on season stats endpoints. - **Asking for `/stats` or `/player-stats` during a match.** They return `409`; use the live endpoints until full time. - **Calling `/shotmap` without checking `shotmap_available`.** - **Reading `xg_available: true` as "every shot has xG".** Check `shotmap_available` or `xg_quality`. - **Calling from the browser.** Keep your key on the server. ### Related endpoints - [Team injuries and suspensions](/docs/reference/teams/injuries) and [player injuries and suspensions](/docs/reference/players/injuries) - [Match lineups](/docs/reference/matches/lineups) - [Match timeline](/docs/reference/matches/timeline) - [Match referee](/docs/reference/matches/referee) --- ## Odds Pre-match and in-play prices from five bookmakers, and how to compare them. Soccer Data collects prices from several bookmakers so you can compare them side by side, follow how they move before kickoff, and track them in play. This guide covers the two odds endpoints, the bookmakers and markets, and the checks worth making first. Every example sends your key in the `X-Api-Key` header. Keep that key on your server. ### Which plan you need | Endpoint | What it returns | Needs | |---|---|---| | [`/matches/{match_id}/odds`](/docs/reference/odds/prematch) | Pre-match prices, with opening and latest values. | Pro or Business plan | | [`/matches/{match_id}/odds/live`](/docs/reference/odds/live) | In-play prices while the match is live. | Pro or Business plan | On the Free plan, you get `403` with the code `PLAN_UPGRADE_REQUIRED`. You can upgrade on the [plans page](/dashboard/subscription) of your dashboard. ### Bookmakers | Bookmaker | Slug | |---|---| | Bet365 | `bet365` | | Paddy Power | `paddy-power` | | BetMGM UK | `betmgm-uk` | | Pinnacle | `pinnacle` | | Betfair Exchange | `betfair-exchange` | The pre-match endpoint takes a `bookmaker` filter with one or more slugs, comma-separated. Leave it out to get every bookmaker. An unknown slug returns `400`. In-play prices come from Bet365, Paddy Power, BetMGM UK and Betfair Exchange. ### Odds format All prices are decimal odds. If your users expect fractional or American odds, convert them in your app. Pre-match and in-play prices are strings, such as `"1.380"`. ### Check before you call Matches and competitions carry flags so you don't have to ask blindly: | Flag | On | Means | |---|---|---| | `odds_available` | Match | Pre-match prices exist. Always `false` for postponed and cancelled matches. | | `live_odds_available` | Match | In-play prices exist right now. | | `odds_available` | Competition | Its matches have prices. | | `live_odds_available` | Competition | At least one of its matches has in-play prices right now. | If a match has no prices of the kind you ask for, `/odds` and `/odds/live` return `404`. Treat that as "no odds for this match", not as a failure. ### Pre-match odds ```bash curl "https://api.soccer-data.co/v1/matches/match_yoeml0e6ae37/odds?bookmaker=bet365,pinnacle" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "match_id": "match_yoeml0e6ae37", "bookmakers": [ { "bookmaker": "Bet365", "markets": { "match_odds": { "home": { "opening": "1.380", "last_seen": "1.380" }, "draw": { "opening": "4.500", "last_seen": "5.000" }, "away": { "opening": "7.500", "last_seen": "7.500" } }, "btts": { "yes": { "opening": "2.000", "last_seen": "2.050" }, "no": { "opening": "1.750", "last_seen": "1.700" } }, "total_goals": { "2.5": { "over": { "opening": "1.725", "last_seen": "1.800" }, "under": { "opening": "2.075", "last_seen": "2.000" } } } } }, { "bookmaker": "Pinnacle", "markets": { "match_odds": { "home": { "opening": "1.377", "last_seen": "1.413" }, "draw": { "opening": "4.910", "last_seen": "4.870" }, "away": { "opening": "7.620", "last_seen": "7.760" } } } } ] } } ``` Every selection has two prices: - `opening`: the first price we saw, or `null` if we don't have it. - `last_seen`: the latest price. Once the match has kicked off, this is the closing price. The endpoint works for upcoming and finished matches alike, so you can also look back at closing lines. #### Markets The example shows three common markets: - `match_odds`: home, draw or away. - `btts`: both teams to score, yes or no. - `total_goals`: a key per line, such as `"2.5"`, each with `over` and `under`. Depending on the bookmaker and the match, you'll also find full-time and half-time results, corners, cards, Asian and European handicaps, team totals, shots, correct score, to qualify and penalty specials. On bigger matches Bet365 adds markets for shots, throw-ins, goal kicks, tackles and offsides, some with per-half versions such as `first_half_match_shots`. A market only appears if that bookmaker prices it. Check that a key exists before reading it. ### In-play odds Ask only while `live_odds_available` is `true`: ```bash curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/odds/live" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```json { "data": { "match_id": "match_0i2m68v7oaxa", "bookmakers": [ { "bookmaker": "Bet365", "markets": { "match_odds": { "home": { "live": "3.400" }, "draw": { "live": "3.100" }, "away": { "live": "2.250" } } } }, { "bookmaker": "Betfair Exchange", "markets": { "match_odds": { "home": { "live": "3.650", "available_to_back": [{ "price": 3.65, "size": 1209.44 }, { "price": 3.6, "size": 1795.66 }], "available_to_lay": [{ "price": 3.7, "size": 480.78 }, { "price": 3.75, "size": 2184.81 }] } } } } ] } } ``` Each selection has a single `live` price. Betfair Exchange also gives the money waiting in the market: `available_to_back` and `available_to_lay`, up to three levels each, best price first, with a `price` and a `size`. Books suspend and reopen during a match, so bookmakers and markets come and go between calls. After full time the endpoint returns `404`. In-play prices follow your plan's live refresh, normally 15 seconds, so polling faster than that returns the same prices. [Live matches](/docs/guides/live-matches) has a polling loop you can reuse. ### How far back odds go Odds history is thinner than match history: - From the 2026/27 season, matches can have opening and closing prices from all five bookmakers. - Before that, most matches only have Bet365 closing prices. For those, `opening` is `null` and `last_seen` holds the closing price. [`/coverage/leagues/{competition_id}`](/docs/reference/coverage/league) shows which seasons have prices, with `opening_odds`, `closing_odds` and `odds` counted separately for each season. To find competitions that have odds at all, use [`/coverage/leagues`](/docs/reference/coverage/leagues) with `data_type=odds`, `opening_odds` or `closing_odds`. For example, Premier League 26/27 has both opening and closing prices, while 25/26 has closing prices only. ### Common mistakes - **Assuming a market or bookmaker is always there.** Check that it exists in the response first. - **Treating `404` as an error.** On the odds endpoints it means the match has no prices of that kind. - **Expecting prices for postponed or cancelled matches.** `odds_available` is always `false` for them. - **Mistyping a bookmaker slug.** An unknown slug returns `400`. ### Related - [Live matches](/docs/guides/live-matches) - [Coverage](/docs/concepts/coverage) - [Rate limits](/docs/concepts/rate-limits) --- ## Data model The things in the API, their ids, their main fields and how they link together. Everything in Soccer Data hangs off a few core things: competitions, their seasons, the matches in those seasons, and the teams and players who take part. This page explains each one, its id and the fields you'll use most. ### IDs Every id is a string made of two parts: - a **word prefix** that says what kind of thing it is, such as `team_`; - **12 lowercase letters and digits** that say which one. So Arsenal is `team_f0ibhxdw7hs5` and the Premier League is `competition_705uv6jm8myn`. Ids are permanent. A team, player or match keeps the same id for good, so you can store ids in your database, put them in config files, and use them in URLs of your own app. Look an id up once and reuse it. | Prefix | What it names | Example | |---|---|---| | `competition_` | A league, cup or tournament | `competition_705uv6jm8myn` (Premier League) | | `season_` | One season of a competition | `season_hr1ie7yrsdmt` (Premier League 26/27) | | `team_` | A club or national team | `team_f0ibhxdw7hs5` (Arsenal men) | | `player_` | A player | `player_ul3vc6om97u5` (Bukayo Saka) | | `match_` | A single match | `match_yoeml0e6ae37` (Arsenal v Leeds United) | | `shot_` | One shot in a match's shot map | `shot_as8bbsa4f1mq` | | `manager_` | A manager, as `home_manager` or `away_manager` on a match | `manager_1dtehmda8rsp` | | `referee_` | A match referee | `referee_dxvcpogubown` | A few rules make ids easy to work with: - **Treat them as opaque strings.** Don't parse the characters after the prefix or turn them into numbers; they carry no meaning. - **Copy them exactly.** Use the ids you get in our responses. An id we don't recognise, whether it has a typo or comes from another data source, returns `404 NOT_FOUND`. - **Find them by name when you need to.** [Find any ID](/docs/guides/finding-ids) shows every way to look them up. ### How things connect ```text Competition └── Season └── Match ── home_team / away_team ── Team ── Player ``` - A **competition** has many **seasons**. The competition's `current_season_id` points to the one running now. - A **season** has many **matches**. Each match carries its `competition_id` and `season_id`. - A **match** links to its two teams (`home_team`, `away_team`) and managers (`home_manager`, `away_manager`). - A **player** links to the team they play for now through `current_team`, which includes their `jersey_number`. - Teams and players also show up in season stats, lineups and match stats. Groups in a tournament aren't separate things with their own ids. A match or table row simply carries a `group_label`, such as `"A"`. ### Competition A league, a cup or a tournament. | Field | Meaning | |---|---| | `id`, `name` | The id and the name. | | `type` | `league`, `cup` or `tournament`. | | `country`, `country_code` | Where it's played. Both are `null` for international competitions. | | `subdivision_code` | The UK nation, such as `GB-ENG`. `null` outside the UK. | | `confederation` | For international competitions, such as `UEFA` or `FIFA`. `null` for domestic ones. | | `current_season_id` | The season running now. Returned when you fetch a single competition. | ### Season One edition of a competition. | Field | Meaning | |---|---| | `id`, `name` | For example "Premier League 26/27". | | `year` | A label for display, such as `26/27` or `2026`. | | `start_year`, `end_year` | Numbers you can sort by. | | `is_current` | `true` for the latest season. | ### Team A club side or a national team. | Field | Meaning | |---|---| | `id`, `name`, `short_name` | The id and names. | | `country` | The team's country. | | `is_mens_team` | `true`, `false`, or `null` when unknown. Clubs often have a men's and a women's team with the same name. | | `primary_competition` | The `id` and `name` of the competition the team mainly plays in, or `null`. Useful to tell a first team from a youth side. | ### Player | Field | Meaning | |---|---| | `id`, `name`, `first_name`, `last_name` | The id and names. | | `position` | `G`, `D`, `M` or `F`, or an empty string when unknown. | | `date_of_birth`, `age` | `YYYY-MM-DD` and whole years. `null` when unknown. | | `nationality` | The country they represent. | | `height_cm` | Height, or `null` when not recorded. | | `current_team` | `id`, `name` and `jersey_number` at their current team. | ### Match | Field | Meaning | |---|---| | `id`, `competition_id`, `season_id` | The match and where it belongs. | | `status` | `scheduled`, `live`, `finished`, `postponed` or `cancelled`. | | `utc_date` | Kickoff, in ISO 8601 and always UTC. | | `matchday` | The league round. Often `null` in cups and knockouts. | | `stage_name` | The round in a knockout, such as `quarter_final` or `final`. `null` for regular league matches. | | `group_label` | The tournament group, or `null`. | | `home_team`, `away_team` | Each with an `id` and `name`. | | `score` | `home` and `away` are normal-time goals. For the overall result, read `winner`, and `after_extra_time` and `penalty_shootout` when present. | | `is_neutral` | `true` when neither side is at home. `null` means we don't know, not `false`. | | `live` | The match clock while it's in play, on the match list. `null` otherwise. | ### Availability flags Some data only exists for some competitions or matches. These flags let you check before you spend a request: | Flag | On | Means | |---|---|---| | `has_team_stats` | Competition | Team stats exist in this competition. | | `has_player_stats` | Competition | Player stats exist in this competition. | | `xg_available` | Competition, match | Some xG data exists. | | `shotmap_available` | Match | The shot map has shots with xG values. | | `xg_quality` | Match | `full` for xG on every shot, otherwise `none`. | | `odds_available` | Competition, match | Pre-match odds exist. | | `live_odds_available` | Competition, match | In-play odds exist right now. | Flags on a competition describe the competition as a whole, not a particular season. For season-by-season answers, use the [coverage endpoints](/docs/concepts/coverage). ### Dates, times and nulls - Kickoff times are UTC. Convert them to your users' time zone when you display them. - Date filters take `YYYY-MM-DD`. To get matches in your own time zone's day, add `utc_offset`, such as `-05:00`. In URLs, write a plus sign as `%2B` (`utc_offset=%2B02:00`), or let `URLSearchParams` do it. - `null` always means "unknown" or "doesn't exist yet". It never stands for zero or `false`. For example, `score.home` is `null` before kickoff, `live` is `null` when a match isn't in play, and `matchday` is usually `null` in a cup (check `stage_name`). ### Common requests | You want | Ask for | |---|---| | A league table | [`/competitions/{competition_id}/seasons/{season_id}/standings`](/docs/reference/competitions/standings) | | A team's season | [`/teams/{team_id}/stats?season_id=...`](/docs/reference/teams/stats) | | A player's season | [`/players/{player_id}/stats?season_id=...`](/docs/reference/players/stats), optionally with `competition_id` | | A squad | [`/teams/{team_id}/players`](/docs/reference/teams/players) | | A women's team | [`/teams?search=Arsenal&is_mens_team=false`](/docs/reference/teams/list) | | One nation's competitions | [`/competitions?country_code=GB-ENG`](/docs/reference/competitions/list); `GB` covers all UK nations | | One day's matches | [`/matches?date_from=...&date_to=...`](/docs/reference/matches/list) with the same date twice | ### Common mistakes - **Converting ids to numbers or trimming the prefix.** Keep them as the strings you received. - **Mixing up teams with the same name.** Check `is_mens_team` and `primary_competition`. - **Reading `null` as zero.** It means unknown. - **Assuming `is_neutral: null` means a home game.** It means we don't know. - **Calling an optional endpoint without checking its flag.** Missing data returns `404` or an empty `data` list. --- ## Pagination How list endpoints split results into pages, and how to read them all. Endpoints that can return a lot of results hand them out a page at a time. You choose the page and its size, and a `meta` object tells you how many pages there are. ### Which endpoints are paged | Endpoint | Default page size | Largest page | |---|---:|---:| | [`/competitions`](/docs/reference/competitions/list) | 20 | 100 | | [`/teams`](/docs/reference/teams/list) | 20 | 100 | | [`/players`](/docs/reference/players/list) | 20 | 100 | | [`/matches`](/docs/reference/matches/list) | 20 | 100 | | [`/coverage/leagues`](/docs/reference/coverage/leagues) | 100 | 200 | Everything else returns the whole list in one go: seasons, squads, standings and timelines, for example. Those endpoints don't take `page` or `per_page`, and sending them gets `400 UNKNOWN_PARAMETER`. ### Parameters | Parameter | Meaning | |---|---| | `page` | Which page you want, counting from 1. Defaults to 1. | | `per_page` | How many results on each page. Defaults to 20 (100 on `/coverage/leagues`). | Both must be whole numbers above zero. A zero, a negative number, or a `per_page` above the maximum returns `400 BAD_REQUEST`. ### The `meta` object Paged responses put a `meta` object next to `data`: ```json { "data": [], "meta": { "page": 1, "per_page": 20, "total": 204, "total_pages": 11 } } ``` | Field | Meaning | |---|---| | `page` | The page you asked for. | | `per_page` | The page size you asked for. The last page can hold fewer results. | | `total` | How many results there are across all pages. | | `total_pages` | How many pages that makes at this page size. `0` when nothing matched. | ### Reading every page Ask for the largest page size, then keep going until `page` reaches `meta.total_pages`. ```javascript async function fetchAllPages(path, filters = {}) { const results = []; for (let page = 1; ; page++) { const params = new URLSearchParams({ ...filters, page: String(page), per_page: "100" }); const res = await fetch(`https://api.soccer-data.co/v1${path}?${params}`, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); if (!res.ok) throw new Error(`Request failed with ${res.status}`); const body = await res.json(); results.push(...body.data); if (page >= body.meta.total_pages) return results; } } const matches = await fetchAllPages("/matches", { competition_id: "competition_705uv6jm8myn", season_id: "season_hr1ie7yrsdmt", }); console.log(`${matches.length} matches`); ``` ```python import os, requests def fetch_all_pages(path, **filters): results, page = [], 1 while True: res = requests.get( f"https://api.soccer-data.co/v1{path}", params={**filters, "page": page, "per_page": 100}, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) res.raise_for_status() body = res.json() results.extend(body["data"]) if page >= body["meta"]["total_pages"]: return results page += 1 matches = fetch_all_pages("/matches", competition_id="competition_705uv6jm8myn", season_id="season_hr1ie7yrsdmt") print(len(matches), "matches") ``` > **Note:** Every page is a separate request and counts against your [rate limits](/docs/concepts/rate-limits). Narrow the list with filters such as `competition_id`, `season_id`, `team_id`, `date_from` and `date_to` before you page through it, rather than downloading everything and filtering in your code. ### Going past the last page - On `/matches`, asking for a page after the last one returns `400 PAGE_OUT_OF_RANGE`. The message tells you the page you asked for and how many pages there are. - On the other paged endpoints you get `200` with an empty `data` list. `meta` still shows the real `total` and `total_pages`. Either way, the loop above never gets there, because it stops at `meta.total_pages`. ### Ordering The order of results is fixed, so pages don't shuffle between requests. - **`/matches`** lists the latest kickoff first (`sort=-utc_date`), so upcoming fixtures come before past results. Use `sort=utc_date` for the soonest first. Matches with the same kickoff time always come back in the same order. - **`/competitions`, `/teams` and `/players`** put the best match first when you `search`, and sort A to Z by name otherwise. Add `sort=name` to get A to Z with a search too. ### One-result shortcuts `per_page=1` combined with the right filters and order answers common questions in one request: ```bash ## A team's next match curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=scheduled&sort=utc_date&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ## A team's latest result (the default order already puts it first) curl "https://api.soccer-data.co/v1/matches?team_id=team_f0ibhxdw7hs5&status=finished&per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` Without `status` and `sort`, `per_page=1` gives you the team's match furthest in the future, which is rarely what you want. ### Common mistakes - **Sending `page` or `per_page` to an endpoint that isn't paged.** It returns `400 UNKNOWN_PARAMETER`. - **Setting `per_page` too high, or `page` to zero.** Both return `400 BAD_REQUEST`. - **Looping past `total_pages` on `/matches`.** You get `PAGE_OUT_OF_RANGE`, not an empty page. - **Forgetting the stop condition.** Always compare `page` with `meta.total_pages`. - **Paging through everything to filter in code.** Filter in the request instead and save requests. --- ## Errors Every error code, what causes it, and which ones are worth retrying. When a request fails, you get an HTTP status and a JSON body that always has the same shape. This page lists the codes and shows how to handle them. ### The error shape ```json { "error": { "code": "NOT_FOUND", "message": "Match not found", "status_code": 404 } } ``` | Field | What it's for | |---|---| | `code` | A stable, uppercase name for the problem. Use it in your logic. | | `message` | A plain-English explanation, often saying exactly what to change. Show it in logs, but don't match on its wording. | | `status_code` | The HTTP status again, for convenience. | Branch on `code` rather than on the HTTP status alone. Several codes share a status: `RATE_LIMITED` and `USAGE_LIMIT_EXCEEDED` are both `429`, but need very different handling. ### All error codes | Status | Code | Meaning | |---|---|---| | 400 | `BAD_REQUEST` | A parameter has the wrong format. | | 400 | `UNKNOWN_PARAMETER` | A query parameter the endpoint doesn't take. | | 400 | `SEASON_COMPETITION_MISMATCH` | The season doesn't belong to that competition. | | 400 | `PAGE_OUT_OF_RANGE` | The page is past the last one. | | 401 | `UNAUTHORIZED` | The key is missing, wrong or revoked. | | 403 | `PLAN_UPGRADE_REQUIRED` | The endpoint needs a higher plan (odds need a paid plan). | | 404 | `NOT_FOUND` | No such endpoint, or no such id. | | 404 | `STATS_NOT_AVAILABLE` | This data isn't covered for that match. | | 409 | `MATCH_IS_LIVE` | Final stats aren't ready while the match is in play; use the live endpoint. | | 429 | `RATE_LIMITED` | Too many requests this minute. Wait for Retry-After seconds. | | 429 | `USAGE_LIMIT_EXCEEDED` | Today's requests are used up. Resets at midnight UTC. | | 502 | `UPSTREAM_UNAVAILABLE` | A data source didn't answer in time. Retry; it isn't counted. | | 503 | `SERVICE_UNAVAILABLE` | Temporarily unavailable. Retry after Retry-After; it isn't counted. | A few more codes can reach you from specific endpoints: - **`CONFLICT` (409):** a live endpoint was called for a match that isn't in play (the message reads "Match is not live"), or a full-match endpoint was called while the match is still live. Check the match `status` and pick the matching endpoint. [Live matches](/docs/guides/live-matches) has the details. - **`MATCH_IS_LIVE` (409):** full match stats were requested during the match. Use [`/matches/{match_id}/live-stats`](/docs/reference/matches/live-stats) until full time. ### What to do about each one #### Retry after waiting These clear up on their own. Wait, then send the same request again. - **`RATE_LIMITED` (429):** you've used this minute's requests. Wait the number of seconds in the `Retry-After` header. - **`SERVICE_UNAVAILABLE` (503):** something on our side is briefly unavailable. Wait for `Retry-After`, then try again. It doesn't count against your daily quota. - **`UPSTREAM_UNAVAILABLE` (502):** a data source didn't answer in time. Try again after a short pause, a few seconds and longer each time. It doesn't count against your daily quota either. Cap your retries, at three or so, so a lasting problem doesn't turn into an endless loop. #### Don't retry Sending the same request again won't change the answer. Fix the request, the key or the plan first. - **`USAGE_LIMIT_EXCEEDED` (429):** today's requests are used up. Its `Retry-After` counts the seconds to midnight UTC, when the quota resets, so retrying in a loop only collects more `429`s. Stop and report it, or [upgrade](/dashboard/subscription). - **`BAD_REQUEST` (400):** a parameter is missing or has the wrong format. The message names it, such as "season_id is required". - **`UNKNOWN_PARAMETER` (400):** the endpoint doesn't take that parameter. The message suggests the closest valid parameter, when one is close, and lists every one the endpoint accepts, for example: `This endpoint doesn't take "team". Did you mean "team_id"? It takes: competition_id, date_from, …` - **`SEASON_COMPETITION_MISMATCH` (400):** the season belongs to a different competition. Get the right seasons from [`/competitions/{competition_id}/seasons`](/docs/reference/competitions/seasons). - **`PAGE_OUT_OF_RANGE` (400):** you asked `/matches` for a page past the last one. Stop at `meta.total_pages`. - **`UNAUTHORIZED` (401):** the key is missing, wrong or revoked. See [Authentication](/docs/authentication). - **`PLAN_UPGRADE_REQUIRED` (403):** the endpoint isn't in your plan. Odds need a paid plan. - **`STATS_NOT_AVAILABLE` (404):** detailed stats were never collected for this match, and never will be. - **`CONFLICT` and `MATCH_IS_LIVE` (409):** the match is in the wrong state for this endpoint. Switch endpoints instead of retrying the same one. #### Not found `NOT_FOUND` (404) covers three situations: - **An id we don't recognise.** The id has a typo, was cut short, or didn't come from us. Ids are permanent, so a valid id never stops working. Copy ids from our responses, or find them with a `search` request. - **A path that isn't an endpoint.** Check it against the [API reference](/docs/reference). - **Data that doesn't exist for this request.** For example, a heatmap for a player who never came on. Check the [availability flags](/docs/concepts/data-model#availability-flags) before calling optional endpoints. In most apps, a `404` for a single item is best handled as "nothing here", not as a crash. ### Handling errors in code This helper turns error responses into exceptions that carry the `code`, waits and retries when that makes sense, and returns `null` for a missing match. ```javascript class SoccerDataError extends Error { constructor(status, code, message) { super(message); this.status = status; this.code = code; } } const RETRYABLE = new Set(["RATE_LIMITED", "SERVICE_UNAVAILABLE", "UPSTREAM_UNAVAILABLE"]); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function getJson(path, params = {}, attempts = 3) { const url = `https://api.soccer-data.co/v1${path}?${new URLSearchParams(params)}`; for (let attempt = 1; ; attempt++) { const res = await fetch(url, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY } }); if (res.ok) return res.json(); const body = await res.json().catch(() => ({})); const code = body.error?.code ?? "UNKNOWN"; const message = body.error?.message ?? res.statusText; if (RETRYABLE.has(code) && attempt < attempts) { const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt; await sleep(wait * 1000); continue; } throw new SoccerDataError(res.status, code, message); } } async function getMatch(matchId) { try { return (await getJson(`/matches/${matchId}`)).data; } catch (err) { if (err instanceof SoccerDataError && err.code === "NOT_FOUND") return null; throw err; } } console.log(await getMatch("match_yoeml0e6ae37")); ``` ```python import os, time, requests RETRYABLE = {"RATE_LIMITED", "SERVICE_UNAVAILABLE", "UPSTREAM_UNAVAILABLE"} class SoccerDataError(Exception): def __init__(self, status, code, message): super().__init__(message) self.status = status self.code = code def get_json(path, params=None, attempts=3): for attempt in range(1, attempts + 1): res = requests.get( f"https://api.soccer-data.co/v1{path}", params=params, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) if res.ok: return res.json() try: error = res.json()["error"] except (ValueError, KeyError): error = {"code": "UNKNOWN", "message": res.reason} if error["code"] in RETRYABLE and attempt < attempts: time.sleep(int(res.headers.get("Retry-After", 2 ** attempt))) continue raise SoccerDataError(res.status_code, error["code"], error["message"]) def get_match(match_id): try: return get_json(f"/matches/{match_id}")["data"] except SoccerDataError as err: if err.code == "NOT_FOUND": return None raise print(get_match("match_yoeml0e6ae37")) ``` Notice that `USAGE_LIMIT_EXCEEDED` isn't in the retry list: the helper raises it straight away. [Rate limits](/docs/concepts/rate-limits) has more on staying inside your limits. --- ## Rate limits Your plan's per-minute and daily limits, the headers that track them, and how caching affects live data. Each plan comes with two limits: how many requests you can make in a minute, and how many in a day. Every response tells you where you stand, so your code never has to guess. ### Plans | Plan | Requests / day | Per minute | Live data refreshed (normally) | Odds | |---|---:|---:|---:|---| | Free | 50 | 10 | 5 min | — | | Pro | 1,000 | 60 | 15 s | Yes | | Business | 10,000 | 300 | 15 s | Yes | Daily limits run per UTC day and reset at midnight UTC. Odds endpoints need a paid plan (Pro or Business). You can see and change your plan on the [plans page](/dashboard/subscription). ### The two limits **Per minute.** Stops short bursts from flooding the API. Once you've used your plan's allowance, further requests get `429 RATE_LIMITED` until the minute is up. **Per day.** Caps your total for the UTC day. Once it's used up, requests get `429 USAGE_LIMIT_EXCEEDED` until midnight UTC, unless you upgrade. Both limits belong to your account, not to a key. All your API keys draw from the same allowance, so creating more keys doesn't add capacity. ### Response headers Every response to a request with a valid key carries these headers, whether it succeeded or not: | Header | What it tells you | |---|---| | `X-RateLimit-Limit` | Requests allowed per minute on your plan. | | `X-RateLimit-Remaining` | Requests left in the current minute. At `0`, the next one is refused. | | `X-RateLimit-Reset` | When the current minute's window ends, as a Unix timestamp in seconds. | | `X-Daily-Quota-Limit` | Requests allowed per UTC day on your plan. | | `X-Daily-Quota-Remaining` | Requests left today. | | `X-Daily-Quota-Reset` | When the daily quota resets (the next midnight UTC), as a Unix timestamp in seconds. | | `X-Cache` | `HIT` if the answer came from our cache, `MISS` if it was fetched fresh, `STALE` if it's an older copy served because a fresh one couldn't be fetched right now. | | `X-Cache-Age` | On `STALE` answers only: how old the copy is, in seconds. | | `Retry-After` | Only on `429` and `503`: how many seconds to wait before trying again. | Check them with curl's `-i` flag: ```bash curl -i "https://api.soccer-data.co/v1/competitions?per_page=1" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const res = await fetch("https://api.soccer-data.co/v1/competitions?per_page=1", { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); console.log("This minute:", res.headers.get("X-RateLimit-Remaining")); console.log("Today:", res.headers.get("X-Daily-Quota-Remaining")); ``` Your [usage page](/dashboard/usage) shows the same numbers over time, broken down by endpoint. ### Retry-After `Retry-After` means different things depending on the error: - **`RATE_LIMITED`:** the seconds until the minute window frees up, at most 60. Wait that long, then retry. - **`USAGE_LIMIT_EXCEEDED`:** the seconds until midnight UTC, which can be many hours. Don't retry in a loop; stop and tell someone, or upgrade. - **`SERVICE_UNAVAILABLE`:** a short pause before trying again. These responses don't count against your daily quota. ### What counts as a request A call counts against your daily quota when it reaches the data, whatever the data says back. That includes: - successful answers, including ones served from our cache (`X-Cache: HIT` or `STALE`); - errors that come from the data itself: a `400` for a value it can't use, such as a malformed date, a season from another competition or a page past the last one; a `404` for an id that doesn't exist or data a match doesn't have; or a `409` for a match in the wrong state; - each page of a paged list; - calls made from the [API tester](/dashboard/tester) in your dashboard. These don't count against your daily quota: - our own checks on the request, before it reaches the data: `400 UNKNOWN_PARAMETER` for a parameter the endpoint doesn't take, and `400 BAD_REQUEST` for a parameter sent as a list (such as `team_id[]=…`) instead of one value; - `401` for a missing or invalid key; - `403` when an endpoint isn't in your plan; - `429` when you're over a limit; - `404` for a path that isn't an endpoint at all; - server-side failures, such as `502` and `503`; - [`/health`](/docs/reference/health), which needs no key at all. The per-minute limit is simpler: it counts every call you make with a valid key, whatever the outcome. Calls already refused with `RATE_LIMITED` don't add to it, and neither does `/health`. ### Caching and live data Data that changes during a match, such as live scores, live stats, the match list and league tables, is cached on our side for a short time. How long depends on your plan: | Plan | Live answers are normally at most | |---|---| | Free | 5 minutes old | | Pro and Business | 15 seconds old | If a fresh answer can't be fetched, you may get an older copy instead, marked `X-Cache: STALE`: see [When an answer is stale](#when-an-answer-is-stale). Asking again within that window gives you the same answer, with `X-Cache: HIT`, and still counts as a request. So polling a live endpoint more often than your plan's refresh only spends requests. Every 15 to 30 seconds is a good pace on a paid plan, and every 5 minutes on Free. Data that doesn't move during a match, such as competitions, squads and finished-match stats, is cached for longer. #### When an answer is stale If we can't fetch a fresh answer right now, because a data source is slow or down, or we're under heavy load, you get the last good copy instead of an error, with `X-Cache: STALE` and its age in seconds in `X-Cache-Age`. A stale copy can be up to 15 minutes old for data that changes during a match, and up to 6 hours old for data that doesn't. For live data, check that age before showing a score as current. A stale answer counts like any other. If there's no copy recent enough to fall back on, you get `503 SERVICE_UNAVAILABLE` with a `Retry-After` header, and it doesn't count. ### Spending fewer requests - **Cache on your server.** Call the API from your backend and keep the answers. One stored response can serve all your users. This is also the only safe setup, because calling from the browser exposes your key. - **Keep slow-moving data longer.** Competitions, seasons, squads and finished matches rarely change. Store them for hours, or for good, rather than fetching them on every page view. - **Poll only while it matters.** Follow a live match while it's in play and stop at full time. There are no webhooks, so polling is the way to get updates; just keep it targeted. - **Use big pages and tight filters.** `per_page=100` plus filters like `team_id`, `date_from` and `date_to` get more done per request. - **Check flags first.** `xg_available`, `shotmap_available` and `odds_available` save you calls that would only return `404`. - **Be careful with AI agents.** A coding assistant stuck in a retry loop can use a day's requests in minutes. Make one test call, cache while you build, and ask it to respect `Retry-After`. ### Staying under the limit Rather than waiting for a `429`, watch `X-RateLimit-Remaining`. When it gets to `0`, pause until the time in `X-RateLimit-Reset`. And when a `429` does arrive, retry only `RATE_LIMITED`, and only a few times: ```javascript const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function request(path, retries = 3) { for (let attempt = 0; ; attempt++) { const res = await fetch(`https://api.soccer-data.co/v1${path}`, { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); if (res.ok) return res.json(); const { error } = await res.json(); if (error.code === "RATE_LIMITED" && attempt < retries) { await sleep(Number(res.headers.get("Retry-After") ?? 1) * 1000); continue; } throw new Error(`${res.status} ${error.code}: ${error.message}`); } } const { data } = await request("/competitions/competition_705uv6jm8myn"); console.log(data.name); ``` ```python import os, time, requests def request(path, retries=3): for attempt in range(retries + 1): res = requests.get( f"https://api.soccer-data.co/v1{path}", headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) if res.ok: return res.json() error = res.json()["error"] if error["code"] == "RATE_LIMITED" and attempt < retries: time.sleep(int(res.headers.get("Retry-After", 1))) continue raise RuntimeError(f'{res.status_code} {error["code"]}: {error["message"]}') print(request("/competitions/competition_705uv6jm8myn")["data"]["name"]) ``` `USAGE_LIMIT_EXCEEDED` isn't retried here on purpose: it won't clear until midnight UTC. [Errors](/docs/concepts/errors) has a fuller helper that also retries `503`. --- ## Coverage Find out which competitions and seasons have fixtures, stats, xG, lineups, tables and odds before you build on them. Coverage differs from one competition to the next, and from one season to the next. A top league might have xG for every match in recent seasons and only results further back; a smaller competition might have fixtures but no player stats. The coverage endpoints tell you exactly what exists, so you can check before you build a feature or promise data to your users. Every example sends your key in the `X-Api-Key` header. Keep that key on your server. ### The coverage endpoints | Endpoint | What it answers | |---|---| | [`/coverage/summary`](/docs/reference/coverage/summary) | Headline totals: how many competitions, seasons and finished matches we cover. | | [`/coverage/leagues`](/docs/reference/coverage/leagues) | Every competition, with the kinds of data it has and the range of seasons. Filter with `data_type` and `search`. | | [`/coverage/leagues/{competition_id}`](/docs/reference/coverage/league) | One competition, season by season: match counts and how many matches have each kind of data. | `/coverage/leagues` is paged with 100 results per page by default and up to 200. See [Pagination](/docs/concepts/pagination). ### Data types | Key | What it covers | Where you get the data | |---|---|---| | `fixtures` | The matches themselves. | [`/matches`](/docs/reference/matches/list) | | `team_stats` | Team stats for each match. | [`/matches/{match_id}/stats`](/docs/reference/matches/stats) | | `xg` | Expected goals. | `expected_goals` in [`/matches/{match_id}/stats`](/docs/reference/matches/stats) | | `player_stats` | Player stats for each match. | [`/matches/{match_id}/player-stats`](/docs/reference/matches/player-stats) | | `lineups` | Starting line-ups and formations. | [`/matches/{match_id}/lineups`](/docs/reference/matches/lineups) | | `standings` | Whether a league table exists. | [`/competitions/{competition_id}/seasons/{season_id}/standings`](/docs/reference/competitions/standings) | | `closing_odds` | The last prices before kickoff. | `last_seen` in [`/matches/{match_id}/odds`](/docs/reference/odds/prematch) | | `opening_odds` | The first prices we saw. | `opening` in [`/matches/{match_id}/odds`](/docs/reference/odds/prematch) | | `odds` | The same as `closing_odds`, kept for older code. | | Each type says whether it's `available`. Most also give `covered_events`, the number of finished matches that have it, and `coverage_pct`, the share of finished matches that have it, to one decimal place. We may add new keys over time, so skip any you don't recognise. ### Find competitions that have what you need Filter by data type, and by name if you like. This finds competitions with "Premier" in the name that have xG: ```bash curl "https://api.soccer-data.co/v1/coverage/leagues?data_type=xg&search=Premier" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const res = await fetch("https://api.soccer-data.co/v1/coverage/leagues?data_type=xg&search=Premier", { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); const { data } = await res.json(); for (const league of data) { console.log(league.id, league.name, league.seasons.first?.year, "to", league.seasons.last?.year); } ``` Each competition in the list looks like this (trimmed): ```json { "id": "competition_705uv6jm8myn", "name": "Premier League", "country": "England", "seasons": { "count": 13, "first": { "id": "season_yf8xfclqrgom", "year": "14/15" }, "last": { "id": "season_hr1ie7yrsdmt", "year": "26/27" } }, "data_types": { "fixtures": { "available": true }, "xg": { "available": true }, "opening_odds": { "available": true }, "closing_odds": { "available": true }, "player_stats": { "available": true } }, "latest_season": { "id": "season_hr1ie7yrsdmt", "year": "26/27", "status": "in_progress", "finished_events": 50, "total_events": 100 }, "total_finished_events": 4610 } ``` `seasons.first` and `seasons.last` give you a rough range. For the exact list of seasons, use the per-competition endpoint below. ### Check season by season ```bash curl "https://api.soccer-data.co/v1/coverage/leagues/competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const res = await fetch("https://api.soccer-data.co/v1/coverage/leagues/competition_705uv6jm8myn", { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); const { data } = await res.json(); for (const season of data.seasons) { if (season.status === "not_loaded") continue; console.log(season.year, "xG:", season.data_types.xg?.coverage_pct ?? 0, "%"); } ``` Each season looks like this (trimmed): ```json { "id": "season_98l95mxugqs9", "name": "Premier League 25/26", "year": "25/26", "status": "complete", "events": { "finished": 380, "total": 380 }, "data_types": { "xg": { "available": true, "covered_events": 380, "coverage_pct": 100 }, "opening_odds": { "available": false, "covered_events": 0, "coverage_pct": 0 }, "closing_odds": { "available": true, "covered_events": 380, "coverage_pct": 100 }, "player_stats": { "available": true, "covered_events": 380, "coverage_pct": 100 }, "standings": { "available": true } } } ``` `standings` only says whether a table exists, so it has no counts. #### Season status | Status | Means | |---|---| | `complete` | Every loaded match has been played. | | `in_progress` | Some matches are still to come. | | `not_loaded` | We know the season exists, but have no matches for it. Leave it out of your coverage figures. | The season ids here are the same ones you use with `/matches`, standings and the stats endpoints, so you can go straight from a coverage check to the data. ### Quick checks on a competition For a yes-or-no answer about a whole competition, fetch it with [`/competitions/{competition_id}`](/docs/reference/competitions/show) and read its flags: ```bash curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" \ -H "X-Api-Key: YOUR_API_KEY" ``` The flags are `has_team_stats`, `has_player_stats`, `xg_available`, `odds_available` and `live_odds_available`. They say whether the data exists anywhere in the competition, not which seasons have it. For that, use the coverage endpoints above. ### How far back the data goes It depends on the competition and the kind of data. Some examples: | Competition | Matches from | |---|---| | Premier League | 2014/15 | | LaLiga, Bundesliga, Serie A, Ligue 1, UEFA Champions League | 2020/21 | | MLS | 2020 | Even within one competition, data types start at different points. In the Premier League, fixtures, lineups, team stats, player stats and tables go back to 2014/15, closing odds to 2015/16, xG to 2022/23, and opening odds to 2026/27. #### Odds history - Up to the 2026/27 season, most matches have closing prices only, mostly from Bet365. - From 2026/27, matches can also have opening prices, from Bet365, Paddy Power, BetMGM UK, Pinnacle and Betfair Exchange. Check `opening_odds` and `closing_odds` for each season you need. [Odds](/docs/guides/odds) has more. ### Ask your AI assistant If you build with an AI coding tool, a prompt like this gets you a coverage report in one go: ```text Using the Soccer Data API (reference: https://soccer-data.co/llms.txt), call GET /coverage/leagues/competition_ozwhqjc3z9ic from my server and print a table of LaLiga seasons with coverage_pct for xg, player_stats and closing_odds. Skip seasons whose status is not_loaded. ``` ### Good to know - Only finished matches count towards `covered_events`, `coverage_pct` and `finished_events`. - In a season that's still running, `total` can be lower than the full number of matches, because later fixtures haven't been added yet. - Coverage figures are refreshed every few hours, so a match that has just finished may take a while to show up in them. - The per-competition endpoint doesn't always list seasons in date order. Sort by `year` yourself if order matters. ### Common mistakes - **Assuming a fixed number of years.** Check each competition and each data type. - **Counting `not_loaded` seasons.** Skip them. - **Relying on competition flags for a particular season.** They describe the whole competition. - **Expecting deep odds history.** Before 2026/27 it's mostly closing prices from one bookmaker. --- ## API reference Every endpoint at a glance, plus the conventions they all share. This section documents every Soccer Data endpoint: its parameters, the fields it returns and an example response. This page collects the rules they share and links to each one. ### Basics - **Base URL:** `https://api.soccer-data.co/v1` - **Method:** every endpoint is a `GET` that returns JSON. The API is read-only. - **Authentication:** send your key in the `X-Api-Key` header. Your first key is made when you sign up; create more on the [API keys](/dashboard/keys) page. Only [`/health`](/docs/reference/health) works without one. See [Authentication](/docs/authentication). - **Where to call from:** your server, so the key stays private. ```bash curl "https://api.soccer-data.co/v1/competitions?search=Premier" \ -H "X-Api-Key: YOUR_API_KEY" ``` ```javascript const res = await fetch("https://api.soccer-data.co/v1/competitions?search=Premier", { headers: { "X-Api-Key": process.env.SOCCER_DATA_KEY }, }); const { data, meta } = await res.json(); ``` ```python import os, requests res = requests.get( "https://api.soccer-data.co/v1/competitions", params={"search": "Premier"}, headers={"X-Api-Key": os.environ["SOCCER_DATA_KEY"]}, ) data = res.json()["data"] ``` ```php $data = Http::withHeaders(['X-Api-Key' => env('SOCCER_DATA_KEY')]) ->get('https://api.soccer-data.co/v1/competitions', ['search' => 'Premier']) ->json('data'); ``` > **Tip:** The [API tester](/dashboard/tester) in your dashboard runs any endpoint as your signed-in account, with no key needed, so you can see real responses before writing code. ### Response shapes - **Paged lists** (competitions, teams, players, matches, coverage by competition) return a `data` array and a `meta` object with `page`, `per_page`, `total` and `total_pages`. - **Single items** return one object in `data`. - **Short lists** (seasons, standings, squads) return a `data` array with no `meta`. - A few endpoints, such as shot maps and timelines, add other top-level fields next to `data`. ### IDs Ids are strings that start with a word saying what they name: `competition_`, `season_`, `team_`, `player_`, `match_`, `shot_`, `manager_` or `referee_`, then 12 letters and digits. They never change, so store them freely. Find them with the `search` parameter on competitions, teams and players. See [Data model](/docs/concepts/data-model). ### Pagination Paged lists return 20 results by default and up to 100 with `per_page`; `/coverage/leagues` returns 100 by default and up to 200. Move through pages with `page` and stop when it reaches `meta.total_pages`. See [Pagination](/docs/concepts/pagination). ### Errors Errors come as an `error` object with `code`, `message` and `status_code`. Write your logic against `code`. Two you'll meet early: - `400 UNKNOWN_PARAMETER`: the endpoint doesn't take that parameter. The message suggests the closest valid one and lists the parameters the endpoint takes. - `401 UNAUTHORIZED`: the key is missing, wrong or revoked. [Errors](/docs/concepts/errors) lists every code. ### Limits and plans Every authenticated response carries `X-RateLimit-*` headers for the per-minute limit, `X-Daily-Quota-*` headers for the daily limit, and `X-Cache` to show whether the answer came from cache. Going over a limit returns `429` with a `Retry-After` header. Odds need a paid plan (Pro or Business). See [Rate limits](/docs/concepts/rate-limits). ### Endpoints #### General | Endpoint | Path | What it's for | |---|---|---| | [Health check](/docs/reference/health) | `/health` | Check the API is up. Needs no key and doesn't count against your limits. | #### Competitions | Endpoint | Path | What it's for | |---|---|---| | [List competitions](/docs/reference/competitions/list) | `/competitions` | Search and filter leagues, cups and tournaments. | | [Get a competition](/docs/reference/competitions/show) | `/competitions/{competition_id}` | One competition, with its `current_season_id` and data flags. | | [List seasons](/docs/reference/competitions/seasons) | `/competitions/{competition_id}/seasons` | Every season of a competition, newest first. | | [List groups](/docs/reference/competitions/groups) | `/competitions/{competition_id}/seasons/{season_id}/groups` | The groups of a tournament season. | | [Standings](/docs/reference/competitions/standings) | `/competitions/{competition_id}/seasons/{season_id}/standings` | The league table, or the group tables. | #### Teams | Endpoint | Path | What it's for | |---|---|---| | [List teams](/docs/reference/teams/list) | `/teams` | Search teams, or list the teams in a season. | | [Get a team](/docs/reference/teams/show) | `/teams/{team_id}` | One team's profile. | | [Squad](/docs/reference/teams/players) | `/teams/{team_id}/players` | The current squad. | | [Season stats](/docs/reference/teams/stats) | `/teams/{team_id}/stats` | A team's record, position and form for one season. | | [Standings history](/docs/reference/teams/standings) | `/teams/{team_id}/standings` | Every competition and season where the team has a table entry. | | [Injuries and suspensions](/docs/reference/teams/injuries) | `/teams/{team_id}/injuries-suspensions` | Who's out, and why. | #### Matches | Endpoint | Path | What it's for | |---|---|---| | [List matches](/docs/reference/matches/list) | `/matches` | Fixtures, results and live matches, with filters. | | [Get a match](/docs/reference/matches/show) | `/matches/{match_id}` | One match, with the score breakdown, venue, referee and managers. | | [Lineups](/docs/reference/matches/lineups) | `/matches/{match_id}/lineups` | Starting line-ups, substitutes and formations. | | [Timeline](/docs/reference/matches/timeline) | `/matches/{match_id}/timeline` | Every event of a finished match. | | [Live timeline](/docs/reference/matches/live-timeline) | `/matches/{match_id}/live-timeline` | Events so far, while the match is live. | | [Team stats](/docs/reference/matches/stats) | `/matches/{match_id}/stats` | Team stats and xG for a finished match. | | [Live team stats](/docs/reference/matches/live-stats) | `/matches/{match_id}/live-stats` | Score, clock and team stats while the match is live. | | [Player stats](/docs/reference/matches/player-stats) | `/matches/{match_id}/player-stats` | Every player's stats for a finished match. | | [Live player stats](/docs/reference/matches/live-player-stats) | `/matches/{match_id}/live-player-stats` | Player stats so far, while the match is live. | | [Shot map](/docs/reference/matches/shotmap) | `/matches/{match_id}/shotmap` | Every shot, with xG and pitch position. | | [Player heatmap](/docs/reference/matches/player-heatmap) | `/matches/{match_id}/players/{player_id}/heatmap` | Where one player touched the ball in a match. | | [Referee](/docs/reference/matches/referee) | `/matches/{match_id}/referee` | The referee and their career totals. | #### Players | Endpoint | Path | What it's for | |---|---|---| | [List players](/docs/reference/players/list) | `/players` | Search players, or list a team's players. | | [Get a player](/docs/reference/players/show) | `/players/{player_id}` | One player's profile. | | [Season stats](/docs/reference/players/stats) | `/players/{player_id}/stats` | A player's totals for one season. | | [Injuries and suspensions](/docs/reference/players/injuries) | `/players/{player_id}/injuries-suspensions` | A player's absences. | | [Season heatmap](/docs/reference/players/season-heatmap) | `/players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap` | Where a player touched the ball across a season. | #### Odds | Endpoint | Path | What it's for | |---|---|---| | [Pre-match odds](/docs/reference/odds/prematch) | `/matches/{match_id}/odds` | Opening and latest prices per bookmaker. Pro or Business plan. | | [Live odds](/docs/reference/odds/live) | `/matches/{match_id}/odds/live` | In-play prices while the match is live. Pro or Business plan. | #### Coverage | Endpoint | Path | What it's for | |---|---|---| | [Coverage by competition](/docs/reference/coverage/leagues) | `/coverage/leagues` | Which data types each competition has. | | [Coverage for one competition](/docs/reference/coverage/league) | `/coverage/leagues/{competition_id}` | Season-by-season coverage for one competition. | | [Coverage summary](/docs/reference/coverage/summary) | `/coverage/summary` | Totals across everything we cover. | ### For AI tools The whole documentation, including every endpoint on this page, is also available as a single plain-text file at `https://soccer-data.co/llms.txt`. Point your AI coding tool at it, as described in [Build with AI](/docs/build-with-ai). --- ## Endpoint reference ### GET /health Check API status A lightweight call that tells you whether the API is up. Point an uptime monitor or a deploy check at it. - It only says the service is running. To check that your key works, call a data endpoint such as /competitions instead. Response fields: - `status` (string): healthy when the API is answering normally. - `timestamp` (string): The server's clock at the moment it answered, as an ISO 8601 date-time. Example: curl "https://api.soccer-data.co/v1/health" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /competitions Search competitions Returns leagues, cups and tournaments, 20 per page by default. Use it to look up a competition_id by name or country, then pass that id to the matches, teams and standings endpoints. - country_code=GB brings back English, Scottish, Welsh and Northern Irish competitions together. Use GB-ENG, GB-SCT, GB-WLS or GB-NIR for just one of them. - Asking for a page past the last one returns 400 PAGE_OUT_OF_RANGE. Query parameters: - `search` (string): Part of the competition name, case-insensitive. With sort=relevance the closest match comes first. - `country` (string): Country name, case-insensitive, for example England or Spain. - `country_code` (string): A code as it appears in country_code or subdivision_code, such as GB, GB-ENG, DE or ES. GB covers all four UK nations. - `type` (string, one of league, cup, tournament): Only competitions of this format. - `sort` (string, default "relevance", one of relevance, name): relevance puts the best matches for search first; name orders results A to Z. Without search, both give A to Z. - `page` (integer, default 1): Which page to return, starting at 1. meta.total_pages tells you how many there are. - `per_page` (integer, default 20): How many results per page, from 1 to 100. Response fields: - `data[].id` (string): The competition's id. Pass it as competition_id to other endpoints. - `data[].name` (string): The competition's name. - `data[].country` (string): Country name. Null for international and continental competitions; see confederation for those. - `data[].country_code` (string): Two-letter ISO 3166 country code, such as GB. Null for international competitions. UK nations are told apart in subdivision_code. - `data[].subdivision_code` (string): ISO 3166-2 code for competitions in England, Scotland, Wales or Northern Ireland: GB-ENG, GB-SCT, GB-WLS or GB-NIR. Null everywhere else. - `data[].confederation` (string): The governing body of an international competition: UEFA, CONMEBOL, FIFA, CAF, AFC, CONCACAF or OFC. Null for domestic competitions. - `data[].type` (string): The format: league, cup or tournament. - `data[].has_team_stats` (boolean): True when team statistics exist for this competition. - `data[].has_player_stats` (boolean): True when player statistics exist for this competition. - `data[].odds_available` (boolean): True when bookmaker odds exist for its matches, either pre-match prices or stored closing prices. - `data[].live_odds_available` (boolean): True when at least one of its matches has in-play odds right now. - `data[].xg_available` (boolean): True when expected goals (xG) data exists, with at least one non-zero value. - `meta.page` (integer): The page you're on. - `meta.per_page` (integer): Results per page. - `meta.total` (integer): Matching competitions across all pages. - `meta.total_pages` (integer): How many pages there are. Example: curl "https://api.soccer-data.co/v1/competitions?search=Premier+League" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /competitions/{competition_id} Competition details Returns one competition, including the id of its newest season and how many teams take part. Call it when you know the competition and need its current season. - Read current_season_id from here instead of hard-coding a season, so your app moves on when a new season starts. - current_season_id is null for competitions that are no longer played, so handle that case. Response fields: - `data.id` (string): The competition's id. - `data.name` (string): The competition's name. - `data.country` (string): Country name. Null for international competitions. - `data.country_code` (string): Two-letter ISO 3166 country code. Null for international competitions. - `data.subdivision_code` (string): GB-ENG, GB-SCT, GB-WLS or GB-NIR for competitions in the UK nations. Null everywhere else. - `data.confederation` (string): UEFA, CONMEBOL, FIFA, CAF, AFC, CONCACAF or OFC for international competitions. Null for domestic ones. - `data.type` (string): The format: league, cup or tournament. - `data.has_team_stats` (boolean): True when team statistics exist for this competition. - `data.has_player_stats` (boolean): True when player statistics exist for this competition. - `data.odds_available` (boolean): True when bookmaker odds exist for its matches, either pre-match prices or stored closing prices. - `data.live_odds_available` (boolean): True when at least one of its matches has in-play odds right now. - `data.xg_available` (boolean): True when expected goals (xG) data exists, with at least one non-zero value. - `data.current_season_id` (string): The id of the newest season. Null when the competition is dormant or has been discontinued. - `data.total_teams` (integer): How many teams take part. Example: curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /competitions/{competition_id}/seasons Seasons of a competition Lists every season of a competition, newest first. Use it to find the season_id of a past season for standings, matches or stats. - How far back the seasons go differs from one competition to the next. The coverage endpoints show what data each season has. - The season marked is_current has the same id as current_season_id on the competition. Response fields: - `data[].id` (string): The season's id. Pass it as season_id to other endpoints. - `data[].name` (string): Display name, such as Premier League 25/26. - `data[].year` (string): Short label for the season: one year (2022) or a split season (25/26). - `data[].start_year` (integer): The calendar year the season starts in, for example 2025 for 25/26. Null when it can't be worked out. - `data[].end_year` (integer): The calendar year the season ends in, for example 2026 for 25/26. Can be null. - `data[].is_current` (boolean): True only for the latest season. Example: curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /competitions/{competition_id}/seasons/{season_id}/groups Groups in a tournament season Lists the groups of a season that has a group stage, such as a World Cup, EURO or AFCON, from A up to L. Use the labels to fetch one group's fixtures or table. - Competitions without groups, like a domestic league, return an empty list. - Pass a group_label as group to /matches (together with competition_id) or to the season standings endpoint. - A season_id that belongs to a different competition returns 400 SEASON_COMPETITION_MISMATCH. Response fields: - `data[].group_label` (string): The group's letter, A to L. This is the value the group filters accept. - `data[].name` (string): A name ready to show, in English, such as Group A. Example: curl "https://api.soccer-data.co/v1/competitions/competition_7vpwx91c7j7h/seasons/season_st2tyewgtq2j/groups" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /competitions/{competition_id}/seasons/{season_id}/standings League table Returns the table for one season of a competition: one row per team, ordered by position. For tournaments with a group stage you get every group's table, or a single one with the group filter. - Group-stage tournaments return all groups, sorted by group_label and then position. - Knockout-only cups, such as the FA Cup, have no table and return an empty list. - A season_id that belongs to a different competition returns 400 SEASON_COMPETITION_MISMATCH. - The table can move while matches are in play. On the Free plan it is normally at most 5 minutes old; on paid plans, at most 15 seconds. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). Query parameters: - `group` (string, one of A, B, C, D, E, F, G, H, I, J, K, L): Return just this group's table, case-insensitive. Only meaningful for competitions with a group stage; a plain league gives an empty list. Response fields: - `data[].team.id` (string): The team's id. - `data[].team.name` (string): The team's name. - `data[].position` (integer): Place in the league or group, starting at 1. Null when not set yet, for example in group rows before a tournament begins. - `data[].matches_played` (integer): Matches played. - `data[].wins` (integer): Matches won. - `data[].draws` (integer): Matches drawn. - `data[].losses` (integer): Matches lost. - `data[].goals_for` (integer): Goals scored. - `data[].goals_against` (integer): Goals conceded. - `data[].goal_difference` (integer): goals_for minus goals_against. - `data[].points` (integer): Points earned. - `data[].group_label` (string): The group letter, A to L, in group-stage tables. Null in an ordinary league table. You can pass it back as group. Example: curl "https://api.soccer-data.co/v1/competitions/competition_705uv6jm8myn/seasons/season_98l95mxugqs9/standings" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /teams Search teams Returns clubs and national teams, 20 per page by default. Search by name, or narrow it to one competition or season to get every team taking part. - A club's men's and women's sides can have the same name. Use is_mens_team to keep only the one you want. - is_mens_team can be null when it isn't known, so don't assume it is always set. - Asking for a page past the last one returns 400 PAGE_OUT_OF_RANGE. Query parameters: - `search` (string): Part of the team name, case-insensitive. With sort=relevance the closest match comes first. - `competition_id` (string): Only teams in this competition. Without season_id, the latest season that has a table is used. - `season_id` (string): Only teams in this season. Send it with competition_id; a season from another competition returns 400 SEASON_COMPETITION_MISMATCH. - `country` (string): The team's country, case-insensitive, for example England. - `is_mens_team` (boolean, one of true, false): true keeps men's teams only, false keeps women's teams only. Leave it out to get both. - `sort` (string, default "relevance", one of relevance, name): relevance puts the best matches for search first; name orders results A to Z. Without search, both give A to Z. - `page` (integer, default 1): Which page to return, starting at 1. meta.total_pages tells you how many there are. - `per_page` (integer, default 20): How many results per page, from 1 to 100. Response fields: - `data[].id` (string): The team's id. Pass it as team_id to other endpoints. - `data[].name` (string): The team's full name. - `data[].short_name` (string): A shorter name for tight spaces. - `data[].country` (string): The team's country. - `data[].is_mens_team` (boolean): true for a men's team, false for a women's team, null when unknown. - `data[].primary_competition` (object): The main competition the team plays in. Can be null. - `data[].primary_competition.id` (string): That competition's id. - `data[].primary_competition.name` (string): That competition's name. - `meta.page` (integer): The page you're on. - `meta.per_page` (integer): Results per page. - `meta.total` (integer): Matching teams across all pages. - `meta.total_pages` (integer): How many pages there are. Example: curl "https://api.soccer-data.co/v1/teams?search=Arsenal" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /teams/{team_id} Team details Returns one team with its country, main competition and home stadium. Use it to show a team's profile once you have its id. - For the team's fixtures and results, pass its id as team_id to /matches. - stadium isn't guaranteed to be present for every team. Response fields: - `data.id` (string): The team's id. - `data.name` (string): The team's full name. - `data.short_name` (string): A shorter name for tight spaces. - `data.country` (string): The team's country. - `data.is_mens_team` (boolean): true for a men's team, false for a women's team, null when unknown. Useful when a men's and a women's side share a name. - `data.primary_competition` (object): The main competition the team plays in. Can be null. - `data.primary_competition.id` (string): That competition's id. - `data.primary_competition.name` (string): That competition's name. - `data.stadium` (object): The team's home ground. - `data.stadium.name` (string): Stadium name. - `data.stadium.capacity` (integer): How many people it holds. - `data.stadium.city` (string): The city it's in. Example: curl "https://api.soccer-data.co/v1/teams/team_74p8tg2k93z4" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /teams/{team_id}/players Team squad Returns the players currently in a team's squad, with their profile details and shirt numbers. Use it to build a squad page or to collect player ids for stats calls. - Everything comes in one response, up to 100 players. There is no pagination. - For a national team, you get the players who represent it. - Pass a player's id as player_id to /players/{player_id}/stats for their season numbers. Response fields: - `data[].id` (string): The player's id. - `data[].name` (string): Full name. - `data[].first_name` (string): First name. - `data[].last_name` (string): Last name. - `data[].position` (string): G (goalkeeper), D (defender), M (midfielder) or F (forward). An empty string when unknown. - `data[].date_of_birth` (string): Birth date as YYYY-MM-DD. Null when unknown. - `data[].age` (integer): Age in years. Null when the birth date is unknown. - `data[].nationality` (string): The player's nationality. - `data[].height_cm` (integer): Height in centimetres. Null when unknown. - `data[].current_team.id` (string): The id of the team the player is at now. - `data[].current_team.name` (string): That team's name. - `data[].current_team.jersey_number` (integer): Shirt number at that team. Null when unknown. - `data[].slug` (string): A URL-friendly version of the name. Can be null. - `data[].short_name` (string): Abbreviated name, such as A. Isak. Can be null. - `data[].preferred_foot` (string): The player's stronger foot. Can be null. - `data[].country_slug` (string): A URL-friendly version of the player's country. Can be null. - `data[].market_value` (number): Estimated market value, when one is known. Otherwise null. - `data[].contract_until` (string): When the current contract ends, as YYYY-MM-DD. Can be null. - `data[].national_team` (object): The national team the player represents, or null. - `data[].national_team.id` (string): The national team's id. - `data[].national_team.name` (string): The national team's name. Example: curl "https://api.soccer-data.co/v1/teams/team_74p8tg2k93z4/players" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /teams/{team_id}/injuries-suspensions Team absences Returns a team's injured and suspended players, split into two lists. Use it before a match to see who is unavailable, or with current=false to look back at past absences. - By default a suspension only shows up where it applies: a club sees bans from club competitions, a national team sees bans from international ones. Bans whose competition is unknown always show up. - A suspension with reason national_team means the player is away on international duty, not banned. Clubs see these; national teams don't. - If the same ban is reported twice, it is listed once. - expected_return is an estimate. Injuries have no fixed end date. - To check a single player, use /players/{player_id}/injuries-suspensions. Query parameters: - `current` (boolean, default true, one of true, false): true lists only players who are out right now. false lists every record, past ones included. Response fields: - `data.injuries` (array): Injury records. Always present, possibly empty. - `data.injuries[].player_id` (string): The injured player's id. - `data.injuries[].status` (string): How serious it is, for example out or day_to_day. Can be null. - `data.injuries[].reason` (string): A code for the injury, such as leg_injury. Can be null. - `data.injuries[].start_date` (string): When the injury began, as an ISO 8601 date-time. Can be null. - `data.injuries[].expected_return` (string): When the player is expected back, as an ISO 8601 date-time. An estimate; null when open-ended or unknown. - `data.injuries[].active` (boolean): True while the injury is still keeping the player out. Past injuries (with current=false) are false. - `data.suspensions` (array): Suspension records. Always present, possibly empty. - `data.suspensions[].player_id` (string): The suspended player's id. - `data.suspensions[].reason` (string): A code for the suspension, such as red_card_suspension. national_team means international duty, not a ban. Can be null. - `data.suspensions[].matches` (integer): How many matches the ban lasts. Can be null. - `data.suspensions[].competition` (object): The competition the ban applies to. The player can still play in other competitions. Can be null. - `data.suspensions[].competition.id` (string): That competition's id. - `data.suspensions[].competition.name` (string): That competition's name. - `data.suspensions[].start_date` (string): When the suspension starts, as an ISO 8601 date-time. Can be null. - `data.suspensions[].end_date` (string): When the suspension ends, as an ISO 8601 date-time. Can be null. - `data.suspensions[].active` (boolean): True while the suspension is in force. Past suspensions (with current=false) are false. Example: curl "https://api.soccer-data.co/v1/teams/team_6y79oahh4we9/injuries-suspensions" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /teams/{team_id}/stats Team season record Returns a team's results, goals, table position, recent form and expected goals for one season. Use it for a team summary card or to compare sides over a season. - season_id is required. Leaving it out returns 400 BAD_REQUEST. - Get season ids from /competitions/{competition_id}/seasons, or take current_season_id from the competition. - Returns 404 NOT_FOUND when the team has no stats for that season. - The xG totals only add up the matches counted in xg_matches, which can be fewer than matches_played. Check both before comparing teams. Query parameters: - `season_id` (string, required): The season you want numbers for. Response fields: - `data.team_id` (string): The team's id. - `data.season_id` (string): The season the numbers cover. - `data.competition_id` (string): The competition that season belongs to. - `data.matches_played` (integer): Matches played. - `data.wins` (integer): Matches won. - `data.draws` (integer): Matches drawn. - `data.losses` (integer): Matches lost. - `data.points` (integer): Points earned. - `data.position` (integer): Place in the table. - `data.goals_for` (integer): Goals scored. - `data.goals_against` (integer): Goals conceded. - `data.goal_difference` (integer): goals_for minus goals_against. - `data.form` (string): Results of the latest finished matches that season, up to five letters: W, D or L. - `data.expected_goals_for` (number): xG created, summed over the matches in xg_matches. Null when none of them has xG, for example in a competition without xG coverage. - `data.expected_goals_against` (number): xG conceded, summed over the matches in xg_matches. Null when none of them has xG. - `data.xg_matches` (integer): How many matches the xG totals are based on. Example: curl "https://api.soccer-data.co/v1/teams/team_f0ibhxdw7hs5/stats?season_id=season_98l95mxugqs9" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /teams/{team_id}/standings Where a team has tables Lists every competition and season in which a team has a row in the table. Use it to find the competition and season ids you need for a team's past and present tables. - Competitions are ordered by their most recent season. Inside each one, seasons run newest first. - A team that only plays knockout cups gets an empty list. - To get the full table, call /competitions/{competition_id}/seasons/{season_id}/standings with the ids from here. Response fields: - `data[].competition.id` (string): The competition's id. - `data[].competition.name` (string): The competition's name. - `data[].seasons[].id` (string): A season in which the team has a table row. - `data[].seasons[].name` (string): That season's display name. - `data[].seasons[].group_label` (string): The group letter, A to L, when the team's table was a group. Null for an ordinary league. Example: curl "https://api.soccer-data.co/v1/teams/{team_id}/standings" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches List matches Search fixtures and results across every competition we cover. Filter by competition, season, team, date range or status to build fixture lists, results pages or a live scoreboard. Results come 20 per page, newest kickoff first. - There is no single date filter. For one day's matches, send the same date as both date_from and date_to. - Send status=live to get every match in play right now. It is the easiest starting point for a live scoreboard. - If you send both competition_id and season_id, the season must belong to that competition, or you get a 400. - group only works together with competition_id. Without it you get a 400. - Asking for a page past the last one returns a 400 PAGE_OUT_OF_RANGE error. Use meta.total_pages to know where to stop. - Score breakdown fields stay null until a match is finished. - While matches are live, responses are normally at most 5 minutes old on Free and 15 seconds old on Pro and Business. Check live.updated_at to see how recent the clock is. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). Query parameters: - `competition_id` (string): Only return matches from this competition. - `season_id` (string): Only return matches from this season. If you also send competition_id, the season has to belong to it. - `team_id` (string): Only return matches this team plays in, at home or away. - `date_from` (date): Earliest kickoff day to include, as YYYY-MM-DD. The day is read in the utc_offset time zone. - `date_to` (date): Last kickoff day to include, as YYYY-MM-DD, up to the end of that day. It can't be before date_from. - `utc_offset` (string, default "+00:00"): Time zone for date_from and date_to, as +HH:MM or -HH:MM (up to 14 hours). Use it so a 'day' matches your users' local day. Kickoff times in the response stay in UTC. - `status` (string, one of scheduled, live, finished, postponed, cancelled): Only return matches in this state. Use live for matches being played now. - `matchday` (integer): League round number, from 1. Handy for showing one gameweek. - `stage` (string, default "all", one of regular, playoff, all): regular covers league and group games. playoff covers knockout rounds, cup ties and promotion or relegation play-offs. all returns both. - `group` (string, one of A, B, C, D, E, F, G, H, I, J, K, L): Only return matches from this tournament group. Not case-sensitive. Needs competition_id. - `sort` (string, default "-utc_date", one of -utc_date, utc_date): -utc_date puts the latest kickoff first, which suits results. utc_date puts the soonest first, which suits fixtures. Matches with the same kickoff are ordered by id. - `page` (integer, default 1): Page number, starting at 1. - `per_page` (integer, default 20): How many matches per page, from 1 to 100. Response fields: - `data[].id` (string): The match's id. Pass it to the other match endpoints for lineups, stats, events and more. - `data[].competition_id` (string): The competition this match belongs to. - `data[].season_id` (string): The season this match belongs to. - `data[].matchday` (integer|null): League round number. Null for cup fixtures. On play-off fixtures it is the play-off round number, not a league gameweek. - `data[].stage` (string): regular for league and group games, playoff for knockout, cup and play-off fixtures. - `data[].stage_name` (string|null): Name of the round for cup and knockout games, such as final, semi_final, quarter_final, round_of_16, group_stage or qualifying. Null for normal league games. - `data[].group_label` (string|null): Group letter (A to L) for group-stage games in tournaments that are split into groups. Null otherwise. - `data[].status` (string): Where the match stands: scheduled, live, finished, postponed or cancelled. - `data[].utc_date` (string): Kickoff time in UTC, as an ISO 8601 date-time. - `data[].home_team.id` (string): Home team's id. - `data[].home_team.name` (string): Home team's name. - `data[].away_team.id` (string): Away team's id. - `data[].away_team.name` (string): Away team's name. - `data[].score.home` (integer|null): Home goals in normal time. Null if the match hasn't been played. - `data[].score.away` (integer|null): Away goals in normal time. Null if the match hasn't been played. - `data[].score.final_score` (object|null): Aggregate score with home and away, included only when it differs from the normal-time score. - `data[].score.regulation` (object|null): Score after 90 minutes, with home and away. Null until the match is finished. - `data[].score.after_extra_time` (object|null): Score after 120 minutes, with home and away. Counts goals only, not shoot-out kicks. Null if there was no extra time. - `data[].score.penalty_shootout` (object|null): Penalties scored by each side in a shoot-out, with home and away. Null if the match wasn't settled on penalties. - `data[].score.went_to_extra_time` (boolean|null): Whether extra time was played. False for formats that go straight to penalties. Null until finished, or when we can't tell. - `data[].score.went_to_penalties` (boolean|null): Whether the match was decided by a shoot-out. Null until finished. - `data[].score.winner` (string|null): home, away or draw, taking extra time and penalties into account. Null until finished. - `data[].is_neutral` (boolean|null): True when the match is played at neither team's home ground. Null means we don't know, so don't treat it as false. - `data[].live` (object|null): The in-play clock. Null unless status is live. - `data[].live.match_status` (string|null): Detailed state of a live match: not_started, first_half, halftime, second_half, extra_time, penalties, finished, suspended or in_progress. - `data[].live.elapsed_minutes` (integer|null): Minutes since kickoff as a whole number. It keeps counting through the second half rather than starting again, and doesn't split out stoppage time. - `data[].live.period` (string|null): first_half, halftime, second_half or finished. Null when match_status is extra_time, penalties, suspended, not_started or in_progress. - `data[].live.updated_at` (string|null): When the clock was last updated. If it stops moving, live updates may have stalled, so don't assume the minute is current. - `data[].home_manager` (object|null): Home team's manager. Can be null. - `data[].home_manager.id` (string): Manager's id. - `data[].home_manager.name` (string): Manager's name. - `data[].away_manager` (object|null): Away team's manager. Can be null. - `data[].away_manager.id` (string): Manager's id. - `data[].away_manager.name` (string): Manager's name. - `data[].odds_available` (boolean): True when pre-match odds exist for this match. Always false for postponed or cancelled matches. - `data[].live_odds_available` (boolean): True when in-play odds are available right now. Always false for postponed or cancelled matches. - `data[].xg_available` (boolean): True when match-level expected goals (xG) exist. - `data[].shotmap_available` (boolean): True when a shot map with per-shot xG exists. - `data[].xg_quality` (string): full when per-shot xG values exist, none when they don't. Treat any value you don't recognise as none. - `meta.page` (integer): The page you're on. - `meta.per_page` (integer): Matches per page. - `meta.total` (integer): Matching results across all pages. - `meta.total_pages` (integer): Number of pages available. Example: curl "https://api.soccer-data.co/v1/matches?status=live" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id} Get a match Fetch one match in full: teams, kickoff, a detailed score breakdown with the half-time score, the venue, the referee and both managers. Use it for a match page, or to check which extra data (odds, xG, shot map) exists before you call those endpoints. - Check odds_available, xg_available and shotmap_available first, so you don't spend requests on data that isn't there. - shotmap_available can be false even when xg_available is true. - Score breakdown fields stay null until the match is finished. - This endpoint has no live clock. For the in-play minute, use List matches with status=live or the live stats endpoint. - While a match is live, responses are normally at most 5 minutes old on Free and 15 seconds old on Pro and Business. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). - An unknown match id returns 404 NOT_FOUND. Response fields: - `data.id` (string): The match's id. - `data.competition_id` (string): The competition this match belongs to. - `data.competition_name` (string): The competition's name, so you don't need a second call to show it. - `data.season_id` (string): The season this match belongs to. - `data.matchday` (integer|null): League round number. Null for cup and knockout fixtures. On play-off fixtures it is the play-off round number, not a league gameweek. - `data.stage` (string): regular for league and group games, playoff for knockout, cup and promotion or relegation play-off fixtures. - `data.stage_name` (string|null): Name of the round for cup games, such as final, semi_final or group_stage. Null for normal league games. - `data.group_label` (string|null): Group letter (A to L) for group-stage games in some tournaments. Null otherwise. - `data.status` (string): Where the match stands: scheduled, live, finished, postponed or cancelled. - `data.utc_date` (string): Kickoff time in UTC, as an ISO 8601 date-time. - `data.home_team.id` (string): Home team's id. - `data.home_team.name` (string): Home team's name. - `data.away_team.id` (string): Away team's id. - `data.away_team.name` (string): Away team's name. - `data.score.home` (integer|null): Home goals in normal time. Null if the match hasn't been played. - `data.score.away` (integer|null): Away goals in normal time. Null if the match hasn't been played. - `data.score.final_score` (object|null): Final score with home and away, included only when it differs from the normal-time score (for example after a shoot-out). - `data.score.regulation` (object|null): Score after 90 minutes, with home and away. Null until the match is finished. - `data.score.after_extra_time` (object|null): Goals after 120 minutes, with home and away. Null if there was no extra time. - `data.score.penalty_shootout` (object|null): Penalties scored by each side in a shoot-out, with home and away. Null if the match wasn't settled on penalties. - `data.score.went_to_extra_time` (boolean|null): Whether extra time was played. False for formats that go straight to penalties. Null until finished, or when we can't confirm it. - `data.score.went_to_penalties` (boolean|null): Whether the match was decided by a shoot-out. Null until finished. - `data.score.winner` (string|null): home, away or draw, taking extra time and penalties into account. Null until finished. - `data.score.half_time_home` (integer|null): Home goals at half-time. - `data.score.half_time_away` (integer|null): Away goals at half-time. - `data.venue` (object|null): Where the match is played. Null for cancelled matches that never had a venue. - `data.venue.name` (string): Stadium name. - `data.venue.city` (string): City the stadium is in. - `data.is_neutral` (boolean|null): True when the match is played at neither team's home ground. Null means we don't know, so don't treat it as false. - `data.referee` (object|null): The match referee. For career totals, use the match referee endpoint. - `data.referee.id` (string): Referee's id. - `data.referee.name` (string): Referee's name. - `data.home_manager` (object|null): Home team's manager. Can be null. - `data.home_manager.id` (string): Manager's id. - `data.home_manager.name` (string): Manager's name. - `data.away_manager` (object|null): Away team's manager. Can be null. - `data.away_manager.id` (string): Manager's id. - `data.away_manager.name` (string): Manager's name. - `data.odds_available` (boolean): True when pre-match odds exist. Older matches usually only have closing prices. - `data.live_odds_available` (boolean): True when in-play odds are available right now. - `data.xg_available` (boolean): True when match-level expected goals (xG) exist. - `data.shotmap_available` (boolean): True only when the shot map has at least one shot with an xG above zero. - `data.xg_quality` (string): full when per-shot xG values exist, none when they don't. Treat any value you don't recognise as none. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/lineups Get match lineups Get both teams' starting XIs, substitutes and formations for one match. Before kickoff you get a predicted XI, then the official team sheet, and once the match starts, the players who actually took part. - type tells you which lineup you have. Before the team sheet is out it is predicted. The official team sheet usually arrives about an hour before kickoff. Once the match starts it becomes played. - A predicted XI never has a bench, so bench_available is always false for it. - The starting XI can be confirmed a few minutes before the bench is published. Wait for bench_available to turn true before you show substitutes. - bench_available means the bench has been published, not that it is final. Late changes keep coming in until kickoff, so poll again close to the start. - Returns 404 when there is no lineup for the match yet, or the match id is unknown. - While a match is live, responses are normally at most 5 minutes old on Free and 15 seconds old on Pro and Business. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). Response fields: - `data.match_id` (string): The match's id. - `data.confirmed` (boolean): True for the official team sheet or the XI of a match that has started. False for a predicted XI. - `data.bench_available` (boolean): True once both teams have at least one substitute listed. Always false for a predicted XI. - `data.type` (string): predicted (an expected XI before the team sheet), team_sheet (the official pre-match XI) or played (the XI and substitutes of a match that has started or finished). - `data.home` (object): Home team's lineup. - `data.away` (object): Away team's lineup. Same fields as home. - `data.home.id` (string): Team's id. - `data.home.name` (string): Team's name. - `data.home.formation` (string|null): Formation, such as 4-3-3 or 4-2-3-1. Null when unknown. - `data.home.starting_xi[]` (array): The starting eleven, ordered goalkeeper, defenders, midfielders, forwards, then by shirt number. Handy for drawing a team sheet without sorting yourself. - `data.home.substitutes[]` (array): The bench: players who came on and those who didn't. Can be empty before kickoff, for example with a predicted XI. - `data.home.starting_xi[].id` (string): Player's id. Use it with the match player stats and match heatmap endpoints. Substitutes have the same fields. - `data.home.starting_xi[].name` (string): Player's name. - `data.home.starting_xi[].position` (string|null): G (goalkeeper), D (defender), M (midfielder) or F (forward). Null when unknown. - `data.home.starting_xi[].jersey_number` (integer|null): Shirt number. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/lineups" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/stats Get match stats Get the team stats of a played match: possession, xG, shots, passes, duels, defending and goalkeeping. Every stat comes for the full match and for each half, split into home and away, so you can build a full stats panel or compare the two halves. - Returns 409 MATCH_IS_LIVE while the match is in play. Use the live stats endpoint until full-time. - Returns 404 STATS_NOT_AVAILABLE when detailed stats were never collected for the match, and 404 NOT_FOUND when the match is unknown or its stats aren't in yet. - A period with no recorded value is null, never 0. A real zero looks like {"home": 0, "away": 0}. - expected_goals and np_expected_goals are null when the match has no xG. Check xg_available on the match first. - Stat depth varies by competition. See the coverage docs before you rely on a stat. Response fields: - `data.match_id` (string): The match's id. - `data.overview.{stat}` (object): The headline numbers. Stats: ball_possession, expected_goals, big_chances, total_shots, shots_on_target, goalkeeper_saves, corner_kicks, fouls, yellow_cards, red_cards, passes, accurate_passes, tackles, free_kicks. - `data.shots.{stat}` (object): Shooting detail. Stats: total_shots, shots_on_target, shots_off_target, blocked_shots, shots_inside_box, shots_outside_box, hit_woodwork. - `data.attack.{stat}` (object): Attacking play. Stats: big_chances_missed, touches_in_penalty_area, fouled_in_final_third, offsides. - `data.passes.{stat}` (object): Passing. Stats: accurate_passes, throw_ins, accurate_crosses, accurate_long_balls, final_third_entries. - `data.duels.{stat}` (object): Duels and dribbling. Stats: duels_won_percentage, dispossessed, dribbles_percentage, ground_duels_percentage, aerial_duels_percentage. - `data.defending.{stat}` (object): Defending. Stats: tackles, tackles_won_percentage, interceptions, clearances, ball_recoveries. - `data.goalkeeping.{stat}` (object): Goalkeeping. Stats: saves, goal_kicks, goals_prevented, high_claims. - `data.{group}.{stat}.all` (object|null): The value for the whole match. Null when it wasn't recorded. - `data.{group}.{stat}.first_half` (object|null): The value for the first half. Null when it wasn't recorded. - `data.{group}.{stat}.second_half` (object|null): The value for the second half. Null when it wasn't recorded. - `data.{group}.{stat}.{period}.home` (number): Home team's value. ball_possession and the stats ending in _percentage are percentages. - `data.{group}.{stat}.{period}.away` (number): Away team's value. - `data.np_expected_goals` (object|null): Non-penalty xG, with the same all, first_half and second_half blocks as the other stats. Null when the match has no non-penalty xG, not 0. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/stats" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/live-stats Get live match stats Get running team stats for a match in play, along with the clock and the score. Poll it during the match to keep a live stats panel up to date. Values are totals for the match so far, split into home and away. - Only works while the match is in play. Before kickoff, and for postponed or cancelled matches, it returns 409. - For up to 5 minutes after full-time you still get the final snapshot, with meta.match_status set to finished. After that it returns 409, so switch to the match stats endpoint. - Which stats appear depends on the match. Don't expect every stat from the match stats endpoint to be present. - Responses are normally at most 5 minutes old on Free and 15 seconds old on Pro and Business. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). - There are no webhooks or streams. Poll this endpoint to pick up changes. - Find matches in play with List matches and status=live. Response fields: - `data.match_id` (string): The match's id. - `data.meta` (object): The clock and score at the moment of this snapshot. - `data.meta.match_status` (string|null): Detailed state: not_started, first_half, halftime, second_half, extra_time, penalties, finished, suspended or in_progress. finished only shows for up to 5 minutes after full-time. - `data.meta.elapsed_minutes` (integer|null): Minutes since kickoff, counted straight through both halves, as a whole number. Stoppage time isn't split out. The same value appears as live.elapsed_minutes in List matches. - `data.meta.home_goals` (integer|null): Home team's goals so far. - `data.meta.away_goals` (integer|null): Away team's goals so far. - `data.meta.ht_score` (string|null): Half-time score as text. Can be null. - `data.meta.period` (string|null): first_half, halftime, second_half or finished. - `data.stats.{stat}` (object): One running total per stat, keyed by stat name. Names are the same as in the match stats endpoint, for example ball_possession, expected_goals, total_shots and corner_kicks. - `data.stats.{stat}.all.home` (number): Home team's value for the match so far. - `data.stats.{stat}.all.away` (number): Away team's value for the match so far. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/live-stats" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/player-stats Get match player stats Get every player's stats from a played match: rating, minutes, passing, shooting, duels, defending, goalkeeping and discipline. Use it for player ratings, a man-of-the-match pick or fantasy points. Pass player_ids to get only the players you care about. - Returns 409 MATCH_IS_LIVE while the match is in play. Use the live player stats endpoint until full-time. - Returns 404 STATS_NOT_AVAILABLE when detailed stats were never collected for the match, and 404 NOT_FOUND when the match is unknown or its stats aren't in yet. - rating, the xG fields and ball_recoveries can be null. Treat null as 'not recorded', not as zero. - Stat depth varies by competition. See the coverage docs before you rely on a stat. Query parameters: - `player_ids` (string): Comma-separated player ids. Only these players are returned. Leave it out to get every player. Response fields: - `data[].player_id` (string): Player's id. - `data[].player_name` (string): Player's name. - `data[].team_id` (string): Id of the team the player played for. - `data[].position` (string): G, D, M or F. An empty string when unknown. - `data[].rating` (number|null): Match rating. - `data[].minutes_played` (integer): Minutes on the pitch. - `data[].started` (boolean): Whether the player was in the starting XI. - `data[].played` (boolean): Whether the player got any minutes. False for unused substitutes. - `data[].passing.{stat}` (integer): Passing counts. Stats: total_passes, accurate_passes, key_passes, assists, total_crosses, accurate_crosses, total_long_balls, accurate_long_balls. - `data[].shooting.{stat}` (integer): Shooting counts. Stats: goals, total_shots, shots_on_target, shots_off_target, blocked_shots, big_chances_created. - `data[].shooting.expected_goals` (number|null): Expected goals (xG) from the player's shots. - `data[].shooting.expected_assists` (number|null): Expected assists (xA) from the player's passes. - `data[].shooting.np_expected_goals` (number|null): xG without penalties. - `data[].duels.{stat}` (integer): Duel counts. Stats: duel_won, duel_lost, aerial_won, challenge_lost, won_contest (take-ons won), dispossessed. - `data[].defending.{stat}` (integer): Defensive counts. Stats: tackles, interceptions, clearances. - `data[].defending.ball_recoveries` (integer|null): Times the player won the ball back. Null for older matches where it wasn't recorded; 0 means none. The players' values add up to the team's defending.ball_recoveries in the match stats endpoint. - `data[].goalkeeping.saves` (integer): Saves made. - `data[].general.{stat}` (integer): General counts. Stats: touches, fouls (committed), was_fouled, offsides, yellow_cards, red_cards, possession_lost. - `data[].general.player_subbed_on` (string|null): Id of the player who came on to replace this player. Null if this player wasn't taken off. - `data[].general.player_subbed_off` (string|null): Id of the player this player replaced when coming on. Null if this player didn't come off the bench. Example: curl "https://api.soccer-data.co/v1/matches/{match_id}/player-stats" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/live-player-stats Get live match player stats Get every player's stats while a match is being played, with the same fields as the match player stats endpoint. Poll it to keep live ratings, fantasy scores or player cards up to date. Pass player_ids to follow only a few players. - Only works while the match is in play. Otherwise it returns 409, so switch to the match player stats endpoint after full-time. - Find matches in play with List matches and status=live. - Responses are normally at most 5 minutes old on Free and 15 seconds old on Pro and Business. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). - There are no webhooks or streams. Poll this endpoint to pick up changes. - rating, the xG fields, ball_recoveries and the substitution fields can be null. Query parameters: - `player_ids` (string): Comma-separated player ids. Only these players are returned. Leave it out to get every player. Response fields: - `data[].player_id` (string): Player's id. - `data[].player_name` (string): Player's name. - `data[].team_id` (string): Id of the team the player plays for in this match. - `data[].position` (string): G, D, M or F. An empty string when unknown. - `data[].rating` (number|null): Match rating so far. - `data[].minutes_played` (integer): Minutes on the pitch so far. - `data[].started` (boolean): Whether the player was in the starting XI. - `data[].played` (boolean): Whether the player has had any minutes yet. - `data[].passing.{stat}` (integer): Passing counts. Stats: total_passes, accurate_passes, key_passes, assists, total_crosses, accurate_crosses, total_long_balls, accurate_long_balls. - `data[].shooting.{stat}` (integer): Shooting counts. Stats: goals, total_shots, shots_on_target, shots_off_target, blocked_shots, big_chances_created. - `data[].shooting.expected_goals` (number|null): Expected goals (xG) from the player's shots so far. - `data[].shooting.expected_assists` (number|null): Expected assists (xA) from the player's passes so far. - `data[].shooting.np_expected_goals` (number|null): xG without penalties. - `data[].duels.{stat}` (integer): Duel counts. Stats: duel_won, duel_lost, aerial_won, challenge_lost, won_contest (take-ons won), dispossessed. - `data[].defending.{stat}` (integer): Defensive counts. Stats: tackles, interceptions, clearances. - `data[].defending.ball_recoveries` (integer|null): Times the player won the ball back. Null when not recorded; 0 means none. - `data[].goalkeeping.saves` (integer): Saves made. - `data[].general.{stat}` (integer): General counts. Stats: touches, fouls (committed), was_fouled, offsides, yellow_cards, red_cards, possession_lost. - `data[].general.player_subbed_on` (string|null): Id of the player who came on to replace this player. Null if this player hasn't been taken off. - `data[].general.player_subbed_off` (string|null): Id of the player this player replaced when coming on. Null if this player didn't come off the bench. - `meta.match_status` (string): Always live on this endpoint. Example: curl "https://api.soccer-data.co/v1/matches/{match_id}/live-player-stats" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/timeline Get match timeline Get the events of a played match in the order they happened: goals, cards, substitutions, shots, VAR checks and more. Use it to build a match report, a goal list or a minute-by-minute feed. Filter by event type, period or team to get just what you need. - Returns 409 while the match is live. Use the live timeline endpoint until full-time. - Goals come in three types. Penalties are penalty_scored and own goals are own_goal, so send event_type=goal,penalty_scored,own_goal to get every goal. - Shoot-out kicks are also penalty_scored, with period set to penalties. Use the period filter to leave them out. - Stoppage time is always split out. A card at 45+3 has minute 45 and extra_time 3. - If a match has no timeline, you still get a 200 with an empty events list. meta.coverage is none and meta.reason tells you why. - An unknown event_type returns 400. Query parameters: - `event_type` (string, one of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork, yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end, added_time): Comma-separated list of event types to keep. Leave it out to get every event. - `period` (string, one of first_half, second_half, extra_time_first_half, extra_time_second_half, penalties): Comma-separated list of periods to keep, for example only the second half. - `team_id` (string): Only return events for this team. Response fields: - `data.match_id` (string): The match's id. - `data.coverage` (string): full when a timeline exists for this match, none when it doesn't. - `data.events[]` (array): Events in the order they happened. Empty when there is no timeline. Read the list as it comes; it is already sorted. - `data.events[].sequence` (integer): Position of the event in the match, starting at 1. - `data.events[].minute` (integer): Minute of the event within normal time for that period. Stoppage minutes go in extra_time. - `data.events[].extra_time` (integer): Stoppage minutes on top of minute. 0 for events in normal time. - `data.events[].period` (string): first_half, second_half, extra_time_first_half, extra_time_second_half or penalties. - `data.events[].type` (string): What happened. One of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork (hit the post or bar), yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end or added_time. For own_goal, player is the one who put it in their own net and team is their side, so the goal counts for the other team. - `data.events[].team` (object|null): The team involved. - `data.events[].team.id` (string|null): Team's id. Null when only the name is known. - `data.events[].team.name` (string): Team's name. - `data.events[].team.slug` (string|null): URL-friendly name, handy for building links. - `data.events[].player` (object|null): The player involved. Same fields as team: id (null when only the name is known), name and slug. - `meta.total` (integer): Number of events returned, after filters. - `meta.coverage` (string): Same as data.coverage: full or none. - `meta.reason` (string): Only present when coverage is none. match_not_started, no_events, or not_supported when we don't cover events for this competition. - `meta.last_updated` (string|null): When the timeline last changed. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/timeline" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/live-timeline Get live match timeline Get the events of a match while it is being played: goals, cards, substitutions, shots and more, in order. Poll it during the match to power a live feed or goal alerts. The response has the same shape as the match timeline. - Only works while the match is in play. Otherwise it returns 409, so switch to the match timeline endpoint after full-time. - Find matches in play with List matches and status=live. - There are no webhooks or streams. Poll this endpoint to pick up new events. - Responses are normally at most 5 minutes old on Free and 15 seconds old on Pro and Business. Use meta.last_updated to see how fresh the events are. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). - Penalties are penalty_scored and own goals are own_goal, so send event_type=goal,penalty_scored,own_goal to catch every goal. Shoot-out kicks are penalty_scored with period set to penalties. - An unknown event_type returns 400. Query parameters: - `event_type` (string, one of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork, yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end, added_time): Comma-separated list of event types to keep. Leave it out to get every event. - `period` (string, one of first_half, second_half, extra_time_first_half, extra_time_second_half, penalties): Comma-separated list of periods to keep. - `team_id` (string): Only return events for this team. Response fields: - `data.match_id` (string): The match's id. - `data.coverage` (string): full when we have events for this match, none when we don't. - `data.events[]` (array): Events so far, in the order they happened. Read the list as it comes; it is already sorted. - `data.events[].sequence` (integer): Position of the event in the match, starting at 1. - `data.events[].minute` (integer): Minute within normal time for that period (up to 45 in the first half, up to 90 in the second). Stoppage minutes go in extra_time. - `data.events[].extra_time` (integer): Stoppage minutes on top of minute. 0 for events in normal time. - `data.events[].period` (string): first_half, second_half, extra_time_first_half, extra_time_second_half or penalties. - `data.events[].type` (string): What happened. One of goal, own_goal, shot_on_target, shot_off_target, shot_blocked, shot_saved, shot_woodwork (hit the post or bar), yellow_card, red_card, yellow_red_card, foul, offside, corner_kick, substitution, penalty_awarded, penalty_scored, penalty_missed, penalty_saved, var, period_start, period_end or added_time. For own_goal, player is the one who put it in their own net and team is their side, so the goal counts for the other team. - `data.events[].team` (object|null): The team involved. - `data.events[].team.id` (string|null): Team's id. Null when only the name is known. - `data.events[].team.name` (string): Team's name. - `data.events[].team.slug` (string|null): URL-friendly name, handy for building links. - `data.events[].player` (object|null): The player involved. Same fields as team: id (null when only the name is known), name and slug. - `meta.total` (integer): Number of events returned, after filters. - `meta.coverage` (string): Same as data.coverage: full or none. - `meta.reason` (string): Only present when coverage is none. match_not_started, no_events, or not_supported when we don't cover events for this competition. - `meta.last_updated` (string|null): When the events last changed. - `meta.match_status` (string): Always live on this endpoint. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/live-timeline" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/shotmap Get match shot map Get every shot in a match with its pitch position, outcome and expected goals (xG). Use it to draw a shot map, compare chance quality between teams or break down one player's shooting. Pass player_id to get only that player's shots. - Check shotmap_available on the match first. It is only true when at least one shot has an xG above zero. - It also works during a match. meta.is_final is false while the match is live, so more shots can still appear, and an empty data list just means nobody has shot yet. - A shot's expected_goals can be null when no xG exists for it. The match's xg_quality tells you what to expect. - Penalties are left out of np_xg_summary. stored can differ from live when a match-level figure has been recorded. - Returns 404 STATS_NOT_AVAILABLE when detailed stats were never collected for the match, and 404 NOT_FOUND when the match is unknown or has no shot data. Query parameters: - `player_id` (string): Only return shots taken by this player. Response fields: - `match_id` (string): The match's id. - `event.id` (string): The match's id again. - `event.home_team_id` (string): Home team's id. Compare it with each shot's team_id to tell the sides apart. - `event.away_team_id` (string): Away team's id. - `data[]` (array): Every shot in the match, or only the chosen player's shots. - `data[].id` (string): The shot's id. - `data[].player_id` (string): Id of the player who took the shot. - `data[].player_name` (string): Name of the player who took the shot. - `data[].team_id` (string): Id of the shooting team. - `data[].team_name` (string): Name of the shooting team. - `data[].x` (number): Distance from the goal being attacked, from 0 to 100. 0 is that goal line and 100 is the shooter's own goal line. Every shot is flipped to attack the same goal, so you can plot both teams on one half. The edge of the box is at 16.5 and the penalty spot at about 11. - `data[].y` (number): Position across the pitch, from 0 to 100. 50 is the middle of the goal. The penalty box runs from 20.5 to 79.5. - `data[].minute` (integer): Minute the shot was taken. - `data[].result` (string): What happened to the shot, for example goal, save, miss, block, blocked_off_line or post. - `data[].expected_goals` (number|null): The shot's xG: how likely a shot like this is to go in. Null when no xG exists for it. - `data[].situation` (string|null): How the chance came about: assisted, regular or fast_break (open play), corner, free_kick, set_piece or throw_in_set_piece (dead ball), penalty, shootout or own_goal. - `data[].body_part` (string|null): What the shot was taken with, for example right_foot. - `data[].goal_type` (string|null): Only set for goals, for example regular or own. Null otherwise. - `data[].goal_mouth_location` (string|null): Which part of the goal mouth the shot went to, for example low_centre. - `data[].goal_mouth_coordinates` (object|null): Where the shot reached the goal mouth, as x, y and z numbers. - `data[].block_coordinates` (object|null): Where the shot was blocked, as x, y and z numbers. - `data[].is_blocked_shot` (boolean): Whether the shot was blocked. - `data[].blocked_by_player_id` (string|null): Id of the player who blocked it. - `data[].goalkeeper` (object|null): The goalkeeper facing the shot, with id and name. - `data[].is_goal` (boolean): Whether the shot was a goal. - `data[].is_on_target` (boolean): True for goals, saves and shots cleared off the line. False for own goals, which show up with goal_type own. - `data[].is_headed` (boolean): Whether the shot was a header. - `data[].is_outside_box` (boolean): True when the shot was taken outside the penalty box (x above 16.5, or y outside 20.5 to 79.5). - `data[].is_penalty` (boolean): Whether the shot was a penalty. - `np_xg_summary.live` (object): Non-penalty xG per team, added up from the shots in data. Has home_team and away_team numbers. - `np_xg_summary.stored` (object): The recorded match-level non-penalty xG, with home_team and away_team. Same as live when no recorded figure exists. - `meta.is_final` (boolean): False while the match is live and shots can still be added. True otherwise. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/shotmap" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/referee Get match referee Get the referee for a match along with their career totals for games, yellow cards and red cards. Use it to judge how strict a referee tends to be, for example for card markets or match previews. - referee is null when no referee has been assigned yet, so handle that case. - If you only need the referee's name, Get a match already includes it, so you can skip this call. Response fields: - `data.match_id` (string): The match's id. - `data.referee` (object|null): The referee for this match. Null when none is assigned. - `data.referee.id` (string): Referee's id. - `data.referee.name` (string): Referee's name. - `data.referee.slug` (string|null): URL-friendly name, handy for building links. - `data.referee.country` (string): Country the referee comes from. - `data.referee.country_code` (string|null): Two-letter ISO country code, when we have it. - `data.referee.country_slug` (string|null): URL-friendly country name. - `data.referee.career` (object): Totals across the referee's career. - `data.referee.career.games` (integer): Matches refereed. - `data.referee.career.yellow_cards` (integer): Yellow cards shown. - `data.referee.career.red_cards` (integer): Red cards shown. - `data.referee.career.yellow_red_cards` (integer): Red cards that came from a second yellow. Example: curl "https://api.soccer-data.co/v1/matches/match_9pd0ck6g2m9i/referee" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/players/{player_id}/heatmap Get match player heatmap Get the spots where one player touched the ball in one match. Plot the points to draw a heatmap of where they played. Get player ids from the match lineups or match player stats. - Returns 404 when there is no heatmap for this player in this match, for example an unused substitute or a match without positional data. An unknown match or player id also returns 404. - For a whole season of touches, use the player season heatmap endpoint instead. - How far back heatmaps go depends on the competition. See the coverage docs. Response fields: - `data.match_id` (string): The match's id. - `data.player.id` (string): Player's id. - `data.player.name` (string): Player's name. - `data.team.id` (string): Id of the team the player played for. - `data.team.name` (string): Name of that team. - `data.team.side` (string): home or away. - `data.points[]` (array): The player's touches, in the order they were recorded. - `data.points[].x` (integer): Position along one axis of the pitch, as a percentage from 0 to 100. - `data.points[].y` (integer): Position along the other axis of the pitch, as a percentage from 0 to 100. Example: curl "https://api.soccer-data.co/v1/matches/match_lhpm4gzskh3h/players/player_zm7yzjr75yjn/heatmap" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /players Search players Find players by name, list the players currently at a club, or load several players at once by id. Results come 20 per page. Most apps start here to get the player id that the other player endpoints need. - search needs at least 3 characters. A shorter term returns 400 BAD_REQUEST. - Search ignores case and accents, and treats hyphens and spaces as the same thing. - When you search, meta.total is capped at 1,000 even if more players match. - team_id looks at each player's current club only, so players who have left don't show up. Query parameters: - `search` (string): Part of the player's name, at least 3 characters. With sort=relevance the closest matches come first. - `team_id` (string): Only players who are at this team right now. - `position` (string, one of G, D, M, F): Only players in this position: G goalkeeper, D defender, M midfielder, F forward. - `player_ids` (string): Comma-separated player ids. Returns just those players, which is handy for refreshing a saved list in one request. - `sort` (string, default "relevance", one of relevance, name): relevance puts the best search matches first. name sorts A to Z. Without search, results are always A to Z. - `page` (integer, default 1): Page number, starting at 1. meta.total_pages tells you how many pages there are. - `per_page` (integer, default 20): Players per page, from 1 to 100. Response fields: - `data[]` (array): The players on this page. - `data[].id` (string): The player's id. Pass it to the other player endpoints. - `data[].name` (string): The name the player is usually shown under. - `data[].first_name` (string): First name. - `data[].last_name` (string): Last name. - `data[].position` (string): G, D, M or F. An empty string when the position isn't known. - `data[].date_of_birth` (string|null): Date of birth as YYYY-MM-DD. Null when unknown. - `data[].age` (integer|null): Age in years. Null when the date of birth is unknown. - `data[].nationality` (string): Country the player represents, by name. - `data[].height_cm` (integer|null): Height in centimetres. Null when unknown. - `data[].current_team` (object): The club the player is at now. - `data[].current_team.id` (string): The club's team id. - `data[].current_team.name` (string): The club's name. - `data[].current_team.jersey_number` (integer|null): Shirt number at that club. Null when unknown. - `meta.page` (integer): The page you asked for. - `meta.per_page` (integer): Players per page. - `meta.total` (integer): Matching players across all pages. Capped at 1,000 when you search. - `meta.total_pages` (integer): Number of pages available. Example: curl "https://api.soccer-data.co/v1/players?search=Saka" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /players/{player_id} Player details Fetch one player's profile: name, position, date of birth, age, nationality, height, and the club they play for now with their shirt number. Use it to fill a player card or profile page. - This is profile data only. For goals, assists, minutes and other numbers, use the player season stats endpoint. - An unknown player id returns 404 NOT_FOUND. Response fields: - `data.id` (string): The player's id. - `data.name` (string): The name the player is usually shown under. - `data.first_name` (string): First name. - `data.last_name` (string): Last name. - `data.position` (string): G, D, M or F. An empty string when the position isn't known. - `data.date_of_birth` (string|null): Date of birth as YYYY-MM-DD. Null when unknown. - `data.age` (integer|null): Age in years. Null when the date of birth is unknown. - `data.nationality` (string): Country the player represents, by name. - `data.height_cm` (integer|null): Height in centimetres. Null when unknown. - `data.current_team` (object): The club the player is at now. - `data.current_team.id` (string): The club's team id. - `data.current_team.name` (string): The club's name. - `data.current_team.jersey_number` (integer|null): Shirt number at that club. Null when unknown. Example: curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /players/{player_id}/stats Player season stats A player's totals for one season: appearances, minutes and rating, plus grouped numbers for scoring, shooting, passing, defending, duels and discipline. Use it for season stat tables, player comparisons or fantasy tools. - season_id is required. Leaving it out returns 400 BAD_REQUEST. - If you pass a competition_id that the season doesn't belong to, you get 400 SEASON_COMPETITION_MISMATCH. - expected_goals and expected_assists only add up appearances that had xG data. xg_matches says how many that was, and it can be lower than appearances. Both are null when none of the player's matches had xG. - Percentage fields are numbers such as 76.7, meaning 76.7%. - How much detail you get depends on the competition. GET /coverage/leagues shows what each league has. Query parameters: - `season_id` (string, required): The season to total up. Get it from a competition's seasons list, or use the competition's current_season_id. - `competition_id` (string): Count only matches in this competition. - `stage` (string, default "all", one of regular, playoff, all): regular counts league and group-stage matches only. playoff counts knockout and play-off matches only. all counts every match the player appeared in. Response fields: - `data.player_id` (string): The player these totals belong to. - `data.season_id` (string): The season the totals cover. - `data.team_id` (string): The player's team for these totals. - `data.position` (string): G, D, M or F. An empty string when the position isn't known. - `data.rating` (number|null): The player's rating for the season. Null when there isn't one. - `data.appearances` (integer): Matches the player took part in. - `data.starts` (integer): Matches the player started. - `data.minutes_played` (integer): Total minutes on the pitch. - `data.scoring.goals` (integer): Goals scored. - `data.scoring.assists` (integer): Assists. - `data.scoring.goals_assists_sum` (integer): Goals plus assists. - `data.scoring.goal_conversion_percentage` (number): Share of shots turned into goals, as a percentage. - `data.scoring.penalties_taken` (integer): Penalties taken. - `data.scoring.penalty_goals` (integer): Penalties scored. - `data.scoring.big_chances_created` (integer): Big chances set up for team-mates. - `data.scoring.big_chances_missed` (integer): Big chances the player failed to score. - `data.scoring.expected_goals` (number|null): Season xG, summed over the appearances counted in xg_matches. Null when none of them had xG. - `data.scoring.expected_assists` (number|null): Season xA, on the same basis as expected_goals. Null when none of the appearances had xG. - `data.scoring.xg_matches` (integer): Appearances that had xG data. Can be lower than appearances. - `data.shooting.total_shots` (integer): All shots. - `data.shooting.shots_on_target` (integer): Shots on target. - `data.shooting.shots_off_target` (integer): Shots that missed the target. - `data.passing.total_passes` (integer): Passes attempted. - `data.passing.accurate_passes` (integer): Passes that reached a team-mate. - `data.passing.inaccurate_passes` (integer): Passes that didn't. - `data.passing.pass_accuracy` (number): Accurate passes as a percentage of all passes. - `data.passing.key_passes` (integer): Passes that led to a shot. - `data.passing.accurate_crosses` (integer): Crosses that found a team-mate. - `data.passing.accurate_crosses_percentage` (number): Accurate crosses as a percentage. - `data.passing.accurate_own_half_passes` (integer): Accurate passes in the player's own half. - `data.passing.accurate_opposition_half_passes` (integer): Accurate passes in the opponent's half. - `data.passing.accurate_final_third_passes` (integer): Accurate passes in the final third. - `data.defending.tackles` (integer): Tackles made. - `data.defending.interceptions` (integer): Passes intercepted. - `data.duels.ground_duels_won` (integer): Ground duels won. - `data.duels.ground_duels_won_percentage` (number|null): Share of ground duels won, as a percentage. Null when not recorded. - `data.duels.aerial_duels_won` (integer): Aerial duels won. - `data.duels.aerial_duels_won_percentage` (number|null): Share of aerial duels won, as a percentage. Null when not recorded. - `data.duels.total_duels_won` (integer): All duels won. - `data.duels.total_duels_won_percentage` (number|null): Share of all duels won, as a percentage. Null when not recorded. - `data.duels.successful_dribbles` (integer): Dribbles that beat an opponent. - `data.duels.successful_dribbles_percentage` (number|null): Share of dribbles that succeeded, as a percentage. Null when not recorded. - `data.discipline.yellow_cards` (integer): Yellow cards. - `data.discipline.red_cards` (integer): All red cards, including second yellows. - `data.discipline.direct_red_cards` (integer): Straight red cards. - `data.discipline.penalty_won` (integer): Penalties the player earned. - `data.discipline.penalty_conceded` (integer): Penalties the player gave away. Example: curl "https://api.soccer-data.co/v1/players/player_ul3vc6om97u5/stats?season_id=season_98l95mxugqs9" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /players/{player_id}/injuries-suspensions Player absences A player's injuries and suspensions. By default you get only what keeps them out right now; pass current=false to see their history too. Use it to flag unavailable players before a match or on a profile page. - With current=false, past records come back as well. Check active on each record to tell them apart. - A suspension only counts in its own competition. A player banned in the league can still be free to play in a cup. - A ban reported twice appears only once. - International call-ups listed under an international competition are left out. - expected_return is a best guess, not a fixed date. - To check a whole squad in one request, use the team absences endpoint instead. Query parameters: - `current` (boolean, default true): true returns only injuries and suspensions in effect now. false returns every record, past ones included. Response fields: - `data.injuries[]` (array): The player's injuries. - `data.injuries[].player_id` (string): The injured player. - `data.injuries[].status` (string|null): How serious it is, for example out or day_to_day. - `data.injuries[].reason` (string|null): The kind of injury, as a code such as leg_injury. - `data.injuries[].start_date` (string|null): When the injury started, as an ISO 8601 date-time. - `data.injuries[].expected_return` (string|null): Estimated return date, as an ISO 8601 date-time. Null when there's no estimate or the absence is open-ended. - `data.injuries[].active` (boolean): True while the injury still keeps the player out. False for past injuries. - `data.suspensions[]` (array): The player's suspensions. - `data.suspensions[].player_id` (string): The suspended player. - `data.suspensions[].reason` (string|null): Why the player is out, as a code such as red_card_suspension. national_team means the player is away with their country and misses club games; it isn't a ban. - `data.suspensions[].matches` (integer|null): How many matches the ban lasts. - `data.suspensions[].competition` (object|null): The competition the ban applies to. - `data.suspensions[].competition.id` (string): The competition's id. - `data.suspensions[].competition.name` (string): The competition's name. - `data.suspensions[].start_date` (string|null): When the suspension starts, as an ISO 8601 date-time. - `data.suspensions[].end_date` (string|null): When the suspension ends, as an ISO 8601 date-time. - `data.suspensions[].active` (boolean): True while the suspension is in force. False for past suspensions. Example: curl "https://api.soccer-data.co/v1/players/player_rzw8un0kyit8/injuries-suspensions" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /players/{player_id}/competitions/{competition_id}/seasons/{season_id}/heatmap Player season heatmap Where a player touched the ball across a whole competition season, added up into cells on the pitch. Use it to draw a season heatmap that shows the areas a player works in most. - Every match the player played in that competition season goes into one map. For tournaments, group and knockout games are combined. - x and y are positions on the pitch as percentages from 0 to 100. - Returns 404 NOT_FOUND when there's no heatmap, for example for a season that hasn't been played yet or a competition without positional data. - The season has to belong to the competition in the path. Otherwise you get 400 SEASON_COMPETITION_MISMATCH. Response fields: - `data.player.id` (string): The player's id. - `data.player.name` (string): The player's name. - `data.competition.id` (string): The competition's id. - `data.competition.name` (string): The competition's name. - `data.season.id` (string): The season's id. - `data.season.name` (string): The season's name. - `data.points[]` (array): One entry per pitch cell where the player had the ball. - `data.points[].x` (integer): The cell's x coordinate, as a percentage of the pitch from 0 to 100. - `data.points[].y` (integer): The cell's y coordinate, as a percentage of the pitch from 0 to 100. - `data.points[].count` (integer): Touches in that cell over the season. Always at least 1. Example: curl "https://api.soccer-data.co/v1/players/player_zm7yzjr75yjn/competitions/competition_7vpwx91c7j7h/seasons/season_st2tyewgtq2j/heatmap" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/odds Pre-match odds Bookmaker prices for one match before kickoff, one entry per bookmaker. Every selection shows the first price we saw and the latest one, so you can compare bookmakers or follow how a line moved. Markets run from the result and goals to corners, cards, shots and penalties. Requires the Pro or Business plan. - Needs a paid plan (Pro or Business). The Free plan gets 403 PLAN_UPGRADE_REQUIRED. - Check odds_available on the match before calling. A match without odds returns 404 NOT_FOUND, and postponed or cancelled matches never have any. - Works for upcoming and finished matches. Older matches mostly have Bet365 prices only, often with opening set to null. - If the match has odds but none from the bookmakers you asked for, you get 200 with an empty bookmakers list. - A market only appears when that bookmaker prices it. Shots, throw-ins, goal kicks, tackles and offsides are mostly offered on bigger fixtures, and to_qualify only appears for knockout ties. - Prices are decimal odds sent as strings, such as "1.380". Convert them yourself if you need fractional or American odds. Query parameters: - `bookmaker` (string, one of bet365, paddy-power, betmgm-uk, pinnacle, betfair-exchange): One or more bookmakers, comma-separated, such as bet365,pinnacle. Leave it out to get every bookmaker. An unknown slug returns 400 BAD_REQUEST. Response fields: - `data.match_id` (string): The match these prices are for. - `data.bookmakers[]` (array): One entry per bookmaker with prices for this match. - `data.bookmakers[].bookmaker` (string): Bookmaker name: Bet365, Paddy Power, BetMGM UK, Pinnacle or Betfair Exchange. - `data.bookmakers[].markets` (object): This bookmaker's markets, keyed by market name. Markets it doesn't price are left out. Each selection below is a price object (see {price} fields). - `data.bookmakers[].markets.{market}.{home|draw|away}` (object): Three-way result markets: match_odds, first_half_result and second_half_result. - `data.bookmakers[].markets.{market}.{home|away}` (object): Two-way markets: draw_no_bet, to_qualify, to_score_a_penalty and to_miss_a_penalty. - `data.bookmakers[].markets.{market}.{home_draw|home_away|draw_away}` (object): Double chance markets: double_chance and half_time_double_chance. - `data.bookmakers[].markets.{market}.{yes|no}` (object): Yes or no markets: btts, btts_first_half, btts_second_half and a_penalty_in_match. - `data.bookmakers[].markets.first_team_to_score.{home|away|none}` (object): Which side scores first. none means no goals. - `data.bookmakers[].markets.{market}.{line}.{over|under}` (object): Match over/under markets, keyed by line such as "2.5": total_goals, first_half_total_goals, match_corners, total_cards, match_shots, match_shots_on_target, first_half_match_shots, second_half_match_shots, first_half_match_shots_on_target, second_half_match_shots_on_target, match_throw_ins, first_half_match_throw_ins, second_half_match_throw_ins, match_goal_kicks, first_half_match_goal_kicks, second_half_match_goal_kicks, match_tackles and match_offsides. first_half_total_goals is mostly .5 lines, though some bookmakers add whole and quarter lines. - `data.bookmakers[].markets.{market}.{home|away}.{line}.{over|under}` (object): Team over/under markets, split by side and then keyed by line such as "1.5": team_total_goals, team_corners, team_shots, team_shots_on_target, first_half_team_shots, second_half_team_shots, first_half_team_shots_on_target, second_half_team_shots_on_target, team_throw_ins, first_half_team_throw_ins, second_half_team_throw_ins, team_goal_kicks, first_half_team_goal_kicks, second_half_team_goal_kicks, team_tackles and team_offsides. - `data.bookmakers[].markets.asian_handicap.{home|away}.{line}` (object): Asian handicap price for each side, keyed by that side's handicap, such as "-1.0" or "+0.5". - `data.bookmakers[].markets.handicap_result.{line}.{home|draw|away}` (object): Three-way handicap, keyed by the home team's handicap such as "-1" or "+2". - `data.bookmakers[].markets.total_goals_btts.{line}.{over_yes|over_no|under_yes|under_no}` (object): Total goals combined with both teams to score, keyed by goals line. - `data.bookmakers[].markets.correct_score.{score}` (object): Exact score, keyed as home-away such as "2-1". - `{price}.opening` (string|null): Decimal price when the bookmaker first listed this selection. Null when no opening price was recorded. - `{price}.last_seen` (string): The latest decimal price. For Betfair Exchange it matches the best back price. - `{price}.available_to_back[]` (array): Betfair Exchange only: up to 3 price levels you can back at, best first. An empty list means no unmatched offers right now. Missing for the other bookmakers. - `{price}.available_to_back[].price` (number): Decimal price at this level. - `{price}.available_to_back[].size` (number): Money available at this price. - `{price}.available_to_lay[]` (array): Betfair Exchange only: up to 3 price levels you can lay at, best first. Same rules as available_to_back. - `{price}.available_to_lay[].price` (number): Decimal price at this level. - `{price}.available_to_lay[].size` (number): Money available at this price. Example: curl "https://api.soccer-data.co/v1/matches/match_yoeml0e6ae37/odds" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /matches/{match_id}/odds/live Live odds In-play prices for a match that's under way, one entry per bookmaker quoting it. Poll it during the match to follow how the markets move as the game unfolds. Requires the Pro or Business plan. - Needs a paid plan (Pro or Business). The Free plan gets 403 PLAN_UPGRADE_REQUIRED. - Only useful while the match is live. Check live_odds_available on the match first. When there are no in-play prices, including after full time, you get 404 NOT_FOUND. - Prices come from Bet365, Paddy Power, BetMGM UK and Betfair Exchange, in that order. Pinnacle has no in-play prices here. - Bookmakers suspend and reopen markets during play, so a market or a bookmaker can drop out of one poll and be back in the next. - Answers are normally at most 15 seconds old, so polling more often than that returns the same prices. If a fresh answer can't be fetched, you may get an older copy marked X-Cache: STALE instead: see [When an answer is stale](/docs/concepts/rate-limits#when-an-answer-is-stale). - Prices are decimal odds sent as strings, such as "2.000". Response fields: - `data.match_id` (string): The match these prices are for. - `data.bookmakers[]` (array): One entry per bookmaker quoting in-play prices right now. - `data.bookmakers[].bookmaker` (string): Bookmaker name: Bet365, Paddy Power, BetMGM UK or Betfair Exchange. - `data.bookmakers[].markets` (object): Markets this bookmaker is quoting now, keyed by market name. Each selection below is a price object (see {price} fields). - `data.bookmakers[].markets.{market}.{home|draw|away}` (object): Three-way result markets: match_odds, first_half_result and second_half_result. - `data.bookmakers[].markets.{market}.{home|away}` (object): Two-way markets: draw_no_bet and to_qualify. - `data.bookmakers[].markets.double_chance.{home_draw|home_away|draw_away}` (object): Double chance. - `data.bookmakers[].markets.{market}.{yes|no}` (object): Both teams to score markets: btts, btts_first_half and btts_second_half. - `data.bookmakers[].markets.first_team_to_score.{home|away|none}` (object): Which side scores first. none means no goals. - `data.bookmakers[].markets.{market}.{line}.{over|under}` (object): Match over/under markets, keyed by line such as "2.5": total_goals, match_corners and total_cards. - `data.bookmakers[].markets.{market}.{home|away}.{line}.{over|under}` (object): Team over/under markets, split by side and then keyed by line: team_total_goals and team_corners. - `data.bookmakers[].markets.asian_handicap.{home|away}.{line}` (object): Asian handicap price for each side, keyed by that side's handicap, such as "-0.5" or "+0.5". - `data.bookmakers[].markets.handicap_result.{line}.{home|draw|away}` (object): Three-way handicap, keyed by the home team's handicap such as "-1" or "+2". - `data.bookmakers[].markets.correct_score.{score}` (object): Exact score, keyed as home-away such as "2-1". - `{price}.live` (string): The current decimal price. For Betfair Exchange it matches the best back price. - `{price}.available_to_back[]` (array): Betfair Exchange only: up to 3 price levels you can back at, best first. An empty list means no unmatched offers right now. Missing for the other bookmakers. - `{price}.available_to_back[].price` (number): Decimal price at this level. - `{price}.available_to_back[].size` (number): Money available at this price. - `{price}.available_to_lay[]` (array): Betfair Exchange only: up to 3 price levels you can lay at, best first. Same rules as available_to_back. - `{price}.available_to_lay[].price` (number): Decimal price at this level. - `{price}.available_to_lay[].size` (number): Money available at this price. Example: curl "https://api.soccer-data.co/v1/matches/match_yoeml0e6ae37/odds/live" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /coverage/leagues Data coverage per competition Shows, for each competition, which kinds of data it has, which seasons are loaded and how many finished matches there are. Check it before you build a feature on a competition, or filter by data type to find every competition that has xG or lineups. - Match counts only include finished matches. - seasons.first and seasons.last are not guaranteed to be the oldest and newest seasons. For the full season list, use /coverage/leagues/{competition_id}. - odds is an older name for closing_odds, kept so existing code keeps working. opening_odds is what fills the opening price on the match odds endpoint; closing_odds fills last_seen. - New data type keys can appear over time. Skip any you don't recognise. - Coverage changes slowly, so there's no need to call this often. Query parameters: - `data_type` (string, one of fixtures, team_stats, xg, odds, opening_odds, closing_odds, lineups, player_stats, standings): Only competitions that have this kind of data. - `search` (string): Part of the competition name, case-insensitive. - `page` (integer, default 1): Which page to return, starting at 1. meta.total_pages tells you how many there are. - `per_page` (integer, default 100): How many competitions per page, up to 200. Response fields: - `data[].id` (string): The competition's id. - `data[].name` (string): The competition's name. - `data[].country` (string): Country name. Can be null. - `data[].country_code` (string): Country code. Can be null. - `data[].seasons.count` (integer): How many seasons have matches loaded. - `data[].seasons.first` (object): One end of the loaded range, usually an early season but not always the oldest. Can be null. - `data[].seasons.first.id` (string): That season's id. - `data[].seasons.first.year` (string): That season's label, such as 14/15. Can be null. - `data[].seasons.last` (object): The other end of the loaded range, usually a recent season but not always the newest. Can be null. - `data[].seasons.last.id` (string): That season's id. - `data[].seasons.last.year` (string): That season's label, such as 26/27. Can be null. - `data[].data_types.{type}.available` (boolean): Whether the competition has this kind of data. There is one key per type: fixtures, team_stats, xg, odds, opening_odds, closing_odds, lineups, player_stats and standings. - `data[].latest_season` (object): A summary of the newest season. Can be null. - `data[].latest_season.id` (string): That season's id. - `data[].latest_season.year` (string): That season's label. Can be null. - `data[].latest_season.status` (string): complete, in_progress or not_loaded, judged from the matches loaded so far. - `data[].latest_season.finished_events` (integer): Matches already played that season. - `data[].latest_season.total_events` (integer): All matches in that season, played or not. - `data[].total_finished_events` (integer): Finished matches across every season. - `meta.page` (integer): The page you're on. - `meta.per_page` (integer): Results per page. - `meta.total` (integer): Matching competitions across all pages. - `meta.total_pages` (integer): How many pages there are. Example: curl "https://api.soccer-data.co/v1/coverage/leagues" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /coverage/leagues/{competition_id} Season coverage for a competition Breaks one competition's coverage down season by season: how many matches are finished and what share of them has each kind of data. Use it to see exactly how far back xG, lineups or odds go before you rely on them. - Every season we know of is listed, including ones with no matches loaded. Check status: not_loaded means there are no matches for that season yet. - Seasons are not sorted by date. - odds is an older name for closing_odds, kept so existing code keeps working. opening_odds is the first price seen for each selection; closing_odds is the last one before kick-off. - New data type keys can appear over time. Skip any you don't recognise. Response fields: - `data.id` (string): The competition's id. - `data.name` (string): The competition's name. - `data.country` (string): Country name. Can be null. - `data.country_code` (string): Country code. Can be null. - `data.total_seasons` (integer): Every season known for the competition, loaded or not. - `data.loaded_seasons` (integer): Seasons with at least one match loaded. Same number as seasons.count in /coverage/leagues. - `data.total_finished_events` (integer): Finished matches across the whole competition. - `data.seasons[].id` (string): The season's id. - `data.seasons[].name` (string): The season's display name. - `data.seasons[].year` (string): Short label, such as 25/26. Can be null. - `data.seasons[].status` (string): complete (every loaded match is played), in_progress (some are still to come) or not_loaded (no matches loaded). Based only on matches loaded so far; more values may be added. - `data.seasons[].events.finished` (integer): Matches already played that season. - `data.seasons[].events.total` (integer): All matches in that season, played or not. - `data.seasons[].data_types.{type}.available` (boolean): Whether the season has this kind of data. Keys: fixtures, team_stats, xg, odds, opening_odds, closing_odds, lineups, player_stats and standings. - `data.seasons[].data_types.{type}.covered_events` (integer): Finished matches that have this kind of data. Not every type includes it; standings, for example, only has available. - `data.seasons[].data_types.{type}.coverage_pct` (number): covered_events as a percentage of finished matches, to one decimal place. Not every type includes it. Example: curl "https://api.soccer-data.co/v1/coverage/leagues/competition_705uv6jm8myn" -H "X-Api-Key: $SOCCER_DATA_KEY" ### GET /coverage/summary Coverage at a glance Returns headline totals for everything we cover: how many competitions, seasons and finished matches there are, and how many seasons are complete. Handy for a status page or a quick sanity check. - For a breakdown by competition, use /coverage/leagues. Response fields: - `data.leagues` (integer): Number of competitions. - `data.seasons` (integer): Number of seasons across all competitions. - `data.finished_events` (integer): Number of finished matches. - `data.seasons_by_status.complete` (integer): Seasons where every loaded match has been played. - `data.seasons_by_status.in_progress` (integer): Seasons with matches still to come. - `data.seasons_by_status.not_loaded` (integer): Seasons with no matches loaded. - `data.pct_complete` (number): Complete seasons as a percentage of all seasons, to one decimal place. Example: curl "https://api.soccer-data.co/v1/coverage/summary" -H "X-Api-Key: $SOCCER_DATA_KEY"