Contents
Understanding how qualifying competitions are structured
Before touching a single endpoint, it helps to understand the data hierarchy Sportmonks uses for tournaments like World Cup qualifying:

This is different from a simple domestic league where you have one stage, and everything lives in clean matchweeks. World Cup qualifying, especially UEFA, has distinct stages with their own standings and rules. A Group Stage produces a standings table. A Play-offs stage does not. You need to know which is which before requesting standings, or you’ll get empty arrays back and wonder what went wrong.
The six confederations for the 2026 World Cup qualify through their own separate leagues in Sportmonks:
Finding your league IDs: Use the ID Finder in your Sportmonks dashboard, or call GET /v3/football/leagues/search/World Cup Qualifying to retrieve the league IDs for each confederation. We’ll reference them as {UEFA_LEAGUE_ID}, {CONMEBOL_LEAGUE_ID}, etc. throughout this guide; swap these with your verified values before going to production.
Step 1: Find the active qualifying season
Every league has an active season. For the 2026 World Cup cycle, you want is_current: true. Use the currentSeason include on the leagues endpoint to get the season ID in a single call:
const API_TOKEN = 'YOUR_API_TOKEN';
const BASE_URL = 'https://api.sportmonks.com/v3/football';
async function getQualifyingSeason(leagueId) {
const url = `${BASE_URL}/leagues/${leagueId}?api_token=${API_TOKEN}&include=currentSeason`;
const res = await fetch(url);
const { data } = await res.json();
const season = data.currentSeason;
console.log(`Season: ${season.name}, ID: ${season.id}, Active: ${season.is_current}`);
return season;
}
// Example: UEFA World Cup Qualifying
getQualifyingSeason('{UEFA_LEAGUE_ID}');
Example response (trimmed):
{
"id": "{UEFA_WCQ_SEASON_ID}",
"name": "2024/2025",
"is_current": true,
"starting_at": "2024-09-05",
"ending_at": "2025-11-18",
"finished": false
}
Save that season.id, you’ll use it in nearly every subsequent call.
Step 2: Discover the stages
This is the step most developers skip, and it causes problems later. World Cup qualifying seasons have multiple stages, each with its own rules. Pull them all before building anything else:
async function getSeasonStages(seasonId) {
const url = `${BASE_URL}/stages?api_token=${API_TOKEN}&filters=stageSeasons:${seasonId}`;
const res = await fetch(url);
const { data } = await res.json();
data.forEach(stage => {
console.log(`Stage: "${stage.name}" | ID: ${stage.id} | Finished: ${stage.finished} | Current: ${stage.is_current}`);
});
return data;
}
For UEFA qualifying, you’d typically get back something like:
Stage: "Group Stage" | ID: {GROUP_STAGE_ID} | Finished: true | Current: false
Stage: "Play-offs" | ID: {PLAYOFFS_STAGE_ID} | Finished: false | Current: true
This tells you immediately which stage to query for group standings (the Group Stage) and which to query for fixture results (the Play-offs). Standings only make sense on table-based stages; if you query standings against a knockout stage ID, you’ll get an empty result.
Step 3: Pull group stage standings
Once you have the group stage stage_id for the season you care about, use the standings by season id endpoint and filter by that stage. The Sportmonks Standings endpoint supports filtering by stage IDs using dynamic filters and also supports includes, such as participant details, to add extra fields like full breakdowns.
async function getGroupStageStandings(seasonId, stageId) {
const url = [
`${BASE_URL}/standings/seasons/${seasonId}`,
`?api_token=${API_TOKEN}`,
// include team info and detailed standing metrics
`&include=participant;details`,
// filter to only return standings for the specific stage
`&filters=standingStages:${stageId}`
].join('');
const res = await fetch(url);
const { data } = await res.json();
// Group rows by group_id (for multi-group stages)
const groups = {};
data.forEach(row => {
const groupKey = row.group_id ?? 'ungrouped';
if (!groups[groupKey]) groups[groupKey] = [];
// Build a richer object with key fields
groups[groupKey].push({
position: row.position,
teamName: row.participant?.name,
points: row.points,
played: row.overall?.played ?? null,
won: row.overall?.won ?? null,
draw: row.overall?.draw ?? null,
lost: row.overall?.lost ?? null,
goalsFor: row.overall?.goals_scored ?? null,
goalsAgainst: row.overall?.goals_against ?? null,
result: row.result,
details: row.details // any standing details included
});
});
return groups;
}
The result field on each standing row is particularly useful for qualifying contexts: “up” means the team is in a qualifying position, “down” means they are not, and “equal” means they’re on the boundary. You can use this directly to colour-code qualification zones in your UI without having to hardcode position thresholds yourself.
Step 4: Fetch qualifying fixtures
Once you have your stage IDs, pull fixtures filtered to that stage. The participants include both team names, and the venue include gives you the match location:
async function getFixturesByStage(seasonId, stageId) {
const url = [
`${BASE_URL}/fixtures`,
`?api_token=${API_TOKEN}`,
`&include=participants;venue;scores`,
`&filters=fixtureSeasons:${seasonId};fixtureStages:${stageId}`
].join('');
const res = await fetch(url);
const { data } = await res.json();
return data.map(fixture => ({
id: fixture.id,
name: fixture.name,
kickoff: fixture.starting_at,
venue: fixture.venue?.name ?? 'TBC',
result: fixture.result_info,
scores: fixture.scores,
}));
}
Heads up on pagination: The fixtures endpoint defaults to 25 results per page. For a full qualifying campaign (UEFA alone has over 200 group-stage fixtures), make sure you handle pagination using the has_more flag in the response meta’s pagination object. Pass &per_page=50&page=2 etc. until has_more is false.
Step 5: Get match-level statistics
For any individual fixture, you can pull detailed match statistics (team stats for that match) using the fixture ID. Sportmonks returns all match statistics for both teams when you include statistics on a fixture request. You can optionally filter those statistics by specific statistic types (like shots, corners, possession, etc) using the dynamic filter syntax.
async function getFixtureStats(fixtureId) {
const url = [
`${BASE_URL}/fixtures/${fixtureId}`,
`?api_token=${API_TOKEN}`,
// include team statistics for this match
`&include=statistics`,
// filter only specific stats types (if desired)
`&filters=fixtureStatisticTypes:42,49,51,52,56,84`
].join('');
const res = await fetch(url);
const { data } = await res.json();
// 'data.statistics.data' contains team statistics for this fixture
return data;
}
Key details
– Use the GET /fixtures/{fixtureId} endpoint with include=statistics to return match-level team statistics.
– The statistics include returns statistics for both teams in the fixture. Each statistic entry includes a type_id representing what the stat is.
– You can apply a filter such as fixtureStatisticTypes:{id1,id2,…} to return only the statistics for specific types you care about.
– The type IDs come from the general Types endpoint (GET /v3/core/types?api_token=YOUR_TOKEN) and represent specific stats (shots on target, possession, corners, fouls, etc.). It’s recommended to fetch and store these once rather than include the type object on every request.
Filtering specific stat types
To keep your response focused, add a filter using the dynamic filter format:
&filters=fixtureStatisticTypes:42,49,51,52,56,84
Replace the IDs with the statistic types you want. You can find valid statistic type IDs from the Types endpoint.
Step 6: Handle the playoff stages
Qualifying playoffs are knockout rounds, not group tables. They have aggregate_id values (for two-legged ties) and no standings. Here’s how to pull playoff fixtures and check which leg each match is:
async function getPlayoffFixtures(seasonId, stageId) {
const url = [
`${BASE_URL}/fixtures`,
`?api_token=${API_TOKEN}`,
`&include=participants;scores;aggregate`,
`&filters=fixtureSeasons:${seasonId};fixtureStages:${stageId}`
].join('');
const res = await fetch(url);
const { data } = await res.json();
return data.map(fixture => ({
id: fixture.id,
name: fixture.name,
leg: fixture.leg, // "1/2" or "2/2" for two-legged ties
aggregate_id: fixture.aggregate_id,
kickoff: fixture.starting_at,
result: fixture.result_info,
}));
}
Grouping by aggregate_id gives you the two legs of a tie together, which you need to determine who advances on aggregate. If aggregate_id is null, it’s a single-leg match.
Step 7: Covering all six confederations
The same pattern above applies to every confederation; you repeat the flow with each league’s ID. Here’s a clean wrapper that runs the full chain for any qualifying league:
async function buildQualifyingData(leagueId) {
// 1. Get the active season
const season = await getQualifyingSeason(leagueId);
// 2. Discover stages
const stages = await getSeasonStages(season.id);
// 3. Identify group vs playoff stages
const groupStage = stages.find(s => s.name.toLowerCase().includes('group'));
const playoffStages = stages.filter(s => !s.name.toLowerCase().includes('group'));
// 4. Pull group standings if applicable
const standings = groupStage
? await getGroupStageStandings(season.id, groupStage.id)
: null;
// 5. Pull fixtures per stage
const allFixtures = await Promise.all(
stages.map(s => getFixturesByStage(season.id, s.id))
);
return {
league_id: leagueId,
season,
stages,
standings,
fixtures: allFixtures.flat(),
};
}
// Run for all confederations
const CONFEDERATION_LEAGUE_IDS = [
'{UEFA_LEAGUE_ID}',
'{CONMEBOL_LEAGUE_ID}',
'{CAF_LEAGUE_ID}',
'{CONCACAF_LEAGUE_ID}',
'{AFC_LEAGUE_ID}',
'{OFC_LEAGUE_ID}',
];
Promise.all(CONFEDERATION_LEAGUE_IDS.map(buildQualifyingData))
.then(results => console.log(JSON.stringify(results, null, 2)))
.catch(console.error);
A note on rate limits and caching
Qualifying data doesn’t change minute-to-minute. Group standings and completed fixture results are static once a matchday finishes. Cache aggressively:
– Static data (season info, stage structure, confirmed results): cache for 24 hours or more.
– Standings mid-matchday: refresh every 5–10 minutes while games are live.
– Live fixture events: use the livescores endpoint (GET /v3/football/livescores) during matchdays; it’s designed for polling.
Your rate limit depends on your plan. Keep an eye on the X-RateLimit-Remaining header in every response and back off if you’re approaching the limit before a matchday window closes.
What you can build with this
The data available across all six qualifying campaigns unlocks more than score trackers. A few ideas worth exploring:
– Qualification probability dashboards: combine standings data with the Predictions endpoint (/v3/football/predictions/fixtures/{fixture_id}) to show each nation’s projected final position and likelihood of reaching the tournament.
– Cross-confederation comparison tools: CONMEBOL has 10 nations in a single group; AFC runs multiple rounds, with group stages feeding into knockout rounds. With all six datasets normalised, you can build a unified tracker showing live qualification status worldwide.
– Historical campaign archives: call GET /v3/football/seasons?filters=seasonLeagues:{league_id} to retrieve all past qualifying seasons and build a searchable archive of every World Cup qualifying campaign in the database.
Summary
Here’s the full call sequence when building a qualifying integration from scratch:
- GET /v3/football/leagues/{league_id}?include=currentSeason : get the active season ID
- GET /v3/football/stages?filters=stageSeasons:{season_id} : discover all stages
- GET /v3/football/standings/seasons/{season_id}?filters=standingStages:{stage_id} : group stage tables
- GET /v3/football/fixtures?filters=fixtureSeasons:{season_id};fixtureStages:{stage_id} : fixtures per stage
- GET /v3/football/fixtures/{fixture_id}?include=statistics;events : match-level detail
The key insight is treating stages as first-class objects rather than an afterthought. Pull them early, classify them, and use them to gate your standings and fixture queries accordingly. Get that right, and the rest falls into place cleanly.
Ready to start building? Check out the full Sportmonks Football API documentation for more information.
Unlock World Cup qualifying data with Sportmonks Football API
Ready to power your app or website with reliable World Cup qualifying data? Sportmonks’ Football API gives developers fast access to structured, live and historical football data from over 2,300 leagues globally, including all six World Cup qualifying confederations.
With detailed fixtures, standings, statistics, and flexible filtering options, you can build scoreboards, probability dashboards, analytics tools, and fan experiences with confidence. Start with a free trial, explore our comprehensive docs, and integrate advanced football data into your project today. Visit the Sportmonks Football API documentation to get started now.


