League Standings & Top Scorers Scraper — ATP, WTA avatar

League Standings & Top Scorers Scraper — ATP, WTA

Pricing

from $1.00 / 1,000 league processeds

Go to Apify Store
League Standings & Top Scorers Scraper — ATP, WTA

League Standings & Top Scorers Scraper — ATP, WTA

League tables, season scoring charts and tennis rankings in one Actor: rank, form, goals, points, qualification zones, next fixture, top scorers with club and nationality, ATP and WTA rankings with points and position change, tennis Elo by surface. No login, no browser.

Pricing

from $1.00 / 1,000 league processeds

Rating

0.0

(0)

Developer

Huyen Trang

Huyen Trang

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 hours ago

Last modified

Categories

Share

Paste a league page and get the full table and the season scoring chart: rank, played, wins, draws, losses, goals, points, points per game, recent form, qualification zone and each team's next fixture — plus top scorers with club, position and nationality.

Paste a ranking page instead and get ATP or WTA rankings with points, position and how far each player moved since the last update. Flip one switch and get tennis Elo ratings on top — overall and per surface — from Tennis Abstract.

No login. No cookies. No API key. No headless browser.

Quick start

  1. In League pages, pick leagues from the list or paste any Flashscore league page. In Ranking pages, pick ATP or WTA tables from the list; leave either field empty if you do not need it.
  2. Press Start. The form as first opened — the Premier League and LaLiga with 50 top scorers each, the ATP top 10 — comes to $0.02265 (see What a run costs).
  3. Open the Output tab. League tables has one row per team, Top scorers one row per player, Rankings one row per ranking position; Leagues lists the league records with their row counts and any error.
  4. For a spreadsheet, press Export, set View to League tables (or Top scorers) and the format to Excel, then Download. Export covers the league records only: for the rankings open the run's Storage tab, pick the rankings dataset and export it there. Details in Download as a spreadsheet.

What you get

CapabilityWhat it means for you
Season scoring chartplayer, club and club code, goals, assists, position and nationality in the same run as the table; points too where the source publishes them (NHL)
Home and away tablestwo extra tables built only from home or only from away matches
Recent form per teama W/D/L string of the latest results, most recent first, and the current streak checked against the league's form table: W6, L10+
Team logosa link to every team's logo in each table row, from the same feed, no extra request
Next fixture per teamopponent and the match date attached to every row — the source publishes the day, not the kickoff time
Qualification zoneswhich rows are Champions League, play-off or relegation — as the source's own label ("Promotion - Champions League"), plus the raw code and colour
ATP and WTA rankingseight tables: singles, doubles and the Race of each, with points and position change
Tennis Elo ratingsoverall Elo plus separate hard, clay and grass ratings for both tours, with peak Elo and the official rank next to it
Conferences, divisions and groupsNBA, NHL, MLB and Nations League rows carry their group, streak and the qualified-for mark; NBA and MLB rows also win percentage and games behind. A team is billed once per table, however many groups it appears in
Form, Over/Under and HT/FT tablesthe extra tables of the league page, overall, home or away, billed like the main table; keep only the lines and form windows you need
Past seasonslastSeasons and seasons return earlier seasons of the same league page, one league record per season
Only what changedonlyChanges returns and bills only the rows that are new or changed since the previous run: made for daily schedules
Joins with match dataleague, season, stage and team ids under the same names and values as in our match Actors
A table view for each kind of rowleague tables, top scorers, home, away and extra tables, rankings; outputLayout: rows writes one record per row instead
Any sport, any countryfootball, basketball, hockey, handball — the same league page format

How this compares

This ActorOther Store Actors (14 Oct 2026)
A 20-team league table$0.00645$0.008 on an ESPN standings Actor ($0.0004 a row) and on a SofaScore Actor that bills $0.008 a run for one league; $0.01005 on a Flashscore league archive Actor; $0.04 on an ESPN sports Actor
The same table with 50 top scorers$0.00695$0.008 on the SofaScore Actor above, whose run also carries 32 leader categories and the league's matches
NBA table, 30 teams in conferences and divisions$0.00915$0.012 on the ESPN standings Actor
ATP top 100$0.07905$0.09 on a tennis rankings Actor; $0.20 on a Tennis Abstract Actor; $0.02 and $0.049 plus a start fee on two Actors with one or no users in the last 30 days
One Elo row$0.00078$0.00049 and $0.002 on two Tennis Abstract Actors
Top scorersyes, with club, club code, position and nationality$0.0002 a row on the Flashscore archive Actor; $0.005 a league on another Flashscore Actor
Home, away, form, over/under and HT/FT tablesyesthe Flashscore archive Actor returns the same table types
Proxiesdirect first, then a datacenter proxy; no residential proxy neededresidential proxies built in or on by default on two SofaScore-based Actors

