# footballsoccerapi.com — complete reference

> Football and soccer data: 710,111 matches across 773 competitions and 158 countries, back to 2012.
>
> Generated from the live API definition on 24 September 2026.
> Every endpoint, parameter and sample below is produced from the same source
> the API itself runs on, so this document cannot disagree with it.
>
> Canonical copy: https://footballsoccerapi.com/documentation/manual.md

---

## At a glance

| | |
| --- | --- |
| Base URL | `https://api.footballsoccerapi.com` |
| Authentication | `X-API-Key: <your key>` request header |
| Format | JSON. Every response carries `data` and `meta`. |
| Endpoints | 64 |
| Matches | 710,111 |
| With a kickoff price | 384,227 |
| Free key | free, 50 calls a day, 60/min |
| Archive | $49/month, 250,000 calls a month, 120/min |
| Live | $99/month, 600,000 calls a month, 180/min |
| Support | https://footballsoccerapi.com/support |

Ids in this API are opaque strings with a type prefix — `mt_` for a match,
`lg_` for a competition, `tm_` for a club. They are stable, and the numeric
form is not part of the contract.

---

## Guides

The reasoning behind the data: how it is collected, what is missing, and the
conventions that are easy to get wrong.

## Getting started

*From nothing to a priced match in four calls.*  
`https://footballsoccerapi.com/documentation/getting-started`

### 1. Get a key

