Skip to main content

Coverage is the same on every plan

Free, Pro, Ultra and Mega all call the same football endpoints against the same data feed. No competition, league or endpoint is restricted by plan — the only difference between plans is the daily request allowance: You can therefore validate coverage for your project on the Free plan before upgrading.

Current catalogue size

Verified live on 3 September 2026 by walking every category:
  • 6,473 football competitions across 272 country and regional categories (including international and continental groups, and the separate ... Amateur categories)
The catalogue changes as competitions are added and seasons roll over, so it is exposed as a live endpoint rather than a static list. Always read it from the API instead of hardcoding.
Use the category walk, not the flat list, when completeness matters. Verified 3 September 2026: GET /api/tournaments reports total: 4234, while walking GET /api/categories/{categoryId}/tournaments across all 272 categories returns 6,473 competitions. 2,244 competitions returned by the category endpoints are missing from the flat list — including England’s Premier League (17), Championship (18) and FA Cup (19), and most of Brazil, Turkey Amateur, Chile Amateur and Australia. The flat list is fine as a quick lookup, but build your competition universe from section 2. We are fixing the gap.

1. Full flat catalogue (partial — see warning above)

id here is the canonical uniqueTournament.id — store this, never the per-season tournament.id. See Canonical IDs.

Dump the catalogue to CSV

2. Browse country by country

2b. Amateur and lower tiers live in separate categories

Amateur and semi-professional competitions are not returned under a country’s main category. Each country has a distinct ... Amateur category:
Retry once on 503 / 504. Uncached calls to heavy endpoints can exceed the 30-second gateway timeout on the first request and succeed immediately afterwards. Verified 3 September 2026: /api/tournaments, /api/tournament/37/info and /api/tournaments/42/seasons each returned 504 cold and 200 in under a second on retry; /api/categories/1587/tournaments returned 503 once, then 200. In a full 272-category sweep, 5 categories failed on the first attempt and all 5 succeeded on retry. A rare variant returns 200 with an empty array (/api/tournaments/493/seasons → seasons: [], then 19 seasons on retry) — treat an unexpectedly empty list as a retryable response, not as “no coverage”.

Archive depth shrinks as you go down the pyramid

Breadth reaches district level, but historical depth does not. Verified live on 1 September 2026 via /api/tournaments/{id}/seasons:
There is no competition named “Hoofdklasse” in the feed — that level of the Dutch pyramid is expressed through the Vierde Divisie and the regional 1e Klasse divisions.
Lower-tier archives contain real data, not empty shells: a 2022/23 Derde Divisie season returns full /standings and paginated /events/last/{page} match lists.

Map archive depth for every league worldwide

Two calls give you an exact seasons-available table for the whole catalogue:
That is roughly 6,500 requests as a one-off — it fits inside a single Pro day (7,500/day).

Bulk import performance

Measured live on 4 September 2026 against production. Warm single-request latency is sub-second (/countries 0.49s, /categories/35/tournaments 0.34s, /tournaments/37/seasons 0.30s, season event pages 0.35–0.8s), so import speed is decided almost entirely by how many requests you run in parallel. Use 32 concurrent workers as the default, a 30-second timeout and one retry. 64 also ran clean with no 429s, but 32 leaves headroom for transient upstream slowness. A sequential importer is the usual cause of a multi-hour region sweep: the first (cold) hit on a heavy path can take 10–30 seconds, and a single-threaded loop pays every one of those serially. A second pass over the same region runs an order of magnitude faster because everything is warm. Further savings:
  • Season event pages already contain the full match row — homeTeam, awayTeam, homeScore/awayScore with period splits, status, startTimestamp, roundInfo, season, tournament. Do not call /api/match/{matchId} per fixture in a first-pass import.
  • 404 on the page after the last one is the normal end-of-pagination signal, not an error. Eredivisie 25/26 (season 77012) is 325 events in 11 requests at 30 per page; Regionalliga Nord 26/27 is 56 events in 2 requests.
  • Pull only the seasons you need. Lower tiers carry the current season only, so a current-season import is one /seasons call plus 1–2 event pages per competition.

Page size is fixed at 30