Prices of other Actors are their Free-plan event prices on 14 October 2026, with the run start where they charge one; the run totals of this Actor include its own $0.00005 start.

Output example

One league record of the default nested layout, from a local test run on 14 October 2026 with the input form as first opened; the arrays are cut to their first item:

{
"leagueUrl": "https://www.flashscore.com/football/england/premier-league/",
"league": "Premier League",
"tournamentId": "dYlOSQOD",
"tournamentSeasonId": "SY30SsKF",
"tournamentStageId": "CfoA8Dmm",
"season": "2026/2027",
"stageName": "Main",
"standings": [
{
"rank": 1, "team": "Manchester City", "teamId": "Wtn9Stg0",
"teamUrl": "https://www.flashscore.com/team/manchester-city/Wtn9Stg0/",
"teamLogoUrl": "https://static.flashscore.com/res/image/data/0vgscFU0-lQuhqN8N.png",
"played": 6, "wins": 6, "draws": 0, "losses": 0,
"goalsFor": 16, "goalsAgainst": 5, "goalDifference": 11, "points": 18, "pointsPerGame": 3,
"form": "WWWWW", "zone": "q1", "zoneLabel": "Promotion - Champions League (League phase)",
"zoneColor": "#004682", "nextOpponent": "Ipswich", "nextMatchDate": "2026-10-17",
"group": null, "groupType": null, "winPercentage": null, "gamesBehind": null,
"streak": "W6", "streakMin": 6,
"winsRegulation": 6, "winsOvertime": 0, "winsPenalties": 0,
"lossesRegulation": 0, "lossesOvertime": 0, "lossesPenalties": 0, "qualificationStatus": null
}
],
"homeTable": [],
"awayTable": [],
"extraTables": [],
"topScorers": [
{
"rank": 1, "player": "Haaland E.", "playerId": "UmV9iQmE",
"playerUrl": "https://www.flashscore.com/player/haaland-erling/UmV9iQmE/",
"team": "Manchester City", "teamId": "Wtn9Stg0",
"teamUrl": "https://www.flashscore.com/team/manchester-city/Wtn9Stg0/", "teamCode": "MCI",
"goals": 6, "assists": 0, "points": null, "position": "Forward", "nationality": "Norway"
}
],
"rowsTotal": 70,
"rowsReturned": 70,
"rowsBilled": 70,
"truncated": false,
"scrapedAt": "2026-10-14T17:32:27.158Z"
}

A ranking record of the same run, in the run's rankings dataset:

{
"rankingUrl": "https://www.flashscore.com/tennis/rankings/atp/",
"ranking": "ATP Rankings",
"rankingDate": "2026-10-05",
"rows": [
{
"rank": 1, "previousRank": 1, "rankChange": 0, "player": "Sinner Jannik", "playerId": "6HdC3z4H",
"playerUrl": "https://www.flashscore.com/player/sinner-jannik/6HdC3z4H/", "country": "Italy",
"points": 11000, "tournamentsPlayed": 16, "tied": false
}
],
"rowsTotal": 10,
"rowsReturned": 10,
"rowsBilled": 10,
"truncated": false,
"scrapedAt": "2026-10-14T17:32:27.186Z"
}

The dataset schema of the Actor gives every field a title, a description and, where a run had one, an example value.

Fields

Table row — rank, team, team id, page and logo (teamLogoUrl), played, wins, draws, losses, goals for, goals against, goal difference, points, points per game, form (a W/D/L string of the latest results, most recent first), qualification zone as a code, its label from the source's legend and its colour, next opponent, next match date. The next match date is a YYYY-MM-DD day: the standings feed does not carry kickoff times. Rows also carry group and groupType (conference, division or group), winPercentage, gamesBehind, streak (for example W3) with its length as a number (streakMin, see Conferences, divisions and groups), regulation, overtime and shoot-out wins and losses, and qualificationStatus once a place is secured. Rows of the extra tables carry teamLogoUrl too. winPercentage is a share from 0 to 1, null for a team that has not played yet: the source writes zero there. In hockey losses counts every loss, overtime and shoot-out losses included, as the source does; the NHL's own OTL column is lossesOvertime + lossesPenalties, and regulation losses are lossesRegulation.

League record — besides the tables it carries league, season, stageName, the ids tournamentId (the league), tournamentSeasonId (the season) and tournamentStageId (the stage whose table is returned), and extraTables when you ask for them.

