Reference
API documentation
Base URL https://admin.thefootballsignal.com/bsd-api/v1. Every route is a GET. No key, no account, no headers — Accept: application/json is assumed and CORS is open, so a browser can call it directly.
Rate limit
120 requests per minute per IP. Exceeding it returns 429 with a Retry-After header.
Read-only
Anything but GET or HEAD is refused with 405 before routing. There is no write surface to find.
Freshness
Responses carry fetched_at. Last write 9 min ago.
Conventions
Every successful response is an envelope. data holds the result; meta holds the version, the generation time, and — on a list — the pagination.
{
"data": [
"…"
],
"meta": {
"version": "v1",
"generated_at": "2026-09-03T10:55:32+00:00",
"page": 1,
"per_page": 2,
"total": 6000,
"last_page": 3000,
"resource": "events"
}
}
Pagination is page and per_page — not limit and offset. per_page is clamped to 200; anything larger is silently reduced rather than rejected.
Sorting is sort plus dir. Five real columns can be sorted directly — ref, parent_ref, fetched_at, created_at, updated_at — and any other name is read as a key inside the stored payload. An unusable name falls back to the default rather than erroring.
Ids are strings everywhere in the shaped responses, even where the stored payload holds a number. Compare them as strings.
Dates are ISO-8601 with an offset. Date filters compare lexically against the stored string, which is chronological for this format; a bare date_to is treated as the whole of that day.
Meta
What the service is, whether it is healthy, and what it holds.
Service document: version, endpoint index, event parts and totals.
Liveness, object count, and the timestamp of the most recent write.
Every resource with its group, row count and last fetch.
This reference as Markdown.
Leagues
Competitions, their seasons, tables and squads. Every row carries event_count and covered, so you can see how deep the store goes before you build against it.
One competition with its stored detail payload.
Season list, current season marked.
Grounds used in the competition.
Events
/events/{id} returns the fixture plus an index of which parts are stored and when each was fetched; /events/{id}/{part} returns one part exactly as the provider sent it.
One part. Part names are listed below.
Collections
Cross-fixture views of the parts — the column rather than the row. All four accept a comma-separated event_id list, so assembling a board of forty fixtures is one request, not forty.
Raw store
The stored payload, untouched, for any resource. Use this when the shaped endpoints are not the shape you want.
Resource index with counts.
One stored payload by its key.
Event parts
Thirteen parts hang off a fixture. Ask for one with /events/{id}/{part}, or for several at once with /events/{id}?include=stats,lineups. Not every fixture has every part — ?parts_only=1 tells you which exist without transferring any of them.
| Part | Resource | What it holds |
|---|---|---|
| detail | event · 18,643 | Full fixture record — period, current minute, half-time score, referee, coaches, derby flag. |
| stats | event_stats · 18,002 | Team statistics, xG, a shotmap with per-shot coordinates, momentum and average positions. |
| lineups | event_lineups · 17,757 | Starting XI, bench, formations and per-player positions. |
| incidents | event_incidents · 17,978 | Goals, cards, substitutions and VAR events with minutes. |
| player-stats | event_player_stats · 17,979 | Per-player match statistics including xG, expected assists, touches and ratings. |
| h2h | event_h2h · 17,751 | Head-to-head record between the two clubs. |
| odds | event_odds · 17,108 | Consensus odds across the main markets. |
| odds-comparison | event_odds_comparison · 8,515 | Per-bookmaker odds for the same markets. |
| polymarket | event_polymarket · 676 | Prediction-market lines, where they exist. |
| prediction | event_prediction · 1,785 | The provider's stored model prediction. |
| metadata | event_metadata · 16,552 | Preview text and editorial metadata. |
| broadcasts | event_broadcasts · 16,559 | Broadcaster listings by territory. |
| social | event_social · 16,562 | Social handles and links for the fixture. |
Parameters
| Name | Type | Notes |
|---|---|---|
| page | int | Page number. Default 1. |
| per_page | int | Page size, 1–200. Default 50. |
| sort | string | ref · parent_ref · fetched_at · created_at · updated_at, or any payload key. |
| dir | enum | asc or desc. |
| q | string | Free-text match over the key and the stored payload. |
| league_id | int | Restrict to one competition. |
| season_id | int | Restrict to one season. |
| team_id | int | Fixtures involving one club, home or away. |
| team | string | Case-insensitive partial match on either club name. |
| status | enum | notstarted · inprogress · finished · postponed · canceled. Matched literally against the stored value, and the provider has emitted both canceled and cancelled — query the one you need. |
| date_from | date | ISO date. Inclusive. |
| date_to | date | ISO date. Inclusive — the bound moves to the next midnight so the last day is not silently dropped. |
| event_id | list | Comma-separated fixture ids, up to 200. |
| country | string | Competition country, as the provider spells it. |
| is_women | bool | Women's competitions only when 1. |
| has_events | bool | Only competitions with real coverage (1), or only those without (0). |
| include | list | Comma-separated part names. Unknown names are ignored, not rejected. |
| parts_only | bool | Drop the payloads and return only which parts exist — the cheap call. |
| include_table | bool | Embed the full table rather than a summary row. |
| stat | enum | Leaderboard name. Default scorers. |
| ref | string | Exact stored key. |
| parent_ref | string | Exact parent key. |
| days | int | Window for /stats, 2–60. Default 14. |
Errors
Errors use the same envelope, with error.code and error.message instead of data. The code is stable; the message is for a human and may change.
{
"error": {
"code": "part_not_stored",
"message": "No stored 'stats' for event '1'."
},
"meta": {
"version": "v1",
"generated_at": "2026-09-03T10:52:47+00:00"
}
}
| Code | HTTP | Meaning |
|---|---|---|
| event_not_found | 404 | Nothing is stored for that fixture id — not "the fixture does not exist". |
| part_not_stored | 404 | The part name is valid, but that fixture has no such part stored yet. |
| unknown_part | 422 | The part name is not one of the thirteen. The message lists them. |
| season_required | 422 | The route is stored per season and no season_id was given. |
| read_only | 405 | A method other than GET or HEAD. Refused before routing. |
| — | 429 | Over 120 requests a minute from one IP. |
Coverage, honestly
This is a cache, not a live proxy. Nothing you request reaches the upstream provider, so a request can never fail because they are down and can never spend their quota — and equally, it can never conjure data nobody has fetched yet.
Depth is uneven by design. A competition that has had a full-season pull holds thousands of fixtures; one only touched by the daily cross-league sync holds a handful. Two resources are declared but currently empty — league_top and league_bestxi — so /leagues/{id}/top and /leagues/{id}/best-xi answer 404 today. Check the coverage table before you build against a competition.