SOCCER DATA

How to get live football scores from an API

Call Soccer Data's /matches?status=live for every match in play, then poll /matches/{match_id}/live-stats for the score, clock and team stats.

· 8 min read

To get live football scores from an API, call Soccer Data's /matches endpoint with status=live: it returns every match in play with its score and clock. For one match, poll /matches/{match_id}/live-stats for the score, minute and running team stats, /live-timeline for goals, cards and substitutions, and /live-player-stats for each player's numbers. On paid plans (Pro and Business) live data refreshes every 15 seconds, so polling every 15 to 30 seconds keeps a scoreboard current.

Soccer Data is a read-only REST API: every request is a GET with your key in the X-Api-Key header, and every answer is JSON. Keep the key on your server.

Step 1: list the matches in play

curl "https://api.soccer-data.co/v1/matches?status=live&per_page=100" \
  -H "X-Api-Key: YOUR_API_KEY"

The list comes 20 per page by default and up to 100. To watch one league or one team, add competition_id or team_id. For the Premier League:

curl "https://api.soccer-data.co/v1/matches?status=live&competition_id=competition_705uv6jm8myn" \
  -H "X-Api-Key: YOUR_API_KEY"

Each live match carries a score and a live object with the clock:

{
  "id": "match_0i2m68v7oaxa",
  "status": "live",
  "home_team": { "id": "team_7p28lyz7ql3i", "name": "West Ham United" },
  "away_team": { "id": "team_v3nsjaq0vsxs", "name": "Queens Park Rangers" },
  "score": { "home": 0, "away": 1 },
  "live": {
    "match_status": "first_half",
    "elapsed_minutes": 29,
    "period": "first_half",
    "updated_at": "2026-10-09T19:29:14.093Z"
  },
  "live_odds_available": true
}
Field Meaning
live.match_status not_started, first_half, halftime, second_half, extra_time, penalties, finished, suspended or in_progress.
live.elapsed_minutes Minutes since kickoff, counted straight through both halves. Stoppage time isn't split out.
live.updated_at When the clock last moved. If it stops moving, updates may have stalled.

live is null for any match that isn't in play, and its fields can be null in the first moments after kickoff, so check before you display them. Refresh this list every minute or two to pick up new kickoffs and final whistles.

Step 2: poll the live endpoints for a match

Endpoint What it returns
/matches/{match_id}/live-stats Score, clock and running team stats.
/matches/{match_id}/live-timeline Goals, cards, substitutions and other events so far. Filter with event_type, period or team_id.
/matches/{match_id}/live-player-stats Each player's stats so far. Filter with player_ids.
/matches/{match_id}/odds/live In-play prices when live_odds_available is true. Needs Pro or Business.

Live score, clock and team stats

curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-stats" \
  -H "X-Api-Key: YOUR_API_KEY"
{
  "data": {
    "match_id": "match_0i2m68v7oaxa",
    "meta": {
      "match_status": "first_half",
      "elapsed_minutes": 29,
      "home_goals": 0,
      "away_goals": 1,
      "period": "first_half"
    },
    "stats": {
      "ball_possession": { "all": { "home": 55, "away": 45 } },
      "shots_on_target": { "all": { "home": 0, "away": 1 } },
      "expected_goals": { "all": { "home": 0.06, "away": 0.85 } }
    }
  }
}

meta.home_goals and meta.away_goals are the goals so far. Each stat under stats has a home and away value for the match so far, with the same names as the full-match stats endpoint. Which stats appear depends on the match, so check that a key exists before you read it.

Live timeline: goals, cards and substitutions

curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-timeline?event_type=goal,penalty_scored,own_goal" \
  -H "X-Api-Key: YOUR_API_KEY"

Events come in order, each with sequence, minute, extra_time, period, type, team and player. Three details matter when you count goals:

meta.last_updated says when the events last changed. If events aren't covered for a competition, coverage is none and meta.reason is not_supported.

Live player stats

curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/live-player-stats?player_ids=player_5lxcg0mhwqx3" \
  -H "X-Api-Key: YOUR_API_KEY"

Leave out player_ids to get every player. Each entry has player_id, player_name, team_id, position, rating, minutes_played and started, plus grouped counts in passing, shooting, duels, defending, goalkeeping and general. shooting includes expected_goals and expected_assists. rating and the xG fields can be null.

Lineups before kickoff

/matches/{match_id}/lineups works before the match starts:

curl "https://api.soccer-data.co/v1/matches/match_0i2m68v7oaxa/lineups" \
  -H "X-Api-Key: YOUR_API_KEY"

