惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Last Week in AI
Last Week in AI
D
DataBreaches.Net
腾讯CDC
Recent Announcements
Recent Announcements
有赞技术团队
有赞技术团队
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
云风的 BLOG
云风的 BLOG
罗磊的独立博客
月光博客
月光博客
MyScale Blog
MyScale Blog
U
Unit 42
Martin Fowler
Martin Fowler
Stack Overflow Blog
Stack Overflow Blog
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
G
Google Developers Blog
博客园 - 【当耐特】
D
Docker
I
InfoQ
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I Built a Free World Cup 2026 Live Scores API on Apify — ...
Fatih İlhan · 2026-06-18 · via DEV Community

Fatih İlhan

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: stats is always null. 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.