Verified 6 September 2026: events/last/{page} and events/next/{page} always return 30 events per page. ?limit=100 and ?pageSize=100 are ignored — both still return 30. There is no larger-page or bulk-season variant, so total request count is a function of fixture count. The lever is concurrency, not payload size.

Fetch a season’s pages in parallel

Pages within one season are independent, so do not walk them sequentially. Verified 6 September 2026 on Eredivisie 25/26 (tournament 37, season 77012): issuing pages 0–11 all at once returned the complete season — 11 populated pages plus the terminating 404 — in 2.18 seconds. Two ways to know how many pages to request:
season/{seasonId}/rounds is not a backfill route. Round pages hold far fewer events than paged season events — Eredivisie round 1 returns 9 events versus 30 per events/last page — so round-walking roughly triples your request count for the same data.
GET /api/schedule/{date} is not a bulk mirror of a day’s fixtures. Verified 4 September 2026: /api/schedule/2025-11-15 returns 450 events from only 50 competitions while the same response reports totalTournamentsOnDate: 1352. Never use it as a whole-world import route. GET /api/scheduled-tournaments/{date} does return the full set (1,352 entries for that date) and works well as a “which competitions played that day” filter to skip idle competitions.

3. Live and date-scoped competition filters

Reference IDs for major competitions

Depth varies by competition, not by plan

The competition list is identical everywhere, but how much data each competition carries differs: See Data Coverage Questions for the full breakdown.

Player statistics depth by tier

Measured 4 Sep 2026 on top-players (categories returned) and on /api/players/{playerId}/tournament/{id}/season/{seasonId}/statistics (fields returned): A season that has just started returns fewer entries per category rather than fewer categories (Bundesliga 26/27: 30 categories, 29 entries under goals). Only type=overall is served on these endpoints; home and away return 404/503 even though player-statistics/types lists them. Cold calls to season statistics paths can return 503/504 and succeed on retry — retry once before treating a competition as uncovered.

Premier League player-match depth by season

Measured 10 Sep 2026 on GET /api/match/{matchId}/lineups (union of statistic keys across all 40 players in a finished match) and GET /api/match/{matchId}/shotmap for the same match. One lineups request returns every player on both teams — per-player requests are never needed for player-match statistics. Fixtures, scores, lineups and events run back to 92/93 (13043) throughout. Current-season per-match fields include shooting (totalShots, onTargetScoringAttempt, shotOffTarget, blockedScoringAttempt, hitWoodwork), expected data (expectedGoals, expectedGoalsOnTarget, expectedAssists), chance creation (keyPass, bigChanceCreated, bigChanceMissed), passing splits, carries (ballCarriesCount, progressiveBallCarriesCount, totalBallCarriesDistance), defensive actions (totalTackle, wonTackle, interceptionWon, outfielderBlock, totalClearance, ballRecovery, errorLeadToAShot), duels (duelWon, duelLost, aerialWon, aerialLost), goalkeeping (saves, savedShotsFromInsideTheBox, goodHighClaim, punches, goalsPrevented) and physical metrics (kilometersCovered, metersCoveredRunningKm, metersCoveredHighSpeedRunningKm, metersCoveredSprintingKm, numberOfSprints, topSpeed). Match companions cost one request each: shotmap, incidents, statistics (team level), average-positions, graph. There is no heatmap endpoint (404). The 117-field season aggregate is not 117 additional primitives — much of it is percentages, per-90s, sums and splits derived from the per-match fields. Genuinely season-only fields are appearances, matchesStarted, cleanSheet, totwAppearances, card totals, penalty splits, errorLeadToGoal and dribbledPast.

Friendly match depth

Measured 10 Sep 2026 on four Arsenal preseason friendlies (Club Friendly Games, 2025 and 2026): 51–66 player fields per match plus a full shotmap of 19–29 shots, including passing, duel, defensive and physical metrics. The consistent gap is expected data — friendlies carry no player-level expectedGoals/expectedAssists and no xg/xgot on individual shots. Friendly coverage itself remains best-effort.
Endpoints without a documented equivalent — for example /api/categories or /api/unique-tournaments — are not part of the API and return 404. Use the paths listed above.