Every league, ranking and Elo record of the nested layout also carries rowsReturned (the rows it holds: table rows plus top scorers for a league), rowsBilled (the billed ones: a team counts once per table, see Conferences, divisions and groups), rowsTotal (the rows before the run's maximum charge cut it) and truncated (true when the budget covered only part of the rows). A league whose table or scoring chart did not load after retries carries unavailableParts, for example ["topScorers"]; the rest of the league is returned. In an onlyChanges run every row also carries changeType (new or changed), and a nested record unchangedRows, the rows left out (see Only changes).

Top scorer row — rank, player, player page, club, club page, club code (teamCode, for example EDM), goals, assists where published, points where published (points: the NHL chart is ranked by them, goals plus assists), position, nationality.

Join with match data

The ids are the source's own, under the same names as in our match Actors (football, basketball, hockey, baseball, tennis and the multi-sport one), so a table joins its matches without matching names:

HereIn a match recordExample (Premier League, 14 October 2026)
tournamentIdtournamentId — the league, the same for every seasondYlOSQOD
tournamentSeasonIdtournamentSeasonId — the seasonSY30SsKF
tournamentStageIdtournamentStageId — the stageCfoA8Dmm
teamId in a table rowhome.id and away.idCoventry GOvB22xg
leagueUrltournamentUrlhttps://www.flashscore.com/football/england/premier-league/

The values above were read from this Actor's local run and from a cloud run of the football Actor on the same day. League tables, Home table, Away table, Form, Over/Under and HT/FT and Top scorers show the league and season ids next to every row; Top scorers also the player's and club's ids (playerId, teamId).

Tennis rankings. There are no league tables for tennis. A ranking row's playerId is the Flashscore ID of the player, the same as home.id and away.id in our tennis match records (checked on 14 October 2026 on 12 players of local runs, such as Alex de Minaur EZgZ9Xfh and Hubert Hurkacz zuHOBnY9), so matches join with the rankings of their players. Our tennis career Actor reads another source and has no Flashscore IDs: it joins with these rows only by name.

Regular loads: deduplicate and update

Every record carries scrapedAt, the moment it was output; of two copies of the same row keep the one with the larger scrapedAt. To receive only the rows that changed, see Only changes. The key of a row:

RowsKey
League tables, home, away and extra tablestournamentStageId + table + venue + window + line + group + teamId
Top scorerstournamentSeasonId + playerId
RankingsrankingUrl + rankingDate + playerId
Elo tablesrankingUrl + rankingDate + player (Elo rows have no ID)

These are the fields of the rows layout. In the nested layout the table is the list a row comes from (standings, homeTable, awayTable, or an entry of extraTables with its table, venue, window and line), and the league ids are on the league record. Fields that do not apply are empty, so keep them in the key as empty values; a team listed in its conference and in its division appears in both groups, so group stays in the key.

Only changes

Turn on onlyChanges for scheduled runs: a run then returns only the rows that are new or changed since the previous run with the same changesName, each marked with changeType (new or changed), and bills only them: league-result (or ranking-result) only for a league or ranking with at least one such row, standings-row, top-scorer-row and ranking-row only for those rows. A league or ranking without changes gives no record at all, and the status message counts the rows skipped. A run where nothing changed pays only the start and the run minimum (see the costs below). The first run with a new name returns everything. changesKey, the older name of changesName, still works in saved tasks, schedules and API calls: the same value keeps the same memory, and when both are set changesName is used.

A row counts as changed when any of its values changes: rank, games, points, goals, form, streak, zone, group. Links, logos, the zone colour and the next fixture do not count. A table row is remembered by league season, table (main, home, away, each form window and each over/under line), group and team; a top scorer and a ranking row by player, an Elo row by name. Only rows the run returned are remembered: rows cut by the maximum charge of the run come back as new in the next run. A row that leaves the table stays remembered.

The memory lives in your account: a named key-value store standings-changes-<changesName> (standings-changes-default by default), one record per league season and ranking, about 3 KB for a 20-team league with 50 top scorers and 80 KB for the whole ATP table (local runs). A run that found no changes does not rewrite it. Named storages are kept until you delete them (Apify docs). To start over, delete the store or pick a new name. Use one name per schedule, and do not start a run while another run with the same name is still going: both would return and bill the same changes.

Input

{
"leagueUrls": [
"https://www.flashscore.com/football/england/premier-league/",
"https://www.flashscore.com/football/spain/laliga/"
],
"rankingUrls": [
"https://www.flashscore.com/tennis/rankings/atp/"
],
"includeStandings": true,
"includeHomeAwayTables": false,
"includeTopScorers": true,
"maxTopScorers": 50,
"includeEloRankings": true,
"eloTours": ["atp", "wta"]
}

The input form lists popular leagues to pick from: the top five European leagues, the Champions League, the Championship, the Eredivisie, Liga Portugal, MLS, the NBA, the Euroleague, the NHL and MLB. For any other league, open the sport, pick the country and the competition, copy the address from the browser and paste it into the same field. Ranking pages lists all eight ranking tables. A league from the list whose new season has not started gives a free explanatory record instead of a table of zeros (the NBA in mid-October, see Limits). That is the whole setup. An address copied without https:// (www.flashscore.com/...) works as is.

An empty input runs the example. When no league page, no ranking page and no Elo table is asked for, the Actor runs the example leagues of the input form — the Premier League and LaLiga with top scorers — and bills them like any other run (about $0.014 at the prices below). Turning on includeEloRankings alone is enough to skip the example: then only the Elo tables are returned.

Conferences, divisions and groups

Leagues split into conferences, divisions or groups (NBA, NHL, MLB, UEFA Nations League) carry the group on every row. Teams appear once per group, so an NBA team has a conference row and a division row: the NBA table is 60 rows for 30 teams, the NHL table 64 rows for 32.

A team is billed once per table. Its first row is a billed table row; its repeat rows in the same table (the division after the conference) are free. The NBA table is 30 billed rows, the NHL table 32; the home and away tables and each extra table count their teams the same way. rowsBilled in the record shows the count.

Win percentage comes from the source where the table is ranked by it (NBA, MLB); games behind is computed against the group leader. Hockey tables are ranked by points, so NHL rows carry winPercentage and gamesBehind as null. Every row also has the current streak, overtime and shoot-out wins and losses, and the "Qualified for" mark once a place is secured. The streak is read from the form string, which carries the last five results; a streak that fills all five is checked in the league's form table, whose windows of the last 10 matches (and 15, 20... where the league has them) count wins, draws and losses. That is one more request per league, free, made only when some team is on such a streak and not at all when tableTypes already holds form. All six matches of a team won gives W6; the last 10 all won gives W10+; where the table cannot tell, the streak stays W5+, five or more. streakMin holds the length as a number: exact for W6, the lower bound for W10+. Measured on 14 October: Manchester City W6 and Barcelona W8 this season, the Edmonton Oilers an exact W5 (six games, the first one not won), the Denver Nuggets ending 2025/2026 on W10+. Shorter streaks are exact: L2. Home and away tables are checked only against the form-home and form-away tables, when you ask for them.

Archive season pages (for example nba-2025-2026) work. When the current stage is a playoff bracket, the regular-season table of the same season is returned. A competition split into several leagues or stages returns the stage its page opens on: for the UEFA Nations League that is League A only (16 teams); Leagues B to D are not returned.

Extra tables

tableTypes adds more tables from the league's standings page: form (the table over the last 5, 10... matches, one table per window), over-under (matches above and below each total line, one table per line) and ht-ft (half-time/full-time outcomes, football only), each also as -home and -away. One extra request per table type per league. A league that does not publish a table simply returns none.

Every extra table is billed like the main table, a team once per table, and one type can hold several tables. Measured on 14 October: on the Premier League form gave two windows (last 5 and last 10), over-under seven lines and ht-ft one table — 10 tables, 200 rows, 200 billed. On the NHL form and over-under gave 11 tables of 64 rows — 704 rows, 352 billed.

Keep only the lines and windows you need. The source sends every line and window in one answer, so overUnderLines (for example ["2.5"]) and formWindows (for example ["5"]) cost no extra request: the other tables are dropped before billing. On the NHL, form and over-under with overUnderLines: ["5.5"] and formWindows: ["5"] kept 2 of the 11 tables: 64 billed rows instead of 352, and $0.02697 instead of $0.10473 for the run with the main table (local runs on 14 October). A line or window the league does not have is named in the log and the run's status message together with the ones it has; a league left with no table at all gets a free record with the error no table after filters. A value that is not a number stops the run before the first request.

Past seasons

lastSeasons returns that many seasons of each league page, newest first, counting back from the season the page opens on: "lastSeasons": 3 on the Premier League page gives 2026/2027, 2025/2026 and 2024/2025, and on an archive page such as nba-2009-2010 it starts from 2009/2010. seasons names seasons as the source writes them, anywhere in the league's list: 2019/2020 in football, 2025 in MLB (2019/20 and 2019-20 work too). The two fields add up, and a season asked for twice comes once. The list of seasons is already on the league page (the Premier League page lists 115), so a season takes the same table requests as a league and no other page.

Each season is billed as a league: league-result plus its rows. Two leagues with "lastSeasons": 2 and "seasons": ["2019/20"] gave six league records (120 table rows, 300 top scorers) for $0.04145 with the start in a local run on 14 October. The MLB page in October opens on the 2026 play-offs, and "lastSeasons": 2 gave the 2026 and 2025 regular-season tables; a season that has not started gives the free season not started record, as the NBA's 2026/2027 did next to its 2025/2026 table. A season name the league does not have is named in the log and the status message with the nearest seasons it has; when none of the named seasons exists, the league gets a free record with the error season not found. The maximum charge of the run is checked before each season. A record of a season other than the one the page opens on carries stageName: null: the page names the stages of its own season only.

Output layout

outputLayout: nested (the default) gives one record per league or ranking, with the table, top scorers and ranking rows as arrays inside it. outputLayout: rows gives one record per table row, top scorer and ranking position, with the league or ranking fields repeated and a recordType field (standings-row, top-scorer-row, ranking-row, elo-ranking-row, error). In the rows layout standings-row covers the main, home, away and extra tables alike; the table and venue fields tell them apart. Billing is the same in both layouts.

Leagues and rankings are in two datasets of the run. League records stay in the run's default dataset; ranking and Elo records go to a second dataset named rankings. The number of records does not change, and a table view never mixes a league with a ranking. In the Output tab the Rankings output opens the second dataset; through the API its id is storageIds.datasets.rankings of the run (see League table API: call it from code). The Export button on the run page and run-sync-get-dataset-items read the default dataset only, that is the leagues.

The dataset views work in both layouts. In the nested layout League tables, Top scorers, Home table, Away table, Form, Over/Under and HT/FT and Rankings unwind the arrays of each record into one row per team, scorer or position. A league or a ranking that returned no data shows there as one row with its error. In the rows layout every view lists all row records of its dataset, told apart by recordType, table and venue.

Download as a spreadsheet

  • Export on the run page: pick a view — League tables, Top scorers, Home table, Away table or Form, Over/Under and HT/FT — and Excel: one row per team or player, numbers as numbers. The default All fields puts each league on one row with every row of its tables spread over the columns: about 1,300 for the Premier League with 50 top scorers, more with home, away and extra tables, and CSV and Excel files stop at 2,000 columns. Export covers the league records only ("The Export button on the Run page exports only the default dataset", Apify docs): for the rankings open the run's Storage tab, pick the rankings dataset in the Dataset list and export it there. The Rankings output in the Output tab opens the same dataset in its view.
  • API: https://api.apify.com/v2/datasets/<datasetId>/items?view=standingsRows&format=xlsx gives the League tables view as an Excel file, with column headers named after the fields (rank, team, points); view=topScorerRows, homeTableRows, awayTableRows, extraTableRows and overview work the same way, view=rankingRows on the rankings dataset. Add clean=true to drop empty rows.
  • Prefer Excel to CSV for opening in Excel: in locales that use ; as the list separator a CSV file opens as one column.
  • outputLayout: rows writes one record per row instead, so even All fields exports flat; the number of records grows to one per row.

League table API: call it from code

Any Apify client works. The API tab of this page has ready code with this Actor's full name and your token; below, <owner> stands for the owner part of that name and $APIFY_TOKEN for your own token. maxTotalChargeUsd caps what one run may cost: the run stops before the next league once the cap is reached (see What a run costs). A synchronous call waits for the run and returns the items of the default dataset, that is the league records; a run longer than 300 seconds answers with 408, so keep synchronous calls small and use a client library for big runs.

curl -X POST "https://api.apify.com/v2/acts/<owner>~league-standings-top-scorers/run-sync-get-dataset-items?view=standingsRows&format=csv&maxTotalChargeUsd=0.05" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"leagueUrls": ["https://www.flashscore.com/football/england/premier-league/"]}'

