Skip to content
League tables

Football standings API for current and past league tables

A league table looks simple and hides a lot: groups, phases, deductions and tie-breakers that differ by competition. This page covers the two standings feeds, how to render a table without recomputing it, and how to handle seasons that are already closed.

Primary endpoint

GET/api/v1/football/standings

  • leagueId required, numeric string, e.g. 1204

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

Illustration of stacked league table rows beside a trophy shape.
Standings are rendered as the source publishes them, including groups and phases.
01

Render the table you are given

Developers who have just pulled a season of results are often tempted to compute the table themselves. Three points for a win, one for a draw, sort by points then goal difference. It works for one league and quietly fails for the next, because competitions break ties differently. Some use head-to-head records first, some use goals scored, some apply points deductions decided off the pitch, and some split the season into phases with points carried or halved.

The standings feed returns the table the source publishes, positions included. Our advice is to display that order and those values, and to use your own calculation only as a sanity check that flags disagreements for a person to look at. When your sum and the source disagree, the explanation is usually a rule you did not know about rather than an error in the feed.

02

Two feeds, two jobs

Both standings feeds are marked sample received in the public catalogue, meaning a response has returned and final qualification of freshness, coverage and licensing is pending.

  • standings, with a required leagueId: the current table for one competition. The catalogue cache time is around fifteen minutes.
  • historical-standings, with leagueId and season: a table for a past season where the source supplies one. Cached for about an hour.
  • seasons: the directory of season values you pass to the historical feed. Not included in Starter's league-scoped set.
  • team, with a teamId: profile detail to enrich a table row. Profiles are not included on Starter.
Current and historical tablesFlow: seasons directory provides season values, historical-standings returns past tables, standings returns the current table, and your renderer shows every group.01seasonsvalid values02standingscurrent, leagueId03historical-standingsleagueId + season04Render groupssource order
Current and historical tables. Discover seasons, fetch the table you need, store a dated snapshot and render every group the response contains.
03

Fetching and storing a table

The Python snippet requests a current table and saves it with a timestamp. Saving snapshots is cheap and pays off: when a reader asks why their club dropped a place overnight, a snapshot from yesterday answers it faster than any log.

Keep team identifiers as strings and keep attribute names exactly as returned, including any with an at-sign prefix. Your table component can map those names to columns at render time. Do not build a normalised standings schema on top of the feed and then discard the original; the next competition you add may carry a column your schema has no room for.

Python: save a dated standings snapshotpython
import os, json, datetime, requests

r = requests.get(
    "https://footballapi.net/api/v1/football/standings",
    params={"leagueId": "1204"},
    headers={"X-API-Key": os.environ["FOOTBALLAPI_KEY"]},
    timeout=20,
)
r.raise_for_status()
stamp = datetime.datetime.utcnow().strftime("%Y%m%dT%H%M")
with open(f"standings-1204-{stamp}.json", "w") as f:
    json.dump(r.json(), f)
04

Groups, conferences and split seasons

Not every competition has one table. A cup with a group stage has several small tables. Some leagues split into a championship group and a relegation group partway through. Others run separate opening and closing tournaments in the same calendar year. Your rendering code should accept that one standings response can contain more than one table, and should label each one with whatever group or stage name the source provides rather than a guess.

Test your component against at least one competition with groups before you call it done. A table component written against a single twenty-team league tends to fall apart when it first meets four groups of four, usually by merging them into one sixteen-row list sorted by points, which is nonsense.

  • Iterate over every table in the response, not just the first.
  • Show the group or stage label the source supplies.
  • Do not assume a fixed number of rows per table.
  • Leave room for annotation such as a points deduction marker.
05

When to refresh

Tables change when matches finish, and occasionally when a disciplinary decision lands midweek. Polling standings every few seconds during a match is pointless because the gateway cache is around fifteen minutes and the source table usually updates after full time. A practical pattern is to refresh a league table a while after the last match of a round ends, and otherwise a few times a day.

For closed seasons, fetch the historical table once, check it, and store it. A finished season should not cost you requests every day.

06

Building season archives

Historical tables power some of the most-read pages on club and league sites: where did we finish ten years ago, who was relegated that season. Archive depth differs by competition and we do not promise a fixed number of years. Use the seasons directory to see what exists for a league before designing a page around a range that may not be available.

Be careful with retrospective changes. A past season table can differ from the one printed at the time if results were later overturned. Show what the feed returns and, where your editors know the history, add your own note.

07

Adding form and context around a table

Readers rarely look at a table in isolation. They want to know which side is in form, who plays next and how far the gap to safety is. You can build most of that context from feeds you already use. The fixtures feed for the same leagueId gives you each club's next match, joined on the team identifier. The recent-results feed lets you show the last few outcomes beside a row, again joined on identifiers rather than names.

Be careful with derived columns. A gap to the team above is simple arithmetic on supplied points, and safe. A projected final position is a forecast, and if you publish one you should label it as your own estimate and explain the method. Never present a projection in the same visual style as the official table, because readers will take it as fact.

Small details make table pages trustworthy: a visible last-updated time taken from when you fetched the snapshot, a short note when a deduction applies, and a link to the competition's fixtures. None of these needs extra requests; they come from the data you already hold and from saving it carefully.

Mobile layouts deserve thought too. A full table with every column is unreadable on a narrow screen, so choose a compact view that keeps position, club, played and points visible, and let readers expand a row for the rest rather than truncating values.

08

Checking a table before you publish it

A table is easy to sanity-check, and doing so catches integration bugs early. For each row, wins plus draws plus losses should equal matches played, and goals for minus goals against should equal goal difference, where the source supplies those columns. Every club in the competition should appear exactly once within its group. When a check fails, it usually points to a mapping mistake on your side, such as reading a column under the wrong attribute name, rather than to the source.

Run these checks automatically after each refresh and hold the previous snapshot on your site if a new one fails, alerting a person to look. That way a bad deployment does not put a broken table in front of readers on a Saturday evening, and your editors can see exactly which row and which rule triggered the warning.

09

Plan fit for table products

A five-league table widget fits comfortably on Starter: EUR 29 a month, five selected leagues, 5,000 successful requests per UTC day. Starter excludes profiles and the seasons directory, so a design that links each row to a team profile, or offers a season picker built from the directory, needs Growth at EUR 99 with all available package leagues and 25,000 requests, or Pro at EUR 249 with 250,000.

Successful requests beyond the inclusion cost EUR 0.002 each on the monthly EUR invoice; failed or denied requests are not billed. The account safety ceiling is 100 requests per second. There is no free trial.

Questions developers ask

Should I calculate the table from results?
Use the source table for display. Your own calculation is useful as a check, but tie-break rules and deductions vary by competition.
Can one standings response contain several tables?
Yes, for competitions with groups or phases. Render every table and keep the source labels.
How do I request a past season?
Call historical-standings with leagueId and season. Find valid season values in the seasons directory.
Does Starter include team profiles for table rows?
No. Profiles are not included on Starter. Growth and Pro include the team feed.
How often does a table update?
Usually after matches finish. The gateway cache for standings is around fifteen minutes, so faster polling does not help.

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.