Marketing overview page

A plain-English summary of this coverage, including the per-tier archive table and pricing, lives on the public site at Football Stats API. Useful for sharing with non-technical stakeholders evaluating the API.

Stadium coordinates

Venue objects carry latitude/longitude, city, country and capacity. Verified 1 Sep 2026:
Three ways to reach coordinates:
Cache venue records locally. Stadium coordinates are static, so one lookup per venue is enough — never fetch them per match.

Venue availability by competition tier

Event-level venue (event.venue) is populated for higher tiers only. Measured 3 Sep 2026 on German competitions, page 0 of season events: At tiers without event-level venue data there is no alternative event → venue relation:
  • /api/match/:matchId/venue returns 404 (200 for Bundesliga, 404 for Regionalliga and Oberliga matches).
  • /api/tournament/:id/season/:sid/venues returns 404 (200 for 3. Liga, 404 for Regionalliga and Oberliga seasons).
  • There is no reverse index — /api/venue/:venueId/events/last|next/:page returns 404.
This is a data-availability limit, not a plan restriction. Recommended fallback:
  1. GET /api/match/:matchId — the event has no venue, but homeTeam.venue and awayTeam.venue are fully hydrated (name, city, country, capacity, venueCoordinates). One request covers both clubs.
  2. Or cache the ground per club via /api/teams/:teamId → team.venue, plus /api/venue/:venueId for the full record. Venue IDs resolve normally at these tiers.
  3. Record the provenance (for example venue_source = 'event' | 'home_club') and let event.venue win whenever it is present — it is authoritative and diverges from the home ground for neutral-venue ties (e.g. match 15632636 PSG vs Arsenal played at the Ferenc Puskás Stadium, not Parc des Princes).
Fallback completeness sampled on upcoming fixtures (12 home clubs per competition): Regionalliga Nord 12/12 and Regionalliga Bayern 12/12 with coordinates, Oberliga Baden-Württemberg 7/12 (6 with coordinates). Render missing grounds as unknown rather than guessing, and treat inferred venues as overridable — ground shares, stadium moves and weather relocations are not reflected in the club record.

Quota-efficient retrieval

Avoid polling per-match endpoints on a timer: use the date schedule for the day’s card, then the WebSocket for in-play changes.

Bulk import playbook

The cheapest way to build a full database is competition → seasons → paged season events, reading clubs, venues and results out of the event payloads instead of requesting each entity separately.

1. Resolve the competition once

Store uniqueTournament.id (season-independent). Never persist the per-season tournament.id — it is regenerated every year. Verified depth: 2. Bundesliga (44) exposes 19 seasons, UEFA Champions League (7) exposes 35.
No example: this endpoint returned HTTP 404 for our Bundesliga sample on 24 September 2026. It only returns data when the entity has it (for example knockout draws, penalties or media), so handle an empty or 404 response.

2. Page the season

30 events per page with a hasNextPage flag. A full 34-matchday league season is roughly 13 requests.

3. Harvest entities from the event payload

Each event carries the core relational model. Venue data needs one extra rule: season and round pages can omit venue even when the full match response has it, so hydrate GET /api/match/{matchId} before classifying a match venue as missing. Store venue provenance as event, home_club, or unknown. Never present homeTeam.venue as the confirmed match venue: neutral-site finals and relocated ties can differ from the home club’s ground. Historical venue coverage is incomplete even for major competitions. In a 16 September 2026 sample, 18/30 Champions League 1993/94 events and 20/30 from 1992/93 carried a venue, while the final in each season had no event venue and GET /api/match/{id}/venue returned 404.

4. Cups and international competitions

Same flow, plus one bracket call per season:

5. Play-offs

Check both shapes:
  • Additional rounds inside the same season — call /api/tournament/{id}/season/{seasonId}/rounds and import any rounds beyond the regular matchday count via the normal /events/... pages.
  • A separate competition — search the country/category for “relegation” or “promotion” (for example Ettan, Relegation/Promotion 11617, Bayernliga Relegation/Promotion 18788) and import it like any league.

6. Friendlies