Rankings are in the rankings dataset: start the run, take its id from storageIds.datasets.rankings and read that dataset.

curl -X POST "https://api.apify.com/v2/acts/<owner>~league-standings-top-scorers/runs?waitForFinish=60&maxTotalChargeUsd=0.1" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"rankingUrls": ["https://www.flashscore.com/tennis/rankings/atp/"]}'
# data.storageIds.datasets.rankings in the answer is <rankingsDatasetId>
curl "https://api.apify.com/v2/datasets/<rankingsDatasetId>/items?view=rankingRows&format=csv" \
-H "Authorization: Bearer $APIFY_TOKEN"

Python, with apify-client:

from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("<owner>/league-standings-top-scorers").call(
run_input={"leagueUrls": ["https://www.flashscore.com/football/england/premier-league/"]},
max_total_charge_usd=0.05,
)
print(run.status, run.status_message)
if run.status == "SUCCEEDED":
rows = client.dataset(run.default_dataset_id).list_items(view="standingsRows", skip_empty=True).items
for row in rows:
print(row["rank"], row["team"], row["points"])

JavaScript, with apify-client, rankings included:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('<owner>/league-standings-top-scorers').call(
{
leagueUrls: ['https://www.flashscore.com/football/england/premier-league/'],
rankingUrls: ['https://www.flashscore.com/tennis/rankings/atp/'],
},
{ maxTotalChargeUsd: 0.1 },
);
console.log(run.status, run.statusMessage);
if (run.status === 'SUCCEEDED') {
const tables = await client.dataset(run.defaultDatasetId).listItems({ view: 'standingsRows' });
for (const row of tables.items) console.log(row.rank, row.team, row.points);
const rankingsId = run.storageIds.datasets.rankings;
const rankings = await client.dataset(rankingsId).listItems({ view: 'rankingRows' });
for (const row of rankings.items) console.log(row.rank, row.player, row.points);
}

