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.

GET/events?league_id=1&per_page=2
{
    "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.

GET
/

Service document: version, endpoint index, event parts and totals.

GET
/health

Liveness, object count, and the timestamp of the most recent write.

GET
/resources

Every resource with its group, row count and last fetch.

GET
/stats

Rows written per resource per day.

days

GET
/docs

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.

GET
/leagues

All competitions with coverage counts.

country is_women has_events page per_page sort dir q

GET
/leagues/{id}

One competition with its stored detail payload.

GET
/leagues/{id}/seasons

Season list, current season marked.

GET
/leagues/{id}/standings

The league table. Defaults to the current season.

season_id

GET
/leagues/{id}/events

Fixtures for the competition.

season_id status team_id team date_from date_to page per_page sort dir q

GET
/leagues/{id}/top

A leaderboard for the competition.

stat season_id

GET
/leagues/{id}/venues

Grounds used in the competition.

GET
/leagues/{id}/best-xi

Season Best XI. Stored per season, so season_id is required.

season_id

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.

GET
/events

Fixture list across competitions, chronological.

league_id season_id status team_id team date_from date_to page per_page sort dir q

GET
/events/live

Fixtures currently in play.

league_id season_id team_id team page per_page sort dir q

GET
/events/{id}

One fixture with every stored part.

include parts_only

GET
/events/{id}/{part}

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.

GET
/standings

Stored league tables. Rows are summaries; include_table embeds the table itself.

league_id include_table page per_page sort dir q

GET
/event-stats

Match statistics and xG payloads, with the fixture attached.

event_id league_id date_from date_to page per_page sort dir q

GET
/odds

Consensus odds per fixture.

event_id league_id date_from date_to page per_page sort dir q

GET
/predictions

Stored model predictions.

event_id league_id date_from date_to page per_page sort dir q

Raw store

The stored payload, untouched, for any resource. Use this when the shaped endpoints are not the shape you want.

GET
/datasets

Resource index with counts.

GET
/datasets/{resource}

Stored payloads for one resource.

ref parent_ref league_id country page per_page sort dir q

GET
/datasets/{resource}/{ref}

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.

PartResourceWhat 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

NameTypeNotes
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.

404/events/1/stats
{
    "error": {
        "code": "part_not_stored",
        "message": "No stored 'stats' for event '1'."
    },
    "meta": {
        "version": "v1",
        "generated_at": "2026-09-03T10:52:47+00:00"
    }
}
CodeHTTPMeaning
event_not_found404Nothing is stored for that fixture id — not "the fixture does not exist".
part_not_stored404The part name is valid, but that fixture has no such part stored yet.
unknown_part422The part name is not one of the thirteen. The message lists them.
season_required422The route is stored per season and no season_id was given.
read_only405A method other than GET or HEAD. Refused before routing.
429Over 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.

Open the playground