Friendlies are modelled as ordinary competitions, not as a special endpoint: For a complete backfill, call /api/tournaments/{id}/seasons, then walk both /api/tournament/{id}/season/{seasonId}/events/last/{page} and /events/next/{page} until hasNextPage is false or the next page is empty/404. Seasons use calendar years. For one club, /api/teams/{teamId}/events/last|next/{page} is the most reliable cross-competition view because friendly and competitive matches use the same team ID. Do not use /api/schedule/{date} as the primary friendly importer. It is useful as a same-day cross-check, but it is not a complete worldwide fixture mirror. Use /api/newly-added-events as a lightweight new-fixture signal and hydrate the returned match IDs.

7. Logos — fetch once, cache forever

V2 image routes are restored as of 10 September 2026. You can now import logos via V2 (/v2/football/images/teams/{id}, /v2/football/images/tournaments/{id}) as well as the free V1 routes and the deep-coverage V6 filename-based route.
When V1 has no crest for a team it returns HTTP 200 with a fixed grey placeholder — the same 1,466-byte PNG every time. Detect it by body length or checksum, not status code, and retry those teams against the V6 route: V6 carries crests down to regional and amateur divisions. V6 logo fields are bare filenames — pass them through verbatim, including any !w80 suffix. V6 IDs are opaque strings, so match by team name plus country once and store the pairing.
Normalize country labels before matching. V3 and V6 return English country names or country IDs; a local-language value such as Duitsland, Frankrijk, Soedan or Tsjaad will not equal the API value directly. Query V6 first, then V3 for misses. V3 search is fuzzy, so reject every wrong-country result rather than accepting its first suggestion. Treat generic club names and V6 results with a null country as manual-review cases.
Logos are static. Download each one once, store it in your own object storage or CDN, and never re-request it.

Ongoing sync and change detection

Once the initial import is done, keep the local copy current from the same endpoints — there is no push feed, and no dedicated change route (updated-events, events/updated, changes and match/{matchId}/changes all return 404).

Every event carries a change stamp

Verified 10 September 2026. Each event object in season pages, /api/schedule/{date} and /api/match/{matchId} includes:
changeTimestamp is Unix epoch seconds and is 0 for fixtures that have never been touched (typical for scheduled matches). Store it per match and re-write a row only when the incoming value is greater than the stored one. The block ships inside the page payload you already fetch, so change detection costs no extra requests, and it works on old seasons too (1988/89 Eredivisie events carry full score-level change lists).

Newly added fixtures

Returns identifiers only — eventId, homeTeamId, awayTeamId, uniqueTournamentId — with no timestamps (57 entries when measured). Use it to discover cup draws and newly scheduled or rescheduled matches, then hydrate through the season pages or /api/match/{matchId}.

Detecting newly added historical seasons

Older seasons are appended to existing competitions over time, and they arrive with high season IDs rather than in chronological order — Eredivisie (37) 88/89–91/92 are 90654–90657 while 25/26 is 77012. Re-read the season list and diff the ID set:
Monthly is frequent enough. Do not infer “no new archive” from season ordering — always compare IDs.
Do not build the daily loop on /api/schedule/{date} alone — it is not a complete world mirror (213 events returned on 10 September 2026 while the same response reported a far larger totalTournamentsOnDate). It is a good same-day cross-check, not a source of truth.

Friendlies stay in sync the same way

Friendlies are ordinary competitions with one season per calendar year (853 has 17 seasons, 2010–2026), so events/next/{page} and events/last/{page} behave normally — season 87177 returned a full 30-event page of played matches and 10 upcoming fixtures. They also appear in /api/teams/{teamId}/events/last|next/{page} and in the date schedule, and they use the same stable team IDs as competitive matches (Arsenal 42 in both the Premier League and Club Friendly Games).

Import hygiene

  • Keep concurrency at 4–8 parallel requests and honour the ttl field in responses.
  • Retry once on an occasional 404 or 5xx — a single retry clears nearly all transient upstream blips.
  • Ask support for a temporary daily-limit lift before a large backfill; it reverts automatically when it expires.
Last modified on September 24, 2026