Sign in with Google at [/login](https://footballsoccerapi.com/login). The account is created and the key
issued in the same step — no card, no trial clock. The free key is real data on every
competition in the archive, for yesterday, today and the fixtures ahead, at 50 calls a day.

### 2. Make a call

Your key goes in the `X-API-Key` header. There is nothing else to send.

```
curl "https://api.footballsoccerapi.com/v1/countries" \
  -H "X-API-Key: $FSAPI_KEY"
```

### 3. Understand the shape

Every response has two top-level keys. `data` is what you asked for — an
object for a single resource, an array for a list. `meta` always carries
`data_as_of` so you know how old the answer is, and `request_id` so you
can quote an exact call in a support thread. Lists also carry `count` and
`next_cursor`.

### 4. Go deeper

The workhorse is [`/v1/matches`](https://footballsoccerapi.com/documentation/matches) — every
filter composes, so you can ask for finished matches in one competition and season that carry
an exchange price. From a match, [`/v1/matches/{id}/odds`](https://footballsoccerapi.com/documentation/match-odds)
gives you the market.

Read [pagination](https://footballsoccerapi.com/documentation/pagination) before you pull anything large, and
[errors](https://footballsoccerapi.com/documentation/errors) before you ship.

---

## Authentication

*One header. No tokens to refresh, no OAuth dance.*  
`https://footballsoccerapi.com/documentation/authentication`

### The header

Send your key as `X-API-Key` on every request. There are no bearer tokens to
refresh and no signing to implement.

```
curl "https://api.footballsoccerapi.com/v1/matches?limit=5" \
  -H "X-API-Key: fsa_live_8f3c2b91d4e7a06c5b8"
```

### Keep it server-side

**Never put your key in a browser or a mobile app.** Anything shipped to a device can be
read off it. Call the API from your own server and pass the result on.

Calls made with your key count against your limits and count as made by you.

### Rotating

If a key leaks, rotate it from [your account](https://footballsoccerapi.com/account). A new key is issued
immediately and the old one stops working the moment you confirm — anything still using
it starts getting `401`s, so update your code first.

### What failure looks like

A missing or unknown key returns `401`. A key that is valid but on a plan that
does not reach the endpoint returns `403` naming the plan it needs. Neither is ever
a bare refusal — see [errors](https://footballsoccerapi.com/documentation/errors).

---

## Pagination

*Page numbers for pagers, cursors for exports. Same endpoints, either style.*  
`https://footballsoccerapi.com/documentation/pagination`

Walking a set you already have the ids for? That is a different job
— see [batch requests](https://footballsoccerapi.com/documentation/batch-requests), which does
it in one request rather than one per id.

### Two ways, one endpoint

Every list endpoint pages both ways, and you can switch between them mid-stream.
**Page numbers** suit a UI pager or a bounded query, because the response tells you how
many there are in total. **Cursors** suit exports and catch-up, because a deep page stays
fast and the boundary cannot move underneath you.

If you send both, `page` wins.

### Page numbers

Ask for a page with `page`, which is 1-based.

```
curl "https://api.footballsoccerapi.com/v1/matches?league_id=39&season=2024&page=2&limit=100" \
  -H "X-API-Key: $FSAPI_KEY"
```

```
{
  "meta": {
    "count": 100,
    "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
    "page": 2,
    "per_page": 100,
    "total": 380,
    "total_pages": 4,
    "next_page": 3,
    "prev_page": 1
  },
  "data": [ ... ]
}
```

You do not have to ask for page 1 explicitly. **Any first call — one with no cursor —
already carries the totals**, so you learn the size of the result set from the same request
that gives you the first rows.

Page numbers are the wrong tool for very large exports. A deep offset gets progressively
slower, and if new matches land while you iterate, a page boundary shifts and you can see a
row twice or miss one. For those, use a cursor.

### Cursors

A cursor points at a position in the sort order rather than counting from the start.
Page 10,000 costs the same as page 1, and rows arriving underneath the query cannot move the
boundary. Follow `meta.next_cursor` until it is absent.

```
curl "https://api.footballsoccerapi.com/v1/matches?has_prices=true&limit=1000" \
  -H "X-API-Key: $FSAPI_KEY"
# -> meta.next_cursor: "MTcyOTMzNzQwMDoxMjA4MDk5"

curl "https://api.footballsoccerapi.com/v1/matches?has_prices=true&limit=1000&cursor=MTcyOTMzNzQwMDoxMjA4MDk5" \
  -H "X-API-Key: $FSAPI_KEY"
```

Cursors are opaque. Pass `meta.next_cursor` back unchanged and do not build or
parse one yourself — the format is ours to change. When it is absent you have reached
the end.

A continuation stays lean: it returns `count` and `next_cursor` only.
You already learned the totals from the first call, and counting them again on every page of a
300-call export is work nobody asked for.

```
let cursor = null;
do {
  const url = "https://api.footballsoccerapi.com/v1/matches?has_prices=true&limit=1000"
            + (cursor ? "&cursor=" + cursor : "");
  const r = await fetch(url, { headers: { "X-API-Key": key } });
  const { data, meta } = await r.json();
  handle(data);
  cursor = meta.next_cursor;
} while (cursor);
```

### Which to reach for

**Use `page`** for UI pagers, for jumping to a specific page, and for small or
date-bounded queries where you want to show someone how many results there are.

**Use `cursor`** for bulk exports, full-archive pulls, training sets, and
catching up after a disconnect.

You are not locked in. A page-numbered response also carries `next_cursor`, so
you can start on page 1 and switch to cursors once you are past the first page.

### Page size

`limit` takes 1–1000 and defaults to 100. It applies to both styles.

The whole priced archive is around 384,227 matches, which at
1,000 a page is roughly 300 calls — comfortably inside the Archive monthly allowance.

---

## Rate limits

*Two ceilings, printed on every response. Going over is never a bill.*  
`https://footballsoccerapi.com/documentation/rate-limits`

### The limits

Every plan has a per-minute ceiling and a longer allowance. The per-minute limit stops a
runaway loop; the allowance covers normal use.

**Free key** — 60 a minute, 50 a day, resetting at 00:00 UTC.

**Archive** — 120 a minute, 250,000 a month.

**Live** — 180 a minute, 600,000 a month.

### Knowing where you stand

Every response carries your position, so you never have to guess or count.

```
x-ratelimit-limit: 120
x-ratelimit-remaining: 118
x-ratelimit-reset: 41
```

`reset` is seconds until the window rolls.

### Going over

You get a `429` with `retry-after` in seconds. **There is no overage
charge, no automatic upgrade and no surprise invoice.** The worst case is that you wait.

```
HTTP 429 Too Many Requests
retry-after: 41
```

Back off for the number of seconds given rather than retrying immediately — a tight
retry loop just burns the next window too.

### Fair use

Holding several free accounts to multiply the allowance is a breach of the
[acceptable use policy](https://footballsoccerapi.com/acceptable-use). If you need more than a plan allows, ask
— an arrangement is nearly always cheaper than the effort of evading the limit.

---

## Errors

*Every refusal tells you what to do about it.*  
`https://footballsoccerapi.com/documentation/errors`

### The shape

Errors use the same envelope as everything else. `error.code` is stable and safe
to branch on; `error.message` is for humans and may be reworded.

```
{
  "error": {
    "code": "plan_required",
    "message": "Half-time prices need the Archive plan.",
    "your_plan": "free",
    "needs_plan": "archive",
    "upgrade_url": "/pricing?from=odds"
  },
  "meta": { "request_id": "01J9QW3C7M4KX2VB" }
}
```

### The codes

`400 invalid_parameter` — a filter was the wrong type or outside its range.
The message names the parameter.

`401 missing_key` / `401 unknown_key` — no `X-API-Key`
header, or one we do not recognise. A rotated key gives this.

`403 plan_required` — a valid key on a plan that does not reach this
endpoint. Always names the plan needed.

`403 outside_plan_window`, a free key asking for a match or a date older than its
window. The error states the window and names the plan that carries older matches.

`404 not_found` — no such match, club or competition.

`429 rate_limited` — see [rate limits](https://footballsoccerapi.com/documentation/rate-limits).

`500 server_error` — ours, not yours. Quote the `request_id`.

### request_id

Every response carries one, success or failure. Quote it in a
[support thread](https://footballsoccerapi.com/#s08) and we can find the exact call you made rather than asking
you to describe it.

### What to retry

Retry `429` after `retry-after`, and `500` with a backoff.
Never retry `400`, `401`, `403` or `404` —
the answer will not change.

---

## Timezones &amp; dates

*Kickoff in UTC and in local time, because both matter.*  
`https://footballsoccerapi.com/documentation/timezones`

### Two clocks

Every match carries its kickoff twice. `kickoff_utc` is a Unix timestamp and is
the authority — use it for ordering, windows and anything arithmetic.
`kickoff_local_date` and `kickoff_local_time` are what the clock said
where the match was played.

Tottenham against West Ham on 19 October 2024 kicked off at `1729337400`, which is
11:30 UTC. Locally it was a 12:30 kickoff. Both are true, and a fixture list that shows 11:30 to
a London reader is wrong.

### Date filters

`date_from` and `date_to` take ISO `YYYY-MM-DD` and are
evaluated in UTC, inclusive at both ends. `kickoff_date` on a match is the UTC date,
so filtering and displaying agree.

### What "today" means

For `/v1/fixtures/today` and `/v1/results/today`, today is the UTC day.
A late South American kickoff can therefore land on the following UTC date — if that
matters to you, use `date_from` and `date_to` with your own boundaries
rather than the convenience endpoint.

### Seasons

A season is identified by the year it started. The 2024/25 English season is
`2024`. Leagues that run within a single calendar year use that year. This keeps one
integer meaningful across every competition, including the ones that run March to November.

---

## Historical data

*What is in the archive, and how to read the coverage figures.*  
`https://footballsoccerapi.com/documentation/historical-data`

### Scope

710,111 matches across 958
competitions in 158 countries, over ten seasons. Every plan
reaches every competition. Plans differ by how far back you can read and whether the prices
come with it, never by coverage; the free key reads yesterday, today and the fixtures ahead.

### What a row carries

Beyond the scoreline: both clubs’ league position at kickoff, how far the away side
travelled, the ground with its coordinates and city, the half-time score, added time, and
whether the kickoff was moved. Then the exchange market recorded at kickoff.

### Coverage is published, not implied

Not every field exists on every match, and we would rather you had the number than found
out later. Every competition and every season carries a coverage block giving the fill rate
field by field — for the rows you are actually asking about, rather than an average over
everything.

That is the figure to build against. A field can be near complete in one competition and
absent from another, so a single archive-wide percentage would be the wrong number in both
directions. Read the coverage on what you are querying and you will know exactly what you are
getting.

Every field is **in the schema already**. A field that is empty on a match comes back as
`null` rather than being missing from the response, so nothing on your side has to
change as coverage grows.

### Ids do not move

A `match_id` or `team_id` you store today resolves to the same thing
next year. Ids come from the source rather than being generated at import, so a rebuild maps
everything back to the same numbers. That is a promise, not an implementation detail — it
is in the [terms](https://footballsoccerapi.com/terms).

### Freshness

The archive rebuilds nightly. Every response carries `data_as_of`, so you always
know how old the answer is rather than assuming.

---

## Live odds

*What the market snapshots are, and what Live adds.*  
`https://footballsoccerapi.com/documentation/live-odds`

### The snapshots

The archive holds prices from more than one kind of source, and rather than flatten that away we publish which is which. Every priced match carries `kickoff_book_pct`: the three prices as probabilities, added up.

That one number tells you what you are looking at. A three-way **exchange** book runs near 102%, because an exchange takes commission rather than building in a margin, so the implied probability is close to what the crowd actually thought. A single **bookmaker** runs 105–108%, with the margin in the price. A **best of market** price, taken as the best available across many bookmakers, can even land slightly under 100%.

Filter on it rather than trusting a label. If your work needs prices with no margin in them, take `kickoff_book_pct` between 1.00 and 1.03. If you only need a reasonable price, take everything. Anything far below 1.00 is not a real book and is left out of our own aggregates.

Two snapshots are recorded on a match. **Kickoff** is the market as the game started.
**Half-time** is the same market at the interval — the drift between them is the
information.

### Implied probability and overround

Implied probability is `1 / price`. Summed across all three outcomes you get the book percentage, stored on every priced match as `kickoff_book_pct` so you never have to recompute it. [`/v1/odds/overround`](https://footballsoccerapi.com/documentation/odds-overround) gives it by competition and season, and it is a decent
proxy for how seriously a market was taken.

### What Live adds

Archive gives you settled history. **Live** removes the date guard entirely and adds
fixtures ahead, in-play scores and events, and current prices, plus a WebSocket stream and
webhooks so you stop polling.

If you are modelling on history, Archive is the plan. Live is for anything that has to react
while a match is happening.

### A word of care

These are records of what a market showed at a moment in time. They are historical facts, not
predictions, and nothing here is betting advice. Where a figure is derived rather than measured
— a backtest, a calibration — the page it appears on says so.

---

## The whole API in one file

*OpenAPI, Postman and MCP, all generated from the same registry.*  
`https://footballsoccerapi.com/documentation/openapi`

### OpenAPI 3.1

The full surface as a spec you can generate a client from, in any language your toolchain
supports. Regenerated on every release, so it cannot describe an endpoint that no longer exists.

```
curl "https://api.footballsoccerapi.com/v1/openapi.json" -o fsapi.json
```

### Postman

A collection with every endpoint and example parameters filled in. Import it, paste your key
into the `api_key` variable once, and every request in the collection fires.

```
curl "https://api.footballsoccerapi.com/v1/postman.json" -o fsapi.postman_collection.json
```

Optional filters arrive switched off rather than silently narrowing your first call.

### MCP tools

The archive exposed as Model Context Protocol tools, so an assistant can query it without any
glue code. Point it at the endpoint with your key and ask questions in words — how a club
does at home after leading at half-time, and what the market paid at kickoff in those matches.

```
https://api.footballsoccerapi.com/mcp
# header: X-API-Key: your key
```

It is a curated set rather than all 62 endpoints: finding a
competition, finding a club, matches, odds, standings, form, head-to-head, previews, league
stats, price calibration and the backtest. An assistant handed fifty near-identical tools
chooses badly, so it gets the ones worth asking a question about.

Open the endpoint in a browser and it lists the tools rather than returning a protocol
error.

### Why they cannot drift

All three are produced from one registry — the same file that drives the router, these
documentation pages and the plan badges. There is no second list to keep in step, which is the
usual reason an API spec ends up lying.

---

## Batch requests

*One call for fifty matches instead of fifty calls.*  
`https://footballsoccerapi.com/documentation/batch-requests`

Following a set of matches — a weekend’s fixtures, an accumulator, the games one
club played last month — does not need one request each. Pass the ids together and take
one response.

```
curl "https://api.footballsoccerapi.com/v1/matches?ids=mt_0CXSZRJ-mt_087EP2A-mt_2A0Z362" \
  -H "X-API-Key: YOUR_KEY"
```

Up to **fifty ids** in one request, separated by a dash. The response is the ordinary match
list, so everything you already do with it still works.

### Why it is worth doing

Fifty separate requests mean fifty round trips, fifty connections and fifty chances for one
of them to fail. One request means one of each. The rate limit charge is the same either way
— see [limits](#limits) — so this is about speed and reliability rather
than about quota.

We learned this on our own side rather than in theory: switching one of our own collectors
from per-match to batched turned a day of fetching into fifteen minutes, for exactly the same
data.

### Filters still apply

Every other parameter composes with `ids`, which is useful more often than it
sounds. Ask for a set and keep only the ones that finished:

```
https://api.footballsoccerapi.com/v1/matches?ids=ID-ID-ID&status=finished
```

Or a set with a price on them:

```
https://api.footballsoccerapi.com/v1/matches?ids=ID-ID-ID&has_prices=true
```

### A bad id is refused, not dropped

Send an id that does not decode and the whole request fails with `400 invalid_id`,
naming the one at fault.

That is deliberate, and it is the opposite of what several APIs do. Silently returning the
forty-nine that worked gives you a response that looks complete and is not — you would
find that in production, weeks later, as a gap in something you had already shipped. Better to
fail on the request you can still fix.

Duplicates are fine and are collapsed. Sending the same id three times returns it once.

### When not to use it

`ids` is for a set you already know. For walking a competition, a season or the
whole archive, use [the cursor](https://footballsoccerapi.com/documentation/pagination) instead — it is
built for volume and does not ask you to know what you want before you ask for it.

The rule of thumb: if you are generating the id list from a previous response, you probably
want the cursor. If it came from your own database, `ids` is right.

### Limits

Fifty per request. Beyond that you would be paging, and the cursor does it better.

**Each id costs one call against your rate limit.** Asking for fifty matches in one
request costs fifty, the same as asking for them one at a time. We would rather say that
plainly than have you discover it from a `429`.

What batching saves is round trips, not quota. Fifty requests means fifty connections, fifty
lots of latency and fifty chances for one to fail halfway; one request means none of that. On a
slow connection or from a serverless function that is the difference between a page that loads
and one that times out.

Charging per record is deliberate. A limit that counted requests would measure how many
connections you opened rather than how much data you took, and would let a free key pull fifty
times its intended volume by batching. We would rather the limit meant something.

`meta.count` tells you how many came back, which will be fewer than you asked for
if some of those matches fall outside your plan’s window.

---

## The PHP client

*No dependencies, no Composer, and a one-file version.*  
`https://footballsoccerapi.com/documentation/php-client`

An official PHP client is published at
[github.com/FootballSoccerAPI/footballsoccerapi-php](https://github.com/FootballSoccerAPI/footballsoccerapi-php). It needs curl and json, which PHP already
has, and nothing else.

There are two versions in it, and the smaller one is the better place to start.

### The one-file version

Copy `fsapi-simple.php` next to your script. Three functions, no classes, nothing to
install.

```
require 'fsapi-simple.php';

$matches = fsapi_get('/v1/matches', [
    'country' => 'England',
    'status'  => 'finished',
    'limit'   => 5,
]);

foreach ($matches as $m) {
    printf("%s %d-%d %s\n",
        $m['home_team_name'], $m['home_goals'],
        $m['away_goals'], $m['away_team_name']);
}
```

`fsapi_get()` hands back the data. `fsapi_call()` hands back the data and
the meta. `fsapi_walk()` pages through a whole set for you. That is all of it.

### Your key, and the mistake everyone makes

`getenv()` takes the **name** of an environment variable, not the key itself.
Passing the key to it looks for a variable with that name, finds nothing, and you get told a key
is required while holding one.

```
// Wrong — looks for a variable called "fsa_live_..."
$api = new Client(getenv('fsa_live_af1f2c1b...'));

// Right — the key itself
$api = new Client('fsa_live_af1f2c1b...');

// Better — the key stays out of the file
$api = new Client(getenv('FSAPI_KEY'));
```

For the last one, set the variable when you run it:

```
FSAPI_KEY=fsa_live_... php your-script.php
```

Worth the extra step. A key written into a file ends up in a repository, a screenshot or a
support ticket eventually, and then it has to be rotated.

### The full client

Same data, more handled for you: typed exceptions, cursor walking, and batching that chunks a
long list rather than refusing it. Clone the repository and require the autoloader — that is
the installation. Composer works if you use it, but nothing needs it.

```
require 'src/autoload.php';

use FootballSoccerApi\Client;

$api = new Client(getenv('FSAPI_KEY'));
$res = $api->matches(['season' => 2024]);
```

### What the exceptions tell you

A refusal names the plan it needs rather than leaving you a bare 403, and a rate limit carries
the wait from the response rather than a guessed backoff.

```
try {
    $api->live();
} catch (PlanRequiredException $e) {
    printf("Needs %s, you have %s\n", $e->needsPlan(), $e->yourPlan());
} catch (RateLimitException $e) {
    sleep($e->retryAfter);
}
```

A response that is not JSON throws a `TransportException` saying so. That means
something between you and the API answered — a proxy, or a challenge page — and knowing
which layer failed saves an hour of debugging the wrong one.

### The examples

Four of them, each runnable on a free key. The last works out whether home advantage differs by
country in about forty lines, which is a fair test of whether the archive is worth your time.

```
export FSAPI_KEY=your_key

php examples/simple.php
php examples/quickstart.php
php examples/walk-a-season.php
php examples/home-advantage.php
```

There is a [Python client](https://footballsoccerapi.com/documentation/python-client) too, with the same
three functions under the same names, so moving between them is not a relearn.

MIT licensed. If something is wrong or missing, an issue on the repository or a
[ticket](https://footballsoccerapi.com/support) both reach us.

---

## The Python client

*Standard library only, and it goes straight into pandas.*  
`https://footballsoccerapi.com/documentation/python-client`

An official Python client is published at
[github.com/FootballSoccerAPI/footballsoccerapi-python](https://github.com/FootballSoccerAPI/footballsoccerapi-python). Standard library only — no
requests, no httpx, nothing to keep in step with anything else.

That is deliberate. A data client that drags in a stack is a client that breaks when the stack
moves, and the people using this already have enough of that.

There are two versions in it, and the smaller one is the better place to start.

### The one-file version

Copy `fsapi_simple.py` next to your script or notebook. Three functions, no classes,
nothing to install.

```
from fsapi_simple import fsapi_get

matches = fsapi_get("/v1/matches",
                       country="England",
                       status="finished",
                       limit=5)

for m in matches:
    print(m["home_team_name"], m["home_goals"],
          m["away_goals"], m["away_team_name"])
```

`fsapi_get()` hands back the data. `fsapi_call()` hands back the data and
the meta. `fsapi_walk()` pages through a whole set for you. That is all of it, and the
names match the [PHP client](https://footballsoccerapi.com/documentation/php-client) exactly, so moving between them
is not a relearn.

### Straight into pandas

The client does not import pandas and does not depend on it. It does not need to — a
response is plain dictionaries, so it goes into a DataFrame with no adapter and no helper.

```
import pandas as pd
from footballsoccerapi import Client

api = Client()                 # reads FSAPI_KEY

rows = list(api.walk_matches(league_id="lg_24T9Z0G", season=2024))
df = pd.DataFrame(rows)

df["total_goals"] = df["home_goals"] + df["away_goals"]
df.groupby("league_name")["total_goals"].mean()
```

**Check coverage before trusting an average.** A field we do not hold comes back as null
rather than vanishing, so the column is always there and pandas reads it as `NaN`.
Counting those tells you what an average is actually over — prices sit at 53.7% of the
archive and the share varies a lot by competition.

```
df["kickoff_book_pct"].notna().sum()   # how many actually had a price
```

### Your key

Pass it directly, or leave it out and let the client read `FSAPI_KEY` from the
environment:

```
api = Client("fsa_live_...")   # directly
api = Client()                   # reads FSAPI_KEY
```

```
FSAPI_KEY=fsa_live_... python your-script.py
```

The environment version matters more in Python than most places, because notebooks get shared.
A key written into a cell ends up in a screenshot, a repository or a colleague’s copy
eventually, and then it has to be rotated.

### The package

Same data, more handled for you: typed exceptions, cursor walking, and batching that chunks a
long list rather than refusing it.

```
pip install git+https://github.com/FootballSoccerAPI/footballsoccerapi-python.git
```

```
from footballsoccerapi import Client

api = Client()
res = api.matches(season=2024)

res["data"]                 # the matches
res["meta"]["total"]        # how many matched
res["meta"]["data_as_of"]   # when the archive was rebuilt
```

The meta comes back with the data rather than being stripped, because it is where the API says
how old the archive is and how many rows there were — the part that stops a figure being
quoted without its base.

### What the exceptions tell you

```
from footballsoccerapi import PlanRequiredError, RateLimitError

try:
    api.live()
except PlanRequiredError as e:
    print(f"Needs {e.needs_plan}, you have {e.your_plan}")
except RateLimitError as e:
    time.sleep(e.retry_after)
```

Rate limits are retried once by default using the API’s own figure, because guessing a
backoff when the response tells you the answer is worse for both sides. A reply that is not JSON
raises `TransportError` saying so, which means something between you and the API
answered — a proxy, or a challenge page — and knowing which layer failed saves an
hour.

### Walking a season

```
for match in api.walk_matches(league_id="lg_24T9Z0G", season=2024):
    ...   # every match, memory flat
```

The cursor pages by sort position rather than offset, so the ten-thousandth page is as quick as
the first and a match arriving mid-walk cannot shift the boundary and make you skip a row.

### The examples

```
export FSAPI_KEY=your_key

python examples/simple.py
python examples/quickstart.py
python examples/walk_a_season.py
python examples/into_pandas.py
```

MIT licensed. If something is wrong or missing, an issue on the repository or a
[ticket](https://footballsoccerapi.com/support) both reach us.

---

## Changelog

*Every change, dated. v1 only ever gains fields.*  
`https://footballsoccerapi.com/documentation/changelog`

### The promise

Within v1 we **only ever add fields**. Adding one is routine and needs nothing from you.
Removing or renaming one is a changelog event with a deprecation window, announced before it
happens and never on the day.

Ids we issue are stable. What you store today still resolves later.

### Entries

#### 23 September 2026 · The free key now reads the recent days

The free key now reads yesterday, today and the fixtures ahead, across every league, at 50
calls a day. Until today it read the whole archive on a delay, at 500 calls a day. Today now
comes through with no delay at all, as matches settle, which the old key never had.

Why: that allowance was enough to run a product on without paying, which is not what a free
key is for. A free key is for building and testing. It should show every field and every shape
on real, current matches, and the recent days do that.

What moves to the paid plans: anything before yesterday is [Archive](https://footballsoccerapi.com/pricing).
The prices on the free key’s own days, and the in-play feed, come with Archive and Live.

What it means for your code: no field has been removed or renamed. On a free key the price
fields (`best_odds`, the kickoff and half-time prices, matched volume, overround and
the Betfair ids) come back as `null`, each with a note in `held_back`
naming the plan that carries it. Every free response carries `meta.free_key`, stating
what it reaches, the allowance and when it resets. A date wholly before the window is refused
with `403 outside_plan_window`, which states the window, rather than answered with an
empty list that reads as no football that day.

[/v1/results/yesterday](https://footballsoccerapi.com/documentation/results-yesterday) is new, on every plan:
yesterday as a whole UTC day, never sharing a match with today’s results.

Existing keys keep working and need nothing from you. They see the new window from today.

#### 8 September 2026 — Grounds, with capacity and surface

[/v1/grounds](https://footballsoccerapi.com/documentation/grounds) lists every ground we hold detail for, with
its city, capacity, playing surface, and how many matches in the archive were played there.
`/v1/matches` takes `surface` and `venue_id`, so
“finished matches on artificial turf” is now one request.

A match links to a ground through `venue_id`, matched on the ground’s own id
rather than its name — names agreed barely a third of the time, ids agree all of it.
Surface lives on the ground, not copied onto every match played there, so a stadium that
resurfaces is corrected in one place.

#### 8 September 2026 — Lineups and formations

[/v1/matches/{id}/lineups](https://footballsoccerapi.com/documentation/match-lineups) returns the formation,
the starting eleven with shirt numbers and grid positions, the substitutes and the coach for
each side. Grid positions are row and column counting out from the goalkeeper, so a shape can be
drawn without guessing.

`home_formation` and `away_formation` also come back on the match
itself, which is the field to filter and group by.

#### 8 September 2026 — Distance between two clubs

[/v1/teams/{id}/distance/{opponent_id}](https://footballsoccerapi.com/documentation/team-distance) gives the
distance between two grounds as the crow flies, in kilometres and miles. The same measure as
`away_travel_km` on a match, so a distance looked up here and one read off a fixture
agree.

It answers before a fixture exists, which is the point — weighing a cup draw or planning
a season means asking about a game that has not been arranged yet.

#### 8 September 2026 — Travel distances corrected

A club’s ground coordinates were being read from its most recent home fixture, and
fixtures added since the original archive carry no coordinates. Clubs that had played recently
were quietly losing coordinates they already had, and the travel distance with them.

Fixed, and `away_travel_km` went from 69.2% of the archive to 81.7% — about
85,000 matches that should always have had it. If you cached travel distances, they are worth
refetching.

#### 8 September 2026 — Several matches in one call

`/v1/matches` takes an `ids` parameter: up to fifty match ids in one
request, separated by a dash. Every other filter still applies, so a set can be narrowed
further.

An id that is not valid is refused rather than skipped. A response that quietly drops one
looks complete and is not, and that is a bug you would find in production rather than in
development.

#### 8 September 2026 — Match statistics

Shots, shots on target, possession, corners, fouls, offsides, cards, passes and expected
goals, per team per match. On [the match itself](https://footballsoccerapi.com/documentation/match) and on
[its own endpoint](https://footballsoccerapi.com/documentation/match-statistics) for when they are all you want.

Gathered while a match is in play and settled once it finishes, so a game still running may
carry a partial set. Field names follow the same convention as everything else: lower case,
whole words, and a percentage says `_pct`.

#### 6 September 2026 — Documentation published

Full reference for every endpoint, with parameters and live sample responses pulled from the
archive itself. Guides added for authentication, pagination, rate limits, errors, timezones and
historical data.

#### 6 September 2026 — Archive rebuilt

The match archive was restructured into a public schema: lowercase whole-word field names,
proper types, and a coverage table recording the fill rate of every optional field per
competition and season. Cancelled and postponed fixtures no longer report a 0–0 scoreline
— goals are now null unless the match was played.

#### 5 September 2026 — Accounts and free keys

Google sign-in, API key issue and rotation, and an account page showing usage against plan
limits.

#### 5 September 2026 — Pricing published

Two paid plans with the limits printed. Card payments open shortly.

### Following along

This page is the record. Subscription by email and RSS is coming with the first release that
changes a field.

---

## Endpoint reference

Every endpoint takes `X-API-Key` as a header. Paths are relative to
`https://api.footballsoccerapi.com`.

### Reference & discovery (12)

#### `GET /v1/reference` — **F and above**

**Reference data.** Every enum, code and lookup the API uses

Every enum the API can return, in one call: match statuses, result values, plan tiers and the field families. Fetch it once at startup rather than hard-coding strings.

```json
{
    "data": {
        "match_status": [
            "scheduled",
            "in_play_first_half",
            "half_time",
            "in_play_second_half",
            "finished",
            "finished_after_extra_time",
            "postponed",
            "cancelled",
            "abandoned",
            "awarded"
        ],
        "result": [
            "home",
            "draw",
            "away"
        ],
        "plans": [
            "free",
            "archive",
            "live"
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/countries` — **F and above**

**Countries.** Every country, with league and match counts

All 95 countries with their competition count, match count and the seasons covered. This is the natural first call — it tells you what exists before you filter anything.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "country_name": "England",
            "league_count": 37,
            "match_count": 55572,
            "season_first": 2012,
            "season_last": 2026
        },
        {
            "country_name": "USA",
            "league_count": 14,
            "match_count": 23599,
            "season_first": 2012,
            "season_last": 2026
        },
        {
            "country_name": "Turkey",
            "league_count": 11,
            "match_count": 21981,
            "season_first": 2012,
            "season_last": 2026
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/leagues` — **F and above**

**Competitions.** Every league, filterable by country

Every competition, with the country it belongs to, how many matches it holds and how many of those carry an exchange price. The `league_id` is the source division id and never changes.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "match_count": 7634,
            "priced_count": 5766,
            "season_first": 2012,
            "season_last": 2026
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "match_count": 7633,
            "priced_count": 5580,
            "season_first": 2012,
            "season_last": 2026
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "match_count": 7615,
            "priced_count": 5833,
            "season_first": 2012,
            "season_last": 2026
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/leagues/{id}` — **F and above**

**Competition profile.** One league, its seasons and coverage

A single competition with its coverage: seasons held, match count, and the proportion carrying prices.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |

- `league_id` — The competition id.

```json
{
    "data": {
        "league_id": 40,
        "country_name": "England",
        "league_name": "Championship",
        "match_count": 7634,
        "priced_count": 5766,
        "season_first": 2012,
        "season_last": 2026,
        "coverage": {
            "home_kickoff_price": 75.5
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/leagues/{id}/seasons/{season}` — **A and above**

**Season detail.** One league-season, its clubs and dates

One season of one competition: its date range, clubs, match count and per-field coverage.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |
| `season` | path | integer | yes | `2024` |

- `league_id` — The competition id.
- `season` — Season by starting year.

```json
{
    "data": {
        "league_id": 40,
        "country_name": "England",
        "league_name": "Championship",
        "match_count": 7634,
        "priced_count": 5766,
        "season_first": 2012,
        "season_last": 2026,
        "coverage": {
            "home_kickoff_price": 75.5
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/leagues/{id}/seasons/{season}/stats` — **A and above**

**Competition season summary.** A competition-season summarised in one call

One competition, one season, in a single call: matches, goals and goals per match, the home win, draw and away win split, goalless and over 2.5 rates, clean sheets both ways, average travel, and the most common scorelines with their share.

Counted from played matches only, so postponed and cancelled fixtures do not drag the averages. Everything except cards comes from results, so it covers every competition rather than only the ones we hold detail on; cards come from collected statistics and carry their own match count.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |
| `season` | path | integer | yes | `2024` |

- `league_id` — The competition id.
- `season` — Season by starting year.

```json
{
    "data": {
        "league_id": 40,
        "country_name": "England",
        "league_name": "Championship",
        "match_count": 7634,
        "priced_count": 5766,
        "season_first": 2012,
        "season_last": 2026,
        "coverage": {
            "home_kickoff_price": 75.5
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/leagues/{id}/teams` — **A and above**

**Clubs in a competition.** The clubs in a league, by season

The clubs that appear in a competition, optionally narrowed to one season.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |

- `league_id` — The competition id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.

```json
{
    "data": [
        {
            "team_id": 1343,
            "team_name": "Bradford",
            "venue_name": "Valley Parade",
            "city_name": "Bradford",
            "latitude": 53.795696,
            "longitude": -1.776683,
            "match_count": 694
        },
        {
            "team_id": 1338,
            "team_name": "Oxford United",
            "venue_name": "Kassam Stadium",
            "city_name": "Oxford",
            "latitude": 51.742927,
            "longitude": -1.218497,
            "match_count": 688
        },
        {
            "team_id": 1365,
            "team_name": "Grimsby",
            "venue_name": "Blundell Park",
            "city_name": "Grimsby",
            "latitude": 53.5647,
            "longitude": -0.0872,
            "match_count": 687
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/teams` — **F and above**

**Search clubs.** Search every club by name or country

Search across every club in the archive. Each carries its ground, city and coordinates, taken from its most recent home match.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "team_id": 1343,
            "team_name": "Bradford",
            "venue_name": "Valley Parade",
            "city_name": "Bradford",
            "latitude": 53.795696,
            "longitude": -1.776683,
            "match_count": 694
        },
        {
            "team_id": 1338,
            "team_name": "Oxford United",
            "venue_name": "Kassam Stadium",
            "city_name": "Oxford",
            "latitude": 51.742927,
            "longitude": -1.218497,
            "match_count": 688
        },
        {
            "team_id": 1365,
            "team_name": "Grimsby",
            "venue_name": "Blundell Park",
            "city_name": "Grimsby",
            "latitude": 53.5647,
            "longitude": -0.0872,
            "match_count": 687
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/teams/{id}` — **F and above**

**Club profile.** One club, its ids, ground and leagues

A single club: its id, name, ground, coordinates and how many matches it appears in.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |

- `team_id` — The club id.

```json
{
    "data": {
        "team_id": 1343,
        "team_name": "Bradford",
        "venue_name": "Valley Parade",
        "city_name": "Bradford",
        "latitude": 53.795696,
        "longitude": -1.776683,
        "match_count": 694
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/venues` — **A and above**

**Grounds.** Grounds with city and coordinates

Grounds with city and coordinates, derived from home matches. Coordinates are present on essentially every row, which makes distance and travel analysis reliable.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "team_id": 1343,
            "team_name": "Bradford",
            "venue_name": "Valley Parade",
            "city_name": "Bradford",
            "latitude": 53.795696,
            "longitude": -1.776683,
            "match_count": 694
        },
        {
            "team_id": 1338,
            "team_name": "Oxford United",
            "venue_name": "Kassam Stadium",
            "city_name": "Oxford",
            "latitude": 51.742927,
            "longitude": -1.218497,
            "match_count": 688
        },
        {
            "team_id": 1365,
            "team_name": "Grimsby",
            "venue_name": "Blundell Park",
            "city_name": "Grimsby",
            "latitude": 53.5647,
            "longitude": -0.0872,
            "match_count": 687
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/referees` — **A and above**

**Officials.** Officials with match counts

Match officials with the matches they took.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

The named official is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": {
        "match_status": [
            "scheduled",
            "in_play_first_half",
            "half_time",
            "in_play_second_half",
            "finished",
            "finished_after_extra_time",
            "postponed",
            "cancelled",
            "abandoned",
            "awarded"
        ],
        "result": [
            "home",
            "draw",
            "away"
        ],
        "plans": [
            "free",
            "archive",
            "live"
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/referees/{id}` — **A and above**

**Official profile.** One official and the matches they took

One official and the matches attributed to them.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

The named official is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": {
        "match_status": [
            "scheduled",
            "in_play_first_half",
            "half_time",
            "in_play_second_half",
            "finished",
            "finished_after_extra_time",
            "postponed",
            "cancelled",
            "abandoned",
            "awarded"
        ],
        "result": [
            "home",
            "draw",
            "away"
        ],
        "plans": [
            "free",
            "archive",
            "live"
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

### Matches & results (15)

#### `GET /v1/matches` — **F and above**

**Search matches.** The workhorse list, filter on anything on the row

The workhorse of the API. Every filter composes, so you can ask for finished Premier League matches in 2024 that carry a kickoff price and sort them however you like. Cursor paged, so a page boundary stays stable while new matches arrive. Pass `ids` to ask for a known set — up to fifty match ids in one request, dash separated, with every other filter still applying. An id that does not decode is refused rather than skipped, so a response is never quietly short.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `ids` | query | string | no | `mt_0CXSZRJ-mt_087EP2A` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `country` | query | string | no | `England` |
| `season` | query | integer | no | `2024` |
| `team_id` | query | string | no | `tm_1E5HXK9` |
| `date_from` | query | string | no | `2024-08-01` |
| `date_to` | query | string | no | `2024-08-31` |
| `status` | query | string | no | `finished` |
| `result` | query | string | no | `home` |
| `has_prices` | query | boolean | no | `true` |
| `surface` | query | string | no | `artificial_turf` |
| `venue` | query | string | no | `home` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |
| `sort` | query | string | no | `kickoff_utc` |

- `ids` — Up to 50 match ids in one request, separated by a dash. Every other filter still applies, so a set can be narrowed further. Each id costs one call against your rate limit, so this saves round trips rather than quota. An id that is not valid is refused rather than skipped — a response that quietly drops one looks complete and is not.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `team_id` — Matches involving this club, home or away.
- `date_from` — Earliest kickoff date, inclusive. ISO `YYYY-MM-DD`, evaluated in UTC.
- `date_to` — Latest kickoff date, inclusive.
- `status` — Lifecycle filter: `finished`, `scheduled`, `cancelled`, `postponed`, `abandoned`, or one of the in-play states.
- `result` — Full-time result: `home`, `draw` or `away`.
- `has_prices` — Only matches carrying an exchange price at kickoff. Useful when you are modelling and a match without a market is noise.
- `surface` — Restrict to matches played on a given surface: `grass`, `artificial_turf` or `sand_pitch`. Read from the ground rather than the match, so a stadium that resurfaces is corrected in one place.
- `venue` — Restrict to `home` or `away` matches. Omit for both.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.
- `sort` — Sort field, optionally prefixed with `-` for descending. Defaults to `-kickoff_utc`.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/matches/{id}` — **F and above**

**Match detail.** One match, every field it carries

One match with everything the row carries: score, half-time score, both clubs' league position at kickoff, how far the away side travelled, the ground and its coordinates, and the exchange market. **Fetching more than one?** Do not call this in a loop — `/v1/matches?ids=` takes up to fifty match ids in a single request, separated by a dash, which is one round trip instead of fifty. See [batch requests](https://footballsoccerapi.com/documentation/batch-requests). Team statistics are the one thing that does not come back on a list, so if you need those for several matches this endpoint is still the right one.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/matches/{id}/events` — **A and above**

**Match events.** Goals and cards, minute by minute

Goals and red cards with the minute and the scorer, where the record carries them.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

Goals and red cards is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": {
        "match_id": 1639937,
        "home_team_name": "Costa Rica",
        "away_team_name": "Curaçao",
        "events": [
            {
                "minute": 3,
                "extra": null,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Costa Rica",
                "player": "Orlando Sinclair",
                "assist": "Orlando Galo Calderon"
            },
            {
                "minute": 23,
                "extra": null,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Costa Rica",
                "player": "Carlos Mora",
                "assist": null
            },
            {
                "minute": 44,
                "extra": null,
                "type": "Card",
                "detail": "Yellow Card",
                "team": "Costa Rica",
                "player": "Erson Mendez",
                "assist": null
            },
            {
                "minute": 45,
                "extra": 1,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Costa Rica",
                "player": "Carlos Mora",
                "assist": null
            },
            {
                "minute": 57,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 1",
                "team": "Curaçao",
                "player": "Jearl Margaritha",
                "assist": "Kenji Gorre"
            },
            {
                "minute": 57,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 2",
                "team": "Curaçao",
                "player": "Ar'jany Martha",
                "assist": "Tahith Chong"
            },
            {
                "minute": 58,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 1",
                "team": "Costa Rica",
                "player": "Orlando Sinclair",
                "assist": "Gerald Taylor"
            },
            {
                "minute": 58,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 3",
                "team": "Curaçao",
                "player": "Jürgen Locadia",
                "assist": "Jordi Paulina"
            },
            {
                "minute": 59,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 2",
                "team": "Costa Rica",
                "player": "Josimar Alcocer",
                "assist": "Kenyel Jaheim Michel Vargas"
            },
            {
                "minute": 62,
                "extra": null,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Curaçao",
                "player": "Kenji Gorre",
                "assist": "Jordi Paulina"
            },
            {
                "minute": 66,
                "extra": null,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Curaçao",
                "player": "Tahith Chong",
                "assist": "Jordi Paulina"
            },
            {
                "minute": 77,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 3",
                "team": "Costa Rica",
                "player": "Orlando Galo Calderon",
                "assist": "Aaron Murillo Fonseca"
            },
            {
                "minute": 78,
                "extra": null,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Curaçao",
                "player": "Kenji Gorre",
                "assist": null
            },
            {
                "minute": 82,
                "extra": null,
                "type": "Goal",
                "detail": "Normal Goal",
                "team": "Curaçao",
                "player": "Jordi Paulina",
                "assist": "Juninho Bacuna"
            },
            {
                "minute": 83,
                "extra": null,
                "type": "Card",
                "detail": "Yellow Card",
                "team": "Curaçao",
                "player": "Jordi Paulina",
                "assist": null
            },
            {
                "minute": 86,
                "extra": null,
                "type": "subst",
                "detail": "Substitution 4",
                "team": "Costa Rica",
                "player": "Carlos Luis Barahona Jiménez",
                "assist": "dorian rodriguez"
            },
            {
                "minute": 90,
                "extra": null,
                "type": "Card",
                "detail": "Yellow Card",
                "team": "Curaçao",
                "player": "Leandro Bacuna",
                "assist": null
            },
            {
                "minute": 90,
                "extra": 1,
                "type": "subst",
                "detail": "Substitution 4",
                "team": "Curaçao",
                "player": "Juninho Bacuna",
                "assist": "Nicky Souren"
            }
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/matches/{id}/odds` — **A and above**

**Match odds.** Both price snapshots, volume and overround

The exchange market for one match: the three-way price at kickoff, the half-time snapshot where it exists, implied probability for each outcome, the overround, and matched volume.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

The half-time price is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": {
        "match_id": 1490209,
        "home_team_name": "Seattle Sounders",
        "away_team_name": "Real Salt Lake",
        "kickoff": {
            "home": {
                "price": 1.6,
                "implied": 62.5
            },
            "draw": {
                "price": 3.8,
                "implied": 26.3
            },
            "away": {
                "price": 5,
                "implied": 20
            }
        },
        "half_time": {
            "home": {
                "price": null
            }
        },
        "overround": 1.0882,
        "matched_kickoff_volume": null,
        "market_id": "1.262565532"
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/matches/{id}/statistics` — **A and above**

**Match statistics.** Shots, possession, corners and cards for one match

Team statistics for one match — shots, shots on target, possession, corners, fouls, offsides and cards — as two blocks, one per side. They are gathered while a match is in play and settle once it finishes, so a game still running may carry a partial set. They also come back on the match itself; this endpoint is for when they are all you want.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/matches/{id}/lineups` — **A and above**

**Match lineups.** Formation, starting eleven and substitutes

Formation, the starting eleven with shirt numbers and grid positions, the substitutes and the coach, for each side. Grid positions are row and column, counting from the goalkeeper out, so a shape can be drawn without guessing at it. Formation also comes back on the match itself, which is the field to filter and group by.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/teams/{id}/distance/{opponent_id}` — **F and above**

**Distance between clubs.** How far one club travels to another

How far the second club travels to the first, as the crow flies between the two grounds, in kilometres and miles. The same measure as `away_travel_km` on a match, so a distance looked up here and one read off a fixture agree.

Useful before a fixture exists — weighing a cup draw, planning a season, testing whether travel and rest explain anything. If we hold no coordinates for one of the grounds the distance comes back null and the note names which club, rather than leaving you to work out whose fault it is.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `opponent_id` | path | string | yes | `tm_21FP1AT` |

- `team_id` — The club id.
- `opponent_id` — The opposing club id.

Travel distance is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": {
        "team_id": 1343,
        "team_name": "Bradford",
        "venue_name": "Valley Parade",
        "city_name": "Bradford",
        "latitude": 53.795696,
        "longitude": -1.776683,
        "match_count": 694
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/grounds` — **F and above**

**Grounds.** Every ground, with capacity and surface

Every ground we hold detail for, with its city, capacity and playing surface, and how many of our matches were played there. Ordered by that count, so the grounds you are most likely to want come first. Filter by surface to find the artificial pitches — scoring and card rates differ on them, and few sources tell you which is which.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `surface` | query | string | no | `artificial_turf` |
| `min_capacity` | query | integer | no | `30000` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `surface` — Restrict to matches played on a given surface: `grass`, `artificial_turf` or `sand_pitch`. Read from the ground rather than the match, so a stadium that resurfaces is corrected in one place.
- `min_capacity` — Grounds holding at least this many.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.

```json
{
    "data": [],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/grounds/{id}` — **F and above**

**Ground profile.** One ground and how much we hold on it

One ground: name, city, country, capacity, surface, and how many matches in the archive were played there.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `venue_id` | path | string | yes | `vn_0K7Y8MT` |

- `venue_id` — The ground, from `/v1/grounds`.

```json
{
    "data": [],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/matches/{id}/preview` — **L and above**

**Fixture preview.** An upcoming fixture with form and market

An upcoming fixture with both clubs' recent form, their league positions and the current market.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/results/today` — **F and above**

**Today's results.** Everything settled today

Everything settled today, across every competition.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/results/yesterday` — **F and above**

**Yesterday's results.** Everything settled yesterday

Everything settled yesterday, as a whole UTC day, across every competition. A match is in yesterday or today, never both.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/results/latest` — **F and above**

**Latest results.** The most recent settled results

The most recently settled matches, newest first. On the free key this reaches back to yesterday.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/results` — **A and above**

**Results by date.** Settled results over any date range

Settled results over any date range in the archive.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `date_from` | query | string | no | `2024-08-01` |
| `date_to` | query | string | no | `2024-08-31` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `country` | query | string | no | `England` |
| `team_id` | query | string | no | `tm_1E5HXK9` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `date_from` — Earliest kickoff date, inclusive. ISO `YYYY-MM-DD`, evaluated in UTC.
- `date_to` — Latest kickoff date, inclusive.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `team_id` — Matches involving this club, home or away.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/seasons/{league}/{season}` — **A and above**

**A whole season.** A whole season in one call

Every match of one season of one competition, in kickoff order.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |
| `season` | path | integer | yes | `2024` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `league_id` — The competition id.
- `season` — Season by starting year.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

### Team form & stats (9)

#### `GET /v1/teams/{id}/matches` — **A and above**

**A club's matches.** Every match a club has played

Every match a club has played, optionally narrowed by season or to home or away only.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `season` | query | integer | no | `2024` |
| `venue` | query | string | no | `home` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `team_id` — The club id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `venue` — Restrict to `home` or `away` matches. Omit for both.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/teams/{id}/form` — **A and above**

**Recent form.** Last N results, home and away split

The last N results with a home and away split, goals for and against, and the market price in each.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `last` | query | integer | no | `10` |
| `venue` | query | string | no | `home` |

- `team_id` — The club id.
- `last` — How many recent matches to consider, 1–50.
- `venue` — Restrict to `home` or `away` matches. Omit for both.

```json
{
    "data": {
        "team_id": 47,
        "played": 10,
        "record": {
            "won": 6,
            "drawn": 2,
            "lost": 2
        },
        "matches": [
            {
                "match_id": 1528880,
                "kickoff_date": "2026-09-24",
                "home_team_name": "Liechtenstein",
                "away_team_name": "Lithuania",
                "home_goals": 0,
                "away_goals": 2,
                "full_time_result": "away",
                "home_kickoff_price": 10.5
            },
            {
                "match_id": 1528879,
                "kickoff_date": "2026-09-24",
                "home_team_name": "Kosovo",
                "away_team_name": "Rep. Of Ireland",
                "home_goals": 1,
                "away_goals": 0,
                "full_time_result": "home",
                "home_kickoff_price": 2.66
            },
            {
                "match_id": 1528878,
                "kickoff_date": "2026-09-24",
                "home_team_name": "Austria",
                "away_team_name": "Israel",
                "home_goals": 3,
                "away_goals": 1,
                "full_time_result": "home",
                "home_kickoff_price": 1.38
            }
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/teams/{id}/stats` — **A and above**

**Club statistics.** Goals, clean sheets, half-time record

Goals, clean sheets, half-time record and results split by venue, over a season or the whole archive.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `season` | query | integer | no | `2024` |
| `venue` | query | string | no | `home` |

- `team_id` — The club id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `venue` — Restrict to `home` or `away` matches. Omit for both.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/teams/{id}/streaks` — **A and above**

**Runs and streaks.** Current and longest runs

Current and longest runs: wins, unbeaten, clean sheets, scoring and failing to score.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `season` | query | integer | no | `2024` |

- `team_id` — The club id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/teams/{id}/match-stats` — **A and above**

**Team match statistics.** Shots, possession, corners, cards and formations used

What a club actually did on the pitch, averaged per match: shots, shots on target, possession, corners, fouls, offsides and cards. Plus every formation it lined up in, with the record in each — won, drawn, lost and goals both ways.

Everything else on a club is reasoned from results. This is the other kind: it cannot be derived from a scoreline and exists only where it was recorded, so **every average carries the number of matches it was computed from**. An average over three matches and one over thirty-eight are not the same claim.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `season` | query | integer | no | `2024` |
| `venue` | query | string | no | `home` |

- `team_id` — The club id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `venue` — Restrict to `home` or `away` matches. Omit for both.

```json
{
    "data": {
        "team_id": 1343,
        "team_name": "Bradford",
        "venue_name": "Valley Parade",
        "city_name": "Bradford",
        "latitude": 53.795696,
        "longitude": -1.776683,
        "match_count": 694
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/teams/{id}/head-to-head/{opp}` — **A and above**

**Head to head.** The full meeting history between two clubs

The full meeting history between two clubs, with the aggregate record.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `opponent_id` | path | string | yes | `tm_21FP1AT` |
| `limit` | query | integer | no | `100` |

- `team_id` — The club id.
- `opponent_id` — The opposing club id.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/teams/{id}/market` — **A and above**

**How the market prices a club.** How the market has priced them over time

How the exchange has priced a club over time: average kickoff price home and away, and how often the price was right.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `season` | query | integer | no | `2024` |
| `venue` | query | string | no | `home` |

- `team_id` — The club id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `venue` — Restrict to `home` or `away` matches. Omit for both.

```json
{
    "data": {
        "match_id": 1490209,
        "home_team_name": "Seattle Sounders",
        "away_team_name": "Real Salt Lake",
        "kickoff": {
            "home": {
                "price": 1.6,
                "implied": 62.5
            },
            "draw": {
                "price": 3.8,
                "implied": 26.3
            },
            "away": {
                "price": 5,
                "implied": 20
            }
        },
        "half_time": {
            "home": {
                "price": null
            }
        },
        "overround": 1.0882,
        "matched_kickoff_volume": null,
        "market_id": "1.262565532"
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/teams/{id}/travel` — **A and above**

**Travel.** Distance travelled and how they fare on the road

Distance travelled to away matches and the record achieved at each range. Travel distance is present on 99.9% of matches, so this is one of the most complete things in the archive.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `team_id` | path | string | yes | `tm_1E5HXK9` |
| `season` | query | integer | no | `2024` |

- `team_id` — The club id.
- `season` — Season by its starting year. A 2024/25 season is `2024`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/teams/compare` — **A and above**

**Compare clubs.** Two or more clubs side by side

Two or more clubs side by side on the same measures.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `season` | query | integer | no | `2024` |

- `season` — Season by its starting year. A 2024/25 season is `2024`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

### Standings (2)

#### `GET /v1/standings/{league}/{season}` — **A and above**

**League table.** The table as it stood on any date

The table computed from the same match rows, as it stood on any date. Not a stored table — it is derived, so an as_of in the middle of a season gives you the table as it actually was.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |
| `season` | path | integer | yes | `2024` |
| `as_of` | query | string | no | `2024-10-19` |
| `venue` | query | string | no | `home` |

- `league_id` — The competition id.
- `season` — Season by starting year.
- `as_of` — Compute the table as it stood on this date rather than at the end of the season.
- `venue` — Restrict to `home` or `away` matches. Omit for both.

```json
{
    "data": {
        "league_id": 39,
        "season_start_year": 2024,
        "as_of": "2024-10-19",
        "table": [
            {
                "position": 1,
                "team_id": 42,
                "team_name": "Liverpool",
                "played": 8,
                "won": 6,
                "drawn": 1,
                "lost": 1,
                "goals_for": 17,
                "goals_against": 5,
                "goal_difference": 12,
                "points": 19
            },
            {
                "position": 2,
                "team_id": 50,
                "team_name": "Man City",
                "played": 8,
                "won": 5,
                "drawn": 2,
                "lost": 1,
                "goals_for": 17,
                "goals_against": 9,
                "goal_difference": 8,
                "points": 17
            }
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "note": "Computed from match rows, not stored."
    }
}
```

#### `GET /v1/standings/{league}/{season}/positions` — **A and above**

**Positions at every kickoff.** Every club position at every kickoff

Every club's league position before every match it played. This is the same computation that fills home_league_position on a match row.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | path | string | yes | `lg_24T9Z0G` |
| `season` | path | integer | yes | `2024` |

- `league_id` — The competition id.
- `season` — Season by starting year.

```json
{
    "data": {
        "league_id": 39,
        "season_start_year": 2024,
        "as_of": "2024-10-19",
        "table": [
            {
                "position": 1,
                "team_id": 42,
                "team_name": "Liverpool",
                "played": 8,
                "won": 6,
                "drawn": 1,
                "lost": 1,
                "goals_for": 17,
                "goals_against": 5,
                "goal_difference": 12,
                "points": 19
            },
            {
                "position": 2,
                "team_id": 50,
                "team_name": "Man City",
                "played": 8,
                "won": 5,
                "drawn": 2,
                "lost": 1,
                "goals_for": 17,
                "goals_against": 9,
                "goal_difference": 8,
                "points": 17
            }
        ]
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "note": "Computed from match rows, not stored."
    }
}
```

### Fixtures & live (6)

#### `GET /v1/fixtures/today` — **F and above**

**Today's fixtures.** Today's card across every league

Today's card across every competition, with kickoff times in UTC and local.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/fixtures/upcoming` — **F and above**

**Upcoming fixtures.** The fixtures coming up

A rolling window of fixtures ahead.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `days` | query | integer | no | `7` |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `days` — Window size in days, 1–30.
- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/fixtures/rescheduled` — **L and above**

**Rescheduled kickoffs.** Kickoffs that have moved

Fixtures whose kickoff has moved — 11,134 matches in the archive carry that flag.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `days` | query | integer | no | `7` |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `limit` | query | integer | no | `100` |

- `days` — Window size in days, 1–30.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/live` — **L and above**

**In play now.** Everything in play right now

Everything in play right now, with the current score and period.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.

```json
{
    "data": [
        {
            "match_id": 1528880,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Liechtenstein",
            "away_team_name": "Lithuania",
            "home_goals": 0,
            "away_goals": 2,
            "full_time_result": "away",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 10.5,
            "draw_kickoff_price": 4.3,
            "away_kickoff_price": 1.42
        },
        {
            "match_id": 1528879,
            "country_name": "World",
            "league_name": "UEFA Nations League",
            "kickoff_utc": 1790275500,
            "kickoff_date": "2026-09-24",
            "match_status": "finished",
            "home_team_name": "Kosovo",
            "away_team_name": "Rep. Of Ireland",
            "home_goals": 1,
            "away_goals": 0,
            "full_time_result": "home",
            "home_league_position": null,
            "away_league_position": null,
            "away_travel_km": null,
            "home_kickoff_price": 2.66,
            "draw_kickoff_price": 3.28,
            "away_kickoff_price": 2.85
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 2,
        "next_cursor": "MTcyOTMzNzQwMDoxMjA4MDk5",
        "page": 1,
        "per_page": 100,
        "total": 380,
        "total_pages": 4,
        "next_page": 2,
        "prev_page": null
    }
}
```

#### `GET /v1/live/{id}` — **L and above**

**Live match detail.** One match, live

A single match while it is being played.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/live/odds` — **L and above**

**Live prices.** Current prices on in-play matches

Current exchange prices on in-play matches.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `country` | query | string | no | `England` |
| `league_id` | query | string | no | `lg_24T9Z0G` |

- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.

```json
{
    "data": [
        {
            "match_id": 1598672,
            "kickoff_date": "2026-11-17",
            "home_team_name": "Vizela U23",
            "away_team_name": "Santa Clara U23",
            "home_kickoff_price": 2.85,
            "draw_kickoff_price": 3.25,
            "away_kickoff_price": 2.17,
            "full_time_result": null,
            "home_implied": 35.1
        },
        {
            "match_id": 1580739,
            "kickoff_date": "2026-11-14",
            "home_team_name": "Llantwit Major",
            "away_team_name": "Cardiff Draconians",
            "home_kickoff_price": 3.925,
            "draw_kickoff_price": 4.1,
            "away_kickoff_price": 1.75,
            "full_time_result": null,
            "home_implied": 25.5
        },
        {
            "match_id": 1593357,
            "kickoff_date": "2026-11-07",
            "home_team_name": "Olympiakos Piraeus",
            "away_team_name": "PAOK",
            "home_kickoff_price": 1.86,
            "draw_kickoff_price": 3.35,
            "away_kickoff_price": 4,
            "full_time_result": null,
            "home_implied": 53.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

### Odds & markets (9)

#### `GET /v1/odds/kickoff` — **A and above**

**Kickoff prices.** Kickoff prices across matches

Kickoff prices across matches, filterable by price band. The exchange price carries no bookmaker margin, so the implied probability is close to the crowd's real view.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `country` | query | string | no | `England` |
| `season` | query | integer | no | `2024` |
| `date_from` | query | string | no | `2024-08-01` |
| `date_to` | query | string | no | `2024-08-31` |
| `min_price` | query | number | no | `1.5` |
| `max_price` | query | number | no | `2.0` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `date_from` — Earliest kickoff date, inclusive. ISO `YYYY-MM-DD`, evaluated in UTC.
- `date_to` — Latest kickoff date, inclusive.
- `min_price` — Lower bound on the kickoff price for the selection.
- `max_price` — Upper bound on the kickoff price for the selection.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

```json
{
    "data": [
        {
            "match_id": 1598672,
            "kickoff_date": "2026-11-17",
            "home_team_name": "Vizela U23",
            "away_team_name": "Santa Clara U23",
            "home_kickoff_price": 2.85,
            "draw_kickoff_price": 3.25,
            "away_kickoff_price": 2.17,
            "full_time_result": null,
            "home_implied": 35.1
        },
        {
            "match_id": 1580739,
            "kickoff_date": "2026-11-14",
            "home_team_name": "Llantwit Major",
            "away_team_name": "Cardiff Draconians",
            "home_kickoff_price": 3.925,
            "draw_kickoff_price": 4.1,
            "away_kickoff_price": 1.75,
            "full_time_result": null,
            "home_implied": 25.5
        },
        {
            "match_id": 1593357,
            "kickoff_date": "2026-11-07",
            "home_team_name": "Olympiakos Piraeus",
            "away_team_name": "PAOK",
            "home_kickoff_price": 1.86,
            "draw_kickoff_price": 3.35,
            "away_kickoff_price": 4,
            "full_time_result": null,
            "home_implied": 53.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/odds/half-time` — **A and above**

**Half-time prices.** Half-time prices and the drift from kickoff

The half-time snapshot of the same market, and the drift from the kickoff price.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `date_from` | query | string | no | `2024-08-01` |
| `date_to` | query | string | no | `2024-08-31` |
| `limit` | query | integer | no | `100` |
| `page` | query | integer | no | `2` |
| `cursor` | query | string | no | — |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `date_from` — Earliest kickoff date, inclusive. ISO `YYYY-MM-DD`, evaluated in UTC.
- `date_to` — Latest kickoff date, inclusive.
- `limit` — Rows per page, 1–1000. Defaults to 100.
- `page` — Page number, 1-based. The response carries `total`, `total_pages`, `next_page` and `prev_page`, so a UI pager has everything it needs from one call. If you send both `page` and `cursor`, page wins.
- `cursor` — Opaque cursor from `meta.next_cursor`. Pages by sort position rather than offset, so page 10,000 is as fast as page 1 and a new match arriving cannot shift the boundary. Pass it back unchanged; do not build or parse one yourself.

The half-time price is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": [
        {
            "match_id": 1598672,
            "kickoff_date": "2026-11-17",
            "home_team_name": "Vizela U23",
            "away_team_name": "Santa Clara U23",
            "home_kickoff_price": 2.85,
            "draw_kickoff_price": 3.25,
            "away_kickoff_price": 2.17,
            "full_time_result": null,
            "home_implied": 35.1
        },
        {
            "match_id": 1580739,
            "kickoff_date": "2026-11-14",
            "home_team_name": "Llantwit Major",
            "away_team_name": "Cardiff Draconians",
            "home_kickoff_price": 3.925,
            "draw_kickoff_price": 4.1,
            "away_kickoff_price": 1.75,
            "full_time_result": null,
            "home_implied": 25.5
        },
        {
            "match_id": 1593357,
            "kickoff_date": "2026-11-07",
            "home_team_name": "Olympiakos Piraeus",
            "away_team_name": "PAOK",
            "home_kickoff_price": 1.86,
            "draw_kickoff_price": 3.35,
            "away_kickoff_price": 4,
            "full_time_result": null,
            "home_implied": 53.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/odds/favourites` — **A and above**

**Favourites.** Matches grouped by favourite strength

Matches grouped by how short the favourite was, with the record at each band.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `min_price` | query | number | no | `1.5` |
| `max_price` | query | number | no | `2.0` |
| `limit` | query | integer | no | `100` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `min_price` — Lower bound on the kickoff price for the selection.
- `max_price` — Upper bound on the kickoff price for the selection.
- `limit` — Rows per page, 1–1000. Defaults to 100.

```json
{
    "data": [
        {
            "match_id": 1598672,
            "kickoff_date": "2026-11-17",
            "home_team_name": "Vizela U23",
            "away_team_name": "Santa Clara U23",
            "home_kickoff_price": 2.85,
            "draw_kickoff_price": 3.25,
            "away_kickoff_price": 2.17,
            "full_time_result": null,
            "home_implied": 35.1
        },
        {
            "match_id": 1580739,
            "kickoff_date": "2026-11-14",
            "home_team_name": "Llantwit Major",
            "away_team_name": "Cardiff Draconians",
            "home_kickoff_price": 3.925,
            "draw_kickoff_price": 4.1,
            "away_kickoff_price": 1.75,
            "full_time_result": null,
            "home_implied": 25.5
        },
        {
            "match_id": 1593357,
            "kickoff_date": "2026-11-07",
            "home_team_name": "Olympiakos Piraeus",
            "away_team_name": "PAOK",
            "home_kickoff_price": 1.86,
            "draw_kickoff_price": 3.35,
            "away_kickoff_price": 4,
            "full_time_result": null,
            "home_implied": 53.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3
    }
}
```

#### `GET /v1/odds/bands` — **A and above**

**Price bands.** How results fall by price band

How results actually fall by price band: implied probability against the strike rate that followed. This is the calibration exhibit behind the Lab.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/odds/overround` — **A and above**

**Market tightness.** Market tightness by league and season

Overround by competition and season — how close each market ran to 100%.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/odds/volume` — **A and above**

**Matched volume.** Matched volume, pre-match and at kickoff

Matched volume before the match and at kickoff, as a measure of how deep the market was.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

Matched volume is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/betfair/markets/{id}` — **A and above**

**Look up by market id.** Look a match up by its market id

Find a match from the exchange market id, so an archive row joins onto your own logs.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/betfair/events/{id}` — **A and above**

**Look up by event id.** Look a match up by its event id

Find a match from the exchange event id.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `match_id` | path | string | yes | `mt_1JK5QJ9` |

- `match_id` — The match id. Stable across re-imports — what you store today still resolves later.

```json
{
    "data": {
        "match_id": 1490209,
        "league_id": 253,
        "country_name": "USA",
        "league_name": "Major League Soccer",
        "season_start_year": 2026,
        "kickoff_utc": 1790213400,
        "kickoff_date": "2026-09-24",
        "kickoff_local_time": "02:30",
        "match_status": "finished",
        "home_team_id": 1595,
        "home_team_name": "Seattle Sounders",
        "away_team_id": 1606,
        "away_team_name": "Real Salt Lake",
        "home_goals": 2,
        "away_goals": 0,
        "full_time_result": "home",
        "half_time_home_goals": 0,
        "half_time_away_goals": 0,
        "half_time_result": "draw",
        "stoppage_minutes": null,
        "is_kickoff_rescheduled": false,
        "home_league_position": 5,
        "away_league_position": 8,
        "away_travel_km": 1133,
        "venue_name": "Lumen Field",
        "city_name": "Seattle",
        "latitude": 47.595115,
        "longitude": -99.99999999,
        "referee_name": "Joseph Dickerson",
        "betfair": {
            "home_kickoff_price": 1.6,
            "draw_kickoff_price": 3.8,
            "away_kickoff_price": 5,
            "home_half_time_price": null,
            "matched_kickoff_volume": null,
            "market_id": "1.262565532"
        }
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `POST /v1/backtest` — **A and above**

**Backtest your rules.** Your rules, P&L at exchange prices net of commission

Post a rule and get the P&L back, staked level at the kickoff price and net of commission, with the equity curve and the maximum drawdown. The same engine that produces the Lab.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `date_from` | query | string | no | `2024-08-01` |
| `date_to` | query | string | no | `2024-08-31` |
| `min_price` | query | number | no | `1.5` |
| `max_price` | query | number | no | `2.0` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `date_from` — Earliest kickoff date, inclusive. ISO `YYYY-MM-DD`, evaluated in UTC.
- `date_to` — Latest kickoff date, inclusive.
- `min_price` — Lower bound on the kickoff price for the selection.
- `max_price` — Upper bound on the kickoff price for the selection.

```json
{
    "data": {
        "rule": {
            "back": "home",
            "max_price": 1.8,
            "league_id": 39
        },
        "bets": 1284,
        "won": 704,
        "strike_rate": 54.8,
        "roi_net": -3.1,
        "max_drawdown": -18.4,
        "commission": 0.02,
        "staking": "level"
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "note": "P&L at kickoff prices, net of commission."
    }
}
```

### League & global stats (7)

#### `GET /v1/stats/goals` — **A and above**

**Goals.** Goals per game by league and season

Goals per game, over and under rates, and both-teams-to-score, by competition and season.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `country` | query | string | no | `England` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/stats/scorelines` — **A and above**

**Scorelines.** The scoreline distribution

The distribution of exact scorelines.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/stats/home-advantage` — **A and above**

**Home advantage.** Home win rate, by league and season

Home win rate by competition and season — how much home advantage is actually worth, and where it is largest.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `country` | query | string | no | `England` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `country` — Restrict to one country, by name as returned from `/v1/countries`.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/stats/half-time` — **A and above**

**Half-time conversion.** How often half-time leads convert

How often a half-time lead converts to a win, by competition. Half-time scores exist on 83.5% of the archive, so this is well supported.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/stats/kickoff` — **A and above**

**Kickoff time.** Results by kickoff time and weekday

Results by kickoff time and weekday, using the local kickoff time rather than UTC.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/stats/referees` — **A and above**

**Officials.** Cards and results by official

Results and cards by official.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.

The named official is not carried on every match. The coverage figures on each competition and season give the exact share for the rows you are asking for.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

#### `GET /v1/stats/positions` — **A and above**

**Position and result.** How league position tracks the result

How the gap in league position at kickoff tracks the result — the single most complete context field in the archive at 95.9%.

| Parameter | In | Type | Required | Example |
| --- | --- | --- | --- | --- |
| `league_id` | query | string | no | `lg_24T9Z0G` |
| `season` | query | integer | no | `2024` |
| `group_by` | query | string | no | `league` |

- `league_id` — Restrict to one competition. Ids come from `/v1/leagues` — 39 is the English Premier League, 62 is Ligue 2.
- `season` — Season by its starting year. A 2024/25 season is `2024`.
- `group_by` — Aggregate by `league`, `season`, `country` or `league_season`.

```json
{
    "data": [
        {
            "league_id": 40,
            "country_name": "England",
            "league_name": "Championship",
            "matches": 7523,
            "goals_per_game": 2.56,
            "home_win_pct": 42.7
        },
        {
            "league_id": 42,
            "country_name": "England",
            "league_name": "League Two",
            "matches": 7386,
            "goals_per_game": 2.53,
            "home_win_pct": 41.8
        },
        {
            "league_id": 41,
            "country_name": "England",
            "league_name": "League One",
            "matches": 7369,
            "goals_per_game": 2.61,
            "home_win_pct": 42.8
        }
    ],
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB",
        "count": 3,
        "group_by": "league"
    }
}
```

### Platform (2)

#### `GET /v1/usage` — **F and above**

**Your usage.** Your calls, limits and reset window

Your calls today and this period against your plan limits, and when the window resets. The same figures your account page shows.

```json
{
    "data": {
        "plan": "free",
        "per_minute_limit": 60,
        "per_day_limit": 50,
        "used_today": 17,
        "remaining_today": 33,
        "resets_at": "2026-09-26T00:00:00Z"
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

#### `GET /v1/status` — **F and above**

**Service status.** Service health and data_as_of

Service health and data_as_of — how fresh the archive is right now.

```json
{
    "data": {
        "status": "operational",
        "matches": 710111,
        "rebuilt_at": "2026-09-25T03:48:31Z"
    },
    "meta": {
        "data_as_of": "2026-09-25T03:48:31Z",
        "request_id": "01J9QW3C7M4KX2VB"
    }
}
```

### Push & assistant (2)

#### `POST /webhooks` — **L and above**

**Webhooks.** Fire on match start, price move or result

Register a URL and we post to it on match start, price move or result. Every delivery is signed.

```json
{
    "event": "result",
    "match_id": 1208099,
    "home_goals": 4,
    "away_goals": 1,
    "full_time_result": "home",
    "signature": "sha256=..."
}
```

#### `POST /mcp` — **A and above**

**Assistant tools.** The registry as assistant tools

The registry exposed as MCP tools, so an assistant can query the archive without any glue code.

```json
{
    "tools": [
        {
            "name": "fsapi.matches",
            "description": "Search matches"
        },
        {
            "name": "fsapi.odds",
            "description": "Exchange prices for a match"
        }
    ]
}
```

---

## About this document

Assembled from the endpoint registry, the guides and the plan configuration
the website itself renders from. It is regenerated whenever any of those
change, so a stale copy is the only way it can be wrong — check the date at
the top against https://footballsoccerapi.com/documentation/manual.md.

Machine-readable alternatives: `https://footballsoccerapi.com/v1/openapi.json` for the OpenAPI
description, `https://footballsoccerapi.com/v1/postman.json` for Postman, and `https://footballsoccerapi.com/mcp`
for the Model Context Protocol server.

