Skip to content
Fixtures and competitions

Historical football fixtures API for past seasons

Past seasons power archive pages, research projects and season-on-season comparisons. The historical fixtures feed returns a supplied season of fixtures and results for one competition. This page covers the two required parameters, how to build an archive that is requested once and kept, and how to treat the gaps that old data inevitably has.

Primary endpoint

GET/api/v1/football/historical-fixtures

  • 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 an open archive box holding fixture cards, with a faded football pitch outline behind it.
Historical fixtures are requested once per competition and season, then archived on your side.
01

Two parameters, both required

Historical fixtures takes leagueId and season, and both are required. The leagueId is the same positive numeric identifier you use for the current-season fixtures feed, sent as a string. The season is two consecutive four-digit years joined by a hyphen, such as 2026-2027. A request missing either value, or with a season in any other shape, is rejected with a 400 response and the invalid_parameter code, and is not billed.

The pairing matters because a competition identifier alone is ambiguous across time. The same league can span many seasons, sometimes with different formats, different numbers of clubs and, occasionally, a new name. Asking for a specific season keeps every response anchored to one campaign. For valid season values, the season catalogue is the published lookup on Growth and Pro; Starter customers can use the same format for their saved leagues.

02

What a historical response contains

The response follows the gateway's standard envelope. The data object holds fixtures and results for that season as the source supplied them, and meta records the feed, the UTC fetch time, the cache time of about an hour and the qualification state. We do not publish a normalised schema for historical fixtures, and the structure is not guaranteed to be identical to the current-season feed in every detail. Read a sample from your own account and map from that.

Match identifiers in historical responses are the original source identifiers, kept as strings. That is what lets you connect an archived fixture to anything else you hold for the same match, such as statistics you stored at the time. If you ever renumber matches in your own database, keep the source identifier as a separate column so those joins remain possible.

Requesting and archiving a past seasonFlow: choose a leagueId and a season string, request historical fixtures, validate the response and store it permanently, then build archive pages from storage.01leagueIdstring ID02seasonYYYY-YYYY03Validatecheck gaps04Archiveserve from storage
Requesting and archiving a past season. Two required parameters, one request per season, and a permanent copy on your side.
03

Living with gaps in old data

Historical data is rarely as rich as current data. Older seasons may lack kick-off times, venues or some team details, and a match that was abandoned and replayed might appear in a way that differs from how a modern match would. None of this is corrected or filled in by us; you receive what the source has.

Decide in advance how your interface shows absence. A missing kick-off time should read as unknown rather than midnight. A missing venue should leave the line out rather than say TBC, which implies it will be confirmed. A result that looks implausible deserves a check against another reputable reference before you publish commentary on it.

  • Render missing historical values as unknown, never as zero or a default time.
  • Keep the season string with every stored match for clean grouping.
  • Note format changes, such as split seasons, in your archive copy.
04

An archive job that runs once

The TypeScript example archives a list of seasons for one competition, waiting between requests and skipping seasons already stored. Run it when you onboard a competition, then rarely again. A finished season does not need daily refreshing, and every successful request counts against your inclusion whether or not the data changed.

The pause between requests is a courtesy to your own budget and to the 100 requests per second account safety ceiling. Archive jobs are where accidental bursts usually happen, because a loop with no delay can fire hundreds of requests in a moment.

TypeScript: archive selected seasons oncetypescript
const base = "https://footballapi.net/api/v1/football/historical-fixtures";
const headers = { "X-API-Key": process.env.FOOTBALLAPI_KEY! };
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

export async function archive(leagueId: string, seasons: string[]) {
  for (const season of seasons) {
    if (await alreadyStored(leagueId, season)) continue;
    const q = new URLSearchParams({ leagueId, season });
    const res = await fetch(`${base}?${q}`, { headers });
    const body = await res.json();
    if (res.ok) await store(leagueId, season, body); // keep data and meta
    else console.warn(season, body.code, body.requestId);
    await sleep(500);
  }
}
05

What teams build with past fixtures

Club history pages are the obvious use: every result against every opponent across many seasons, built from archived fixtures joined on team identifiers. Researchers use the same data to study home advantage, scheduling congestion or the effect of fixture order on a title race. Publishers use it for on-this-day features and anniversary pieces.

Each of these benefits from storing results season by season with their original identifiers. A research dataset built that way can be rebuilt or extended without re-requesting anything, and a history page can be regenerated if you change your design. For head-to-head summaries between two specific clubs, the dedicated head-to-head feed on Growth and Pro may save you assembling the record yourself.

06

Joining archives to current data

The real value of an archive appears when it meets current data. A club page that shows this season's fixtures alongside every previous meeting with the same opponent needs both sets joined on the same team identifiers. Because both feeds use source identifiers kept as strings, that join is straightforward, provided you never replaced them with your own numbering. Be cautious about clubs that have been reformed, merged or renamed. The source may treat a reformed club as a new team with a new identifier, and it is an editorial decision, not a data one, whether your product presents the two as one history. Record that decision explicitly in your mapping rather than quietly merging records.

07

Checking an archived season

Before publishing an archived season, run a few cheap checks. Count the matches and compare against the number you would expect from the competition format. Confirm every club appears the expected number of times at home and away. Look for duplicate match identifiers, which can indicate a replay recorded alongside the original, and decide how to present it. Where a season has fewer matches than expected, check whether it was curtailed, which happened to several competitions in recent memory, before assuming data is missing. These checks take seconds and catch most problems before readers do.

08

Building an archive once

Past seasons rarely change, so request each one once, store it, and serve archive pages from your copy. Re-request occasionally to pick up source corrections, and log differences when they appear. That approach keeps archive requests close to zero after the initial build.

When you publish an archive, show the season string and the date of your last refresh, so readers know the record reflects the source at that time.

09

Testing past-season requests

Test your season helper against saved responses, and assert that stored fixtures keep their original identifiers so archive links remain stable across refreshes.

10

Errors on archive requests

A 400 invalid_parameter response usually means a malformed season string; check it against the seasons directory. A 404 can mean the source holds no fixtures for that competition and season. Log the requestId from each error body, fix the request and retry once rather than looping.

11

Plan scope

Historical fixtures is a league-addressed feed. On Starter, at EUR 29 a month, requests must use one of your five saved leagues; a leagueId outside your selection is refused. 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 minimum number of seasons.

Because archives are requested once, they cost little. Ten seasons for five leagues is fifty successful requests, a tiny fraction of the Starter inclusion of 5,000 per UTC day. There is no free trial, and no VAT is currently charged.

Questions developers ask

Which parameters are required?
Both leagueId and season. The season must be two consecutive four-digit years with a hyphen, such as 2026-2027.
Can Starter request historical fixtures?
Yes, for the five saved leagues only. A leagueId outside the selection is refused.
How far back does the archive go?
It varies by competition. Check the season catalogue and sample before promising a range to readers.
Are historical match IDs the same as the original ones?
They are the source identifiers as supplied, kept as strings, which is what makes joins to other stored data possible.
Should I refresh past seasons regularly?
No. Request each finished season once and store it. Re-request only if you have a reason to expect a correction.
Will missing fields be filled in?
No. Gaps are passed through as supplied. Show them as unknown in your interface.

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.