The status message is worth printing: it says what was output and why anything was not.

Use it from an MCP client

Apify's MCP server at https://mcp.apify.com exposes Store Actors as tools to MCP clients such as Cursor, VS Code or GitHub Copilot. The client can find this Actor with the search-actors tool and run it with call-actor, or you can pin it as a tool of its own: https://mcp.apify.com?tools=<owner>/league-standings-top-scorers. call-actor returns the run's status and storage ids rather than the rows; get-dataset-items reads the league records from the default dataset and the rankings from the rankings dataset. Details: Apify MCP server.

Schedules, webhooks and integrations

  • Schedules run the Actor or a saved task on a cron expression: a daily run keeps tables and rankings current, and with onlyChanges it returns and bills only the rows that changed (see Only changes).
  • Webhooks call your URL when a run succeeds or fails.
  • An integration that reads the run's default dataset gets the league records; for rankings, give it the dataset id from storageIds.datasets.rankings.

When a league returns nothing

A league, ranking or Elo table that gives no data gets a record with error and details instead, and is not billed. It shows as one row in the table views and in Leagues (a ranking: in Rankings):

errorWhat happened
invalid urlthe value is not a web address; no request is made
page not foundthe source answered 404: check the address
not a league pagethe page loaded but is not a competition page
no league tablethe competition has no table or scoring chart, as with knockout cups such as the FA Cup
no table after filtersoverUnderLines and formWindows left no table of the league; details names the lines and windows it has
no active season stagethe league has no active stage at the source, most likely the off-season
season not foundnone of the seasons named in seasons exists for the league; details names the nearest ones
season not startedthe new season is listed, but no team has played yet
source unavailablethe page or its table feeds did not load after retries; the address may be fine
ranking id not found, no data from ranking feedthe page carries no ranking table, or the table came back empty
no data from elo report, unknown tourthe Elo report could not be read, or eloTours holds a value other than atp and wta

