The World Cup is here and getting clean, structured match data is surprisingly painful. The official FIFA site has no public API. Third-party services either cost money, require OAuth, or return inconsistently shaped JSON that changes mid-tournament.
I spent a few hours building a free Apify actor that wraps the football-data.org v4 API and outputs clean, normalized JSON — ready to drop into a dashboard, a Discord bot, or an automated betting tool. Here's how it works and how to use it.
What it returns
Every run pushes records to an Apify dataset. A match record looks like this:
{
"fetchedAt": "2026-06-18T12:00:00Z",
"mode": "match",
"matches": [
{
"id": 415082,
"utcDate": "2026-06-11T20:00:00Z",
"status": "completed",
"stage": "Group Stage",
"group": "A",
"matchday": 1,
"homeTeam": { "id": 772, "name": "Mexico", "code": "MEX", "crest": "https://..." },
"awayTeam": { "id": 773, "name": "United States", "code": "USA", "crest": "https://..." },
"homeScore": 2,
"awayScore": 1,
"halfTimeHomeScore": 1,
"halfTimeAwayScore": 0,
"goals": [
{ "minute": 23, "type": "REGULAR", "team": "home", "player": "Raúl Jiménez", "assist": null }
],
"bookings": [
{ "minute": 55, "team": "away", "player": "Tyler Adams", "card": "YELLOW" }
],
"venue": "Estadio Azteca",
"stats": null
}
]
}
Four modes are available:
| Mode | What you get |
|---|---|
live |
All in-progress matches right now |
standings |
Full group stage table |
match |
Single match with goals + bookings |
full |
All completed matches (filterable by team) |
Note:
statsis alwaysnull. Possession, shots, corners — these aren't available on football-data.org's free tier. I set the field to null explicitly rather than defaulting to 0, so consumers can handle the absence cleanly.
The interesting parts
Rate limiting done right
football-data.org's free tier caps you at 10 requests per minute. Naive polling will get you 429s. The client enforces a 6-second gap between every request using a timestamp-based throttle:
private async throttle(): Promise<void> {
const now = Date.now();
const elapsed = now - this.lastRequestAt;
if (this.lastRequestAt > 0 && elapsed < THROTTLE_MS) {
await sleep(THROTTLE_MS - elapsed);
}
this.lastRequestAt = Date.now();
}
If a 429 slips through anyway (burst from a previous run), the retry loop adds an extra 6s on top before trying again:
if (status === 429) {
await sleep(THROTTLE_MS);
}
4xx responses that aren't 429 bail out immediately — no point retrying a bad request.
Normalizing the raw API shape
football-data.org returns statuses like FINISHED, IN_PLAY, TIMED, PAUSED. I normalize these to four clean consumer-friendly values:
const STATUS_MAP: Record<string, string> = {
FINISHED: 'completed',
IN_PLAY: 'in_progress',
PAUSED: 'in_progress',
SCHEDULED: 'scheduled',
TIMED: 'scheduled',
AWARDED: 'completed',
};
Groups come back as GROUP_A, GROUP_B, etc. A quick regex strips the prefix:
function mapGroup(raw: string | null | undefined): string | undefined {
if (!raw) return undefined;
const match = raw.match(/^GROUP_([A-Z])$/);
return match ? match[1] : undefined;
}
Goals and bookings reference the scoring team by team.id. To label them as home or away, I compare against homeTeam.id — no string matching, no locale issues:
team: g.team.id === homeId ? 'home' : 'away',
How to use it
1. Get a free API key
Sign up at football-data.org — takes 30 seconds, no credit card.
2. Run the actor
Go to apify.com/seralifatih/wc2026-stats and hit Try for free.
Input:
{
"footballDataApiKey": "your_key_here",
"mode": "live"
}
Or via the Apify API:
POST https://api.apify.com/v2/acts/seralifatih~wc2026-stats/runs?token=YOUR_APIFY_TOKEN
Content-Type: application/json
{ "footballDataApiKey": "your_key_here", "mode": "live" }
Results land in the run's default dataset as individual JSON items.
3. Schedule it for live match days
Set up an Apify Scheduler with cron */2 * * * * in live mode. Each run pushes only currently in-progress matches. Your dataset stays fresh without polling from your own server.
The World Cup runs June 11 – July 19, 2026. Only enable the scheduler on match days to avoid burning free compute.
4. Filter by team
Pass a teamId with mode: full to get only a specific country's completed matches:
{ "footballDataApiKey": "your_key_here", "mode": "full", "teamId": 773 }
Useful for single-country dashboards or per-team analytics.
Tech stack
- Node.js 20 + TypeScript — async pipeline, typed throughout
- Apify SDK v3 — dataset push, input/output schema, proxy support
- axios — HTTP client with timeout and retry logic
- football-data.org v4 — the underlying data source (free tier)
Source: github.com/seralifatih/wc2026-stats
What are you building with World Cup data? Drop it in the comments.
Top comments (0)