Why we serve no table rather than half a season

8 September 2026 · 5 min read

Our source keeps roughly two years of matches. Ask for anything older and it is not an error, it is an empty window. That creates a specific trap, and the honest answer to it is to serve less than we could.

The trap

Suppose we hold a season from January onwards. Real matches, real scores, real scorers. We could compute a table from them and return it. Every field would be populated. Nothing would look broken.

It would also be wrong. Not approximately wrong: a club that led in December and drifted after January appears mid-table, on a points total that matches no published table anywhere. A striker who scored twelve before Christmas appears with three.

Missing data announces itself. Wrong data does not. Someone building on that table gets numbers that are plausible, internally consistent and false, with no error to catch.

The rule

A season is served in full or not at all. On every recomputation we check whether the earliest match we hold is the opening round:

select min(round_num) from matches
where season_id = $1
  and (stage = 'league-phase' or (stage is null and coalesce(stage_num, 1) = 1))

If that is 1, we hold the season from the start and everything derived from it is sound. If it is 14, thirteen rounds are missing, and the standings, player statistics and team statistics for that season are deleted rather than published.

The competition stays. Its current season is untouched. What goes is the claim to know how a past season went when we only saw the end of it.

What you get instead

A season we do not hold in full is absent from the competition's season list, and asking for it by name returns an error that says so:

{
  "error": {
    "code": "invalid_season",
    "message": "season '2024-25' is not available for eredivisie"
  }
}

An error you can branch on beats a table you cannot trust.

What it costs us

Several competitions we hold from part-way through 2024-25 and do not serve at all. That is a season of history per competition that we could advertise and do not, on listings where a competitor's comparison table will show a bigger number than ours.

The trade is worth stating plainly. The reason to buy data rather than scrape it is that somebody has checked it. A provider who publishes a half-season table because the column was there is not doing that job, and you cannot tell which kind you bought until something downstream is wrong.

What we do serve

The current season for every competition, and the one before it wherever we hold it from round one. That is what each competition's season list reports, and it is the truth rather than a ceiling we advertise and fall short of.

More

Head-to-head records, and what they are actually good for

They settle ties in three leagues and fill every match preview. Tested as a predictor on 3,042 league matches, this season's form beat them.

How not to burn your free tier

Most quota goes on requests that could not have returned anything new. Read the cache headers you are already being sent, stop polling faster than the data moves, and know which plan the thing you are building actually needs.

Two teams, same points: every league breaks the tie differently

Goal difference decides it in England, Germany and France. In Spain, Italy, Portugal and Turkey the head-to-head record comes first. Sort a table yourself and you have it wrong for four of the twelve.

Build a live football scoreboard

A working scoreboard in about 60 lines: what /matches/live returns, why you poll every 30 seconds and not faster, and why your API key must never reach the browser.

What's inside a match: lineups, events and ratings

A field-by-field walk through a match detail response: formations, starting elevens, the event timeline, and how to rebuild a goal-by-goal summary from it.

Try it

Every endpoint mentioned here is in the reference, and every listing has a free tier. Start on the front page.