Skip to content
Standings

Historical football standings API for past league tables

Where a club finished in a given season is one of the most searched questions in football. The historical standings feed returns a supplied table for a competition and season. This page covers the parameters, how past tables differ from current ones, and how to build an archive that respects retrospective changes.

Primary endpoint

GET/api/v1/football/historical-standings

  • leagueId required, numeric string, e.g. 1204
  • season required, YYYY-YYYY

Access: All plans; Starter limited to your five saved leagues. Header: X-API-Key.

Illustration of framed league tables hanging in a row on a clubhouse wall, each frame a different season.
Each historical table is one competition and one season, framed and kept.
01

A table for a named season

Historical standings takes two required parameters. The leagueId is the positive numeric competition identifier, sent as a string. The season is two consecutive four-digit years joined by a hyphen, for example 2026-2027. Missing or malformed values produce a 400 response with the invalid_parameter code, which is not billed. On Growth and Pro, the season catalogue lists valid season values; Starter customers use the same format for their saved leagues.

The feed is cached for about an hour, though for a finished season the cache time is almost irrelevant. A past table should be requested once, checked and stored, then served from your own archive indefinitely.

02

What the response holds

Responses follow the standard envelope. The data object holds the table as the source supplies it, and meta records the feed, UTC fetch time, cache time and qualification. There is no normalised standings schema of ours. Team identifiers are strings and source attributes keep their at-sign prefix. Columns that appear in the current standings feed may be absent for older seasons, and the structure for a past season is not guaranteed to be identical to the current one, so map from samples of both.

A season may contain more than one table. Competitions with group stages, split seasons or separate opening and closing tournaments produce several tables, each with its own label. Your archive should store and display every table the response contains, with the labels supplied.

Season table archiveFlow: confirm the season value, request historical standings with leagueId and season, check each group and row, then store the final table permanently and serve from storage.01Season valueYYYY-YYYY02RequestleagueId + season03Check rowsgroups too04Archiveserve locally
Season table archive. Request each finished table once, check it, then serve it from your own archive.
03

When history was rewritten

Final tables are sometimes changed after the season. Points deductions for financial breaches, results overturned on appeal and expunged records when a club withdraws mid-season all alter a table after the last match. The historical table you receive reflects what the source holds, which may be the amended version rather than the table printed in newspapers at the time.

If your audience cares about that distinction, and fans of the affected clubs certainly do, add editorial notes alongside the supplied table. Do not alter the supplied numbers to match your memory of the original; present the data and the note separately.

  • Show supplied tables unaltered.
  • Add editorial notes for deductions or expunged records.
  • Keep the date you archived each table.
04

Archiving a run of seasons

The Python example archives a list of seasons for one competition, pausing between requests and writing each table to its own file. Run it once when you onboard a competition and again only if you learn of a correction. Keep the raw envelope; rendering choices can be revisited later without spending more requests.

Before publishing, check a few rows of each table against the season's results if you hold them, especially for older seasons where the source data may be thinner.

Python: archive final tables for several seasonspython
import os, json, time, requests

H = {"X-API-Key": os.environ["FOOTBALLAPI_KEY"]}
URL = "https://footballapi.net/api/v1/football/historical-standings"

for season in ["2023-2024", "2024-2025", "2025-2026"]:
    r = requests.get(URL, params={"leagueId": "1204", "season": season}, headers=H, timeout=30)
    body = r.json()
    if r.ok:
        json.dump(body, open(f"table-1204-{season}.json", "w"))
    else:
        print(season, body.get("code"), body.get("requestId"))
    time.sleep(1)
05

Archive pages readers return to

Season archive pages are long-lived content. A table from twenty years ago will be visited every time a club celebrates an anniversary or a pundit mentions a famous title race. Give each season a stable URL using the season value, render the table on the server so it loads quickly and is readable without scripts, and link to the previous and next seasons.

Small additions make archive pages much more useful: the champions and relegated clubs highlighted from positions supplied in the table, a link to that season's fixtures if you hold them, and a short note on the format if it differed from today.

06

Following a club through divisions

Club history pages often trace a club up and down a pyramid: champions of the second tier one year, mid-table in the top flight the next. Building that view means requesting tables for several competitions, each with its own leagueId, and finding the club in each by its team identifier. Because identifiers are stable strings across competitions in the same system, the join works without name matching.

Remember that every competition and season you add is another request, and on Starter only your five saved leagues are available. A full pyramid history usually belongs on Growth or Pro.

07

Comparing a club's finishes across seasons

A favourite feature on club sites is a timeline of league finishes. Building it means requesting the table for each season, finding the club by its team identifier in each, and recording its position. Because positions are supplied by the source, you avoid recomputing anything, but you do need to handle seasons in which the club played in a different competition. Store the competition identifier alongside each position, and draw the timeline in a way that makes division changes visible rather than plotting a third-place finish in the second tier as if it were the same as third in the top flight.

Where a season had groups or a split format, decide which table represents the club's finish and document the choice. A position in a relegation group is not comparable to a position in a single table.

08

Columns that come and go

Older tables can carry fewer columns than current ones. Goal difference might be present while goals for and against are not, or a column used in a particular era may have since disappeared. Build your table component to render whichever supplied columns exist for that season, with headings derived from what you find, rather than a fixed template that shows blanks. If you want a consistent layout across seasons, choose the smallest common set of columns and offer the full supplied table on request.

09

When the current season becomes history

At the end of each season, the current standings feed and the historical feed overlap. Before you archive the closing table from the historical feed, compare it with your last current-season snapshot. Small differences can be legitimate, such as a late deduction applied after the last match. Large differences usually mean you requested the wrong season string. Once you are satisfied, switch your archive page to the historical copy and stop polling the current feed for that season.

10

Testing archives

Keep one saved response per format you have encountered: a single table, a grouped season and a season with a split. Render each in tests. Archive pages change rarely, so regressions hide for months unless tests catch them.

11

Plan scope

Historical standings is league addressed. Starter at EUR 29 a month allows requests for your five saved leagues only. Growth at EUR 99 and Pro at EUR 249 cover all available package leagues. Archive depth varies by competition on every plan, and we do not promise a fixed number of seasons.

Archive builds are cheap because they happen once. Twenty seasons for five leagues is a hundred successful requests. Requests above the daily inclusion cost EUR 0.002 each on the monthly EUR invoice; failed and denied requests are not billed. No free trial is offered.

Questions developers ask

What parameters does historical standings need?
leagueId and season, where season is two consecutive four-digit years with a hyphen, such as 2026-2027.
Will a past table match what was printed at the time?
Not always. Tables can reflect later deductions or overturned results. Add editorial notes rather than altering supplied numbers.
Can one season return several tables?
Yes, for competitions with groups or split formats. Display every table with its supplied label.
How often should I request a past season?
Once, then store it. Re-request only if you learn of a correction.
Is historical standings on Starter?
Yes, for your five saved leagues.

Check the plan before you write the integration.

Starter EUR 29, Growth EUR 99, Pro EUR 249 per month. No free trial. No VAT currently charged.