Every run ends with a status message: what was output, how many inputs were not and why (for example 1 page not found, 1 without a league table), parts the source did not return, lines and windows that were not found, where the rankings are, and a stop on the spending limit. A run where nothing loaded because the source did not respond fails with that explanation, so webhooks and alerts see it; when the first three inputs all fail that way, it stops trying the rest. A wrong league or ranking address never fails a run: it succeeds with the breakdown in its status.

Good for

  • Betting and trading models — form tables split by venue, plus the scoring chart.
  • Season dashboards — schedule a daily run and keep tables current.
  • Fantasy and content — top scorers with position and nationality, ready to publish.

Limits

  • Off-season pages show the season the source shows. In October the NBA page already carries the 2026/2027 table with zero games played. When no team of a table has played yet, the Actor returns an explanatory record (
    error: "season not started"
    ) instead of a table of zeros, and nothing is billed for it. Pass the archive page (nba-2025-2026) for last season's final table. A league with no table at all gets an explanatory record too, also not billed.
  • Top scorers exist where the source publishes a scoring chart. Football and hockey do (the NHL chart carries goals, assists and points); the NBA and MLB pages have none, and neither do some cup competitions.
  • Assists are only present where the source lists them, which varies by league.
  • A long streak can stay open. The form string carries the last five results; the form table settles a longer streak only when every match of a window has the same result, so some show as W5+ (five or more) or W10+.
  • One stage per competition. The UEFA Nations League returns League A only.
  • Repeated inputs are dropped. The same league or ranking page given twice (with or without www, a trailing slash, a #/... tab anchor or query parameters), or two addresses of the same table, is fetched and billed once; the log names what was skipped.
  • Temporary load errors are retried twice with a short pause; if the page still does not load, the record says source unavailable and is not billed.

Rankings

Eight tennis tables are available, each on its own page under https://www.flashscore.com/tennis/rankings/: atp, wta, atp-race, wta-race, atp-doubles, wta-doubles, atp-doubles-race and wta-doubles-race; Ranking pages in the input form lists all eight. Each row carries rank, previous rank, the move between them, points, the number of tournaments counted where the source lists it, nationality and a link to the player, and tied when players share a position. In the doubles tables the row is a pair: player, playerId, country and playerUrl hold both partners separated by |. The result also carries rankingDate — the Monday the ranking was published on, so this week's table is distinguishable from last week's. maxRankingRows goes up to 5,000, which covers the whole ATP singles table (2,308 rows on 14 October). Age, points change and best rank are not published in the ranking feed. Ranking records are in the run's rankings dataset (see Output layout).

Elo ratings

Turn on includeEloRankings and the run also returns the tennis Elo leaderboards published by Tennis Abstract (tennisabstract.com) — one record per tour (eloTours, ATP and WTA by default; an empty list also means both), marked "type": "elo-ranking", in the rankings dataset next to the ATP and WTA rankings. Elo is computed from match results rather than tournament points, so it can disagree with the official ranking and gives models a separate signal.

Each row carries the Elo rank, player and profile link, age, overall Elo, separate hard-, clay- and grass-court Elo with their own ranks, career peak Elo with the month it was reached, the official ATP/WTA rank next to the Elo rank, and the source's log-difference measure of how far the two disagree. maxRankingRows caps these tables too. The record carries rankingDate — the source updates the report roughly once a week.

The Elo data comes from the public reports of Tennis Abstract (tennisabstract.com); each elo-ranking record names it in dataSource.

What a run costs

Billing is per row, not per run, so the price of a league follows the size of its table and its scoring chart:

EventPrice
League processed (each season, when several are asked)$0.001
One team row of a table (repeat rows of a team in the same table are free)$0.00027
One scoring-chart row$0.00001
Ranking or Elo table processed$0.001
One ranking or Elo row$0.00078
Run minimum top-up, a unit (see below)$0.00001

The platform also charges $0.00005 when a run starts.

A 20-team league table is $0.001 + 20 × $0.00027 = $0.0064; with the season scoring chart at the default cap of 50 rows it comes to $0.0069. The NBA table of 60 rows has 30 teams and costs $0.001 + 30 × $0.00027 = $0.0091; the NHL table, 32 teams, $0.00964, or $0.01014 with its 50 top scorers. Home and away tables add two tables of 20 teams ($0.0108) to a 20-team league, and the Premier League over-under set of seven lines adds 140 billed rows ($0.0378); overUnderLines and formWindows keep only the tables you pay for.

A ranking page at the default cap of 100 rows is $0.001 + 100 × $0.00078 = $0.079; Elo tables are billed the same way. The ATP singles table has about 2,300 rows (2,308 on 14 October) at $0.00078 a row: all of it, with maxRankingRows raised to its maximum of 5,000, came to $1.80129 in a local run. Lower maxTopScorers and maxRankingRows if you only need the top of each table — that is the whole of the bill. The form as first opened (two leagues with 50 top scorers each, the ATP top 10) came to $0.00005 + 2 × ($0.001 + 20 × $0.00027 + 50 × $0.00001) + ($0.001 + 10 × $0.00078) = $0.02265 with the start in a local test run at these prices on 14 October.

Each season of lastSeasons and seasons is billed as a league. With onlyChanges the league or ranking event and the rows are billed only for what changed (see Only changes). Team logos and the streak check are free.

A league with no table, or with a table where nobody has played yet, returns an explanatory record and is not billed as a table.

Run minimum. A run that reads the source and returns little or nothing billable — a league before its season starts, pages that do not exist — still uses compute, storage and traffic on the platform. Such a run is topped up with the run-minimum event ($0.00001 a unit) to 1.5 times its platform usage at Apify's list prices (compute after the first 5 seconds, which Apify covers). A run that fails, is aborted gracefully or reaches its timeout is topped up only to 1.25 times the usage, which covers what the run cost and no more; an abort without the graceful option (the default in Console and the API) stops the run at once, and it pays only what was charged along the way. Everything the run was charged, the start included, counts toward the minimum, so a run whose tables cover its usage pays no final top-up, and a run with nothing to bill usually comes to a few hundredths of a cent. In a run longer than a minute, part of the top-up can be charged along the way, right after inputs that returned nothing billable, so that a crash later on cannot leave it unpaid. That part is capped at 1.25 times the usage so far and counts toward the final top-up, but it is not refunded if later inputs return tables that would have covered the run. If you pick residential proxies in the Proxy field, their traffic counts at Apify's $8 per GB. The amount and the reason are written to the run's status message and the log. Users on the Apify free plan never pay it, and the minimum never goes past your maximum cost per run: before each input the run checks that the minimum for more work still fits and otherwise stops and says so, and a page that does not answer is not retried once the retry would no longer fit.

The maximum charge you set for a run is respected row by row. When the budget runs out, the last table is returned only with the rows that were billed (in the nested layout it is marked truncated: true, rowsTotal keeps the full count), and the run stops: the next league or ranking is neither fetched nor returned. A team the budget does not cover leaves the table with all its rows. A table is never billed without rows: when the budget left does not cover a table together with at least one of its rows, that table is neither returned nor billed. The run's status message says how many tables were output and how many inputs were not processed.

Speed

Plain HTTP requests, no browser: three per league with the default options, five with home and away tables and one more per extra table type, plus one free request for the form table when a team is on a streak of five or more; a ranking is two (three past 100 rows), an Elo table one. The first requests of a run go out directly, which is faster; the proxy from the input takes over after about 140 requests or as soon as the source refuses a direct request, and is set up only then, so a run that stays direct does not wait for it. If a proxy group or country picked in the input is not available to your plan, the run goes on directly and its status says so. The run starts with 512 MB of memory. When the platform restarts a run (moves it to another server), the league seasons, rankings and Elo tables output before the restart are skipped and not billed again; only a table in progress at that moment can be.

Measured in the cloud on 14 October 2026:

  • the Premier League table over a direct connection: the whole run took 4.1 seconds at 256 MB of memory and 6.5 seconds at 1 GB;
  • the same league with its scoring chart through the Apify proxy: 14 seconds of reading;
  • three leagues with home, away, form, over/under and HT/FT tables, a ranking and an Elo table: 38 seconds, at a memory peak of 175 MB.

FAQ

Is there a football league table API with top scorers? Yes. Paste a league page into leagueUrls: one call returns the full table with rank, points, goals, recent form and qualification zone, plus the season scoring chart with club, position and nationality. The same works for basketball, hockey and handball leagues. Call it from any language over HTTP and get JSON, CSV or Excel; see League table API: call it from code.

Why did a league come back without a table? Its record says why in error and details (see When a league returns nothing): most often an address with a typo, a knockout cup, or a league between seasons. Such a record is free, and the run's status message sums them up.

Where are the rankings? In the run's second dataset, rankings: the Rankings output in the Output tab, storageIds.datasets.rankings in the API. The Export button and run-sync-get-dataset-items return the league records only.

Why does the NBA table have 60 rows when the league has 30 teams? Each team is listed in its conference and in its division. Both rows are returned, and only the first one is billed.

How do I get one row per team in Excel or CSV? Press Export, pick the view League tables and Excel; see Download as a spreadsheet. outputLayout: rows writes one record per row for all tables, scorers and rankings.

How do I join a table with matches from your match Actors? By tournamentSeasonId and teamId (home.id, away.id in a match), or by tournamentId for the league across seasons; see Join with match data.

How do I get last season's table? Set lastSeasons to 2 (this season and the last one) or name it in seasons, such as 2025/2026; each season is billed as a league. See Past seasons.

How do I get only what changed since the last run? Turn on onlyChanges; unchanged rows are neither returned nor billed. See Only changes.

What does truncated mean? The run reached the maximum cost you set while billing that table: it holds the rows that were paid for, and rowsTotal says how many there were.

What does a league cost? About $0.0064 for a 20-team table and $0.0069 with 50 top scorers, plus $0.00005 for the run start; see What a run costs.

Why does a run that returned little show a run-minimum charge? Reading the source costs compute, storage and traffic even when nothing billable comes back. Such a run is topped up to 1.5 times what it used on the platform, or 1.25 times when it fails, is aborted gracefully or reaches its timeout; see Run minimum above.

Do I need residential proxies? No. The Actor reads public pages and feeds with plain HTTP requests, directly and then through the default Apify datacenter proxy.

Support

Open an issue in the Issues tab of this Actor's page. Paste the link to the run (from the run's page in Console): an Actor issue with a run link shares that run, its input, log and storages with the developer automatically, and nothing else in your account (Apify docs). Say what you expected and what you got instead. Do not post your API token or any other secret.

Disclaimer

This is an unofficial Actor. It is not affiliated with, endorsed by, or sponsored by Flashscore, Tennis Abstract or any league, club or competition named here. All trademarks belong to their respective owners. The Actor reads publicly available pages and returns the data they already show to any visitor.