The type field tells you which lineup you have: predicted before the team sheet is out, team_sheet once the official XI arrives (usually about an hour before kickoff), and played once the match starts. confirmed is true for the team sheet, and bench_available turns true once substitutes are listed. Late changes keep coming until kickoff, so fetch again close to the start. You get a 404 while no lineup exists yet.

How often to poll

Live answers are cached on our side, for a length of time that depends on your plan:

Plan Live data normally refreshed every
Free 5 minutes
Pro and Business 15 seconds

Polling faster than that returns the same answer again, and each call still counts as a request. The X-Cache response header shows what you got:

X-Cache Meaning
HIT Served from the cache.
MISS Fetched fresh.
STALE An older copy, served because a fresh one couldn't be fetched. X-Cache-Age gives its age in seconds; for live data it can be up to 15 minutes.

Check X-Cache-Age before you show a stale score as current.

Budget before you build. Polling one endpoint every 15 seconds through a two-hour match is about 480 requests; every 30 seconds halves that. Compare that with your plan's limits on Rate limits, poll only the matches someone is watching, and use only the endpoints you need: for a scoreboard, live-stats alone is enough. When you get a 429, wait the number of seconds in Retry-After.

Before kickoff and after full time

The live endpoints only answer while a match is in play:

When live-stats live-timeline, live-player-stats
Before kickoff 409 409
In play 200 200
After full time 200 with match_status: "finished" for up to 5 minutes, then 409 409

The 409 has the code CONFLICT and the message "Match is not live". Treat it as a signal to stop or switch endpoints. Once the match is over, use /matches/{match_id}/stats, /player-stats and /timeline, which return 409 while the match is still live.

A polling loop in JavaScript

This follows every live Premier League match until full time. It needs Node 18 or later, saved as a .mjs file, and runs on your server:

const API = "https://api.soccer-data.co/v1";
const headers = { "X-Api-Key": process.env.SOCCER_DATA_KEY };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function follow(matchId) {
  while (true) {
    const res = await fetch(`${API}/matches/${matchId}/live-stats`, { headers });

    if (res.status === 409) return; // not live any more

    if (res.status === 429 || res.status === 503) {
      await sleep(Number(res.headers.get("Retry-After") ?? 30) * 1000);
      continue;
    }

    if (!res.ok) throw new Error(`live-stats failed with ${res.status}`);

    const { meta } = (await res.json()).data;
    const stale = res.headers.get("X-Cache") === "STALE"
      ? ` (stale, ${res.headers.get("X-Cache-Age")}s old)`
      : "";
    console.log(`${matchId} ${meta.elapsed_minutes ?? "-"}' ${meta.home_goals ?? 0}-${meta.away_goals ?? 0}${stale}`);

    if (meta.match_status === "finished") return;

    await sleep(30_000);
  }
}

const list = await fetch(
  `${API}/matches?status=live&competition_id=competition_705uv6jm8myn&per_page=100`,
  { headers },
);
const { data: matches } = await list.json();
await Promise.all(matches.map((match) => follow(match.id)));

Each match you follow adds its own requests, so keep the filter tight and stay within your per-minute limit.

Next steps

Frequently asked questions

Is there an API for live football scores?

Yes. Soccer Data is a REST API that returns every match in play from GET /matches?status=live, with the score and match clock for each one. For a single match, GET /matches/{match_id}/live-stats adds running team stats such as possession, shots and expected goals.

How often do live football scores update in Soccer Data?

On Soccer Data's paid plans, Pro and Business, live data refreshes every 15 seconds. Polling every 15 to 30 seconds is enough, because calling more often returns the same cached answer and still counts as a request.

How do I get goals, cards and substitutions as they happen?

Poll Soccer Data's GET /matches/{match_id}/live-timeline endpoint while the match is in play. To catch every goal, ask for event_type=goal,penalty_scored,own_goal, and leave out penalty_scored events whose period is penalties, because those are shoot-out kicks.

Can I get football lineups before kickoff from an API?

Yes. Soccer Data's GET /matches/{match_id}/lineups returns a predicted XI before the team sheet is out, then the official team sheet, which usually arrives about an hour before kickoff. The type and confirmed fields tell you which one you have.

Why does a live endpoint return 409?

Soccer Data's live endpoints return 409 with the code CONFLICT when the match isn't in play, either before kickoff or after full time. After the match, switch to the full-match endpoints /matches/{match_id}/stats, /matches/{match_id}/player-stats and /matches/{match_id}/timeline.

Does Soccer Data have live betting odds?

Yes, on the Pro and Business plans. When a match has live_odds_available: true, GET /matches/{match_id}/odds/live returns in-play prices per bookmaker, and it returns 404 when no in-play prices exist.

Try it with your own key

Fixtures, live scores, stats, xG, standings and odds for 1,100 competitions, in one REST API.

More guides