Three layers of detail
It helps to think of football statistics in three layers. The match layer covers what happened in one game: shots, possession, cards, substitutions, whatever the source records for that competition. The team layer gathers squads and availability. The player layer holds individual profiles. A good match page draws on all three, but each comes from a different feed with a different refresh rhythm.
Depth is not uniform. A top division may carry a rich set of match statistics while a lower league offers little beyond goals and cards. We pass on what the source supplies and do not fill gaps with estimates. Your interface should hide a statistics panel that has nothing to show rather than render rows of zeros, because a zero claims a measurement was taken.
Feeds for statistics work
Each of these is marked sample received in the catalogue: a response has returned, and final freshness, coverage and licensing qualification is pending.
- match-statistics, with leagueId: current match statistics for a competition, cached for about a minute.
- historical-statistics, with leagueId and date: statistics for matches on a past date.
- match, with id and league: a single match in detail. Use the string match identifier from your fixtures.
- predicted-lineups, with leagueId: expected starting line-ups before kick-off.
- squads, with leagueId: registered players per team.
- player, with playerId, and injuries: profile and availability context. Player profiles are not part of Starter.
Assembling a match detail page
A match detail page is a join. Start from the match identifier you already hold from fixtures. Request the single match feed with that id and its league. Before kick-off, add predicted line-ups for the league and pick out the two teams. After kick-off, take the match statistics feed for the league and select the same match by identifier. Every join uses string identifiers from the source; none uses team names.
Request league-wide feeds once and serve many match pages from that single response. If a round has ten matches, fetching match-statistics once and slicing it is ten times cheaper than requesting per match.
const base = "https://footballapi.net/api/v1/football";
const headers = { "X-API-Key": process.env.FOOTBALLAPI_KEY! };
export async function leagueStats(leagueId: string) {
const res = await fetch(`${base}/match-statistics?leagueId=${encodeURIComponent(leagueId)}`, { headers });
if (!res.ok) throw new Error(`stats ${res.status}`);
return res.json(); // keep the source structure; select by match id later
}
export async function oneMatch(id: string, league: string) {
const q = new URLSearchParams({ id, league });
const res = await fetch(`${base}/match?${q}`, { headers });
return res.ok ? res.json() : null;
}Predicted is not confirmed
The predicted-lineups feed is exactly what the name says: an expectation of who will start, published before the team sheet. It is useful for previews and for fantasy managers planning a week. It is not the confirmed starting eleven, and it is not a betting prediction about the result.
Label it clearly in your interface. Words such as expected line-up or predicted eleven are honest; presenting it as the team sheet is not. When the confirmed line-up becomes available in match detail, replace the prediction rather than mixing the two, and never leave a reader thinking a predicted player started when they did not.
Players across competitions
A player can appear in a league, a domestic cup and a continental competition in the same week. Use the source player identifier, kept as a string, to tie those appearances together. Names are risky keys for players even more than for teams: transliterations differ, shirt names differ from legal names, and two players can share a name in the same squad.
The updated-players feed lists recently changed profiles. Polling that and refreshing only the affected players is far cheaper than re-requesting every profile you hold each night.
Refresh rhythm by layer
Match statistics change during play, and the catalogue cache is about sixty seconds, so refreshing every minute or two during a followed match is sensible. Predicted line-ups shift as team news arrives, so a few refreshes on match day are enough. Squads and profiles move slowly and are cached for an hour; daily refreshes plus the updated feeds will keep them current.
Treat historical-statistics as archive data. Request a date once, store it, and do not ask again unless you have a reason to think it was corrected.
Presenting statistics responsibly
Numbers on a match page carry authority, so present them carefully. Show the label the source uses, or a clear translation of it, and avoid relabelling a statistic in a way that changes its meaning. A count of shots is not the same as shots on target, and possession expressed as a share of time is different from a share of passes. If you are not sure what a source attribute measures, leave it out until you are rather than guessing.
Comparisons across competitions need extra caution. Two leagues can record the same statistic with different definitions or completeness, so a ranking of players across leagues built from raw counts can mislead. If you publish such comparisons, say which competitions are included and that definitions may differ.
For injuries and squad availability, timing matters. The injuries feed is cached for around fifteen minutes and reflects what the source reports, which may lag a manager's press conference. Show when you last refreshed it, and do not present an absence as confirmed unless the source does.
Consider an example preview page for a weekend fixture. It might combine the scheduled kick-off from fixtures, both squads, the predicted eleven for each side, any reported injuries, and, after the final whistle, the match statistics. That is five or six requests per league per refresh, shared across every match page in the round. Built this way, a full weekend of previews and reports for five leagues costs a few hundred requests, comfortably inside Starter, while a player-led product with profiles and photos points towards Growth.
Storing statistics for later analysis
Analysts often want to look back across a season: which team created the most chances away from home, how a player's involvement changed after a tactical switch. The feeds give you match-level records; building season views is a storage question. Save each match statistics response keyed by match identifier and the time you fetched it, and keep the raw body alongside any columns you extract for querying.
Keeping the raw body matters because statistics can be revised after the final whistle, and because the attributes a source records can change from one season to the next. If you only keep extracted columns, a revision or a renamed attribute silently breaks your history. With the original saved, you can re-extract at any time.
Mark which snapshot is final in your own system, for example the first fetch made several hours after full time, and base season aggregates on those. Aggregates built from mid-match snapshots will double-count or under-count, and they are a frequent cause of totals that disagree with published figures.
Plan considerations
Starter at EUR 29 a month gives five selected leagues and 5,000 successful requests per UTC day, and includes league-scoped feeds such as match statistics, squads and predicted line-ups for those leagues. It does not include player or team profiles, head-to-head or images, so a statistics product with player pages and photos belongs on Growth at EUR 99 with 25,000 requests a day, or Pro at EUR 249 with 250,000. Both cover all available package leagues.
Extra successful requests cost EUR 0.002 each on the monthly EUR subscription; failed and denied requests are not billed. The safety ceiling is 100 requests per second per account. No free trial is offered and no VAT is currently charged.
Questions developers ask
- Are predicted line-ups the official team sheet?
- No. They are an expectation published before kick-off. Label them as predicted and replace them when confirmed detail is available.
- Why do some matches have fewer statistics?
- Depth depends on what the source records for each competition. We do not estimate missing values.
- How do I get statistics for one match?
- Request match-statistics for the league once and select the match by its string identifier, or call match with id and league.
- Is the player profile feed on Starter?
- No. Player and team profiles, head-to-head, injuries, the single-match service (which needs a static_id with its league) and images are not included on Starter.
- Can I get statistics for a past date?
- Yes, through historical-statistics with leagueId and date, where the source supplies them.
- Do you rename source fields?
- No. Attribute names, including at-sign prefixed ones, are kept as supplied.
Guides and documentation
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.