League Standings & Top Scorers Scraper — ATP, WTA
Pricing
from $1.00 / 1,000 league processeds
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
Maintained by CommunityActor 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
- 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.
- 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).
- 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.
- 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
| Capability | What it means for you |
|---|---|
| Season scoring chart | player, 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 tables | two extra tables built only from home or only from away matches |
| Recent form per team | a 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 logos | a link to every team's logo in each table row, from the same feed, no extra request |
| Next fixture per team | opponent and the match date attached to every row — the source publishes the day, not the kickoff time |
| Qualification zones | which 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 rankings | eight tables: singles, doubles and the Race of each, with points and position change |
| Tennis Elo ratings | overall Elo plus separate hard, clay and grass ratings for both tours, with peak Elo and the official rank next to it |
| Conferences, divisions and groups | NBA, 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 tables | the 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 seasons | lastSeasons and seasons return earlier seasons of the same league page, one league record per season |
| Only what changed | onlyChanges returns and bills only the rows that are new or changed since the previous run: made for daily schedules |
| Joins with match data | league, season, stage and team ids under the same names and values as in our match Actors |
| A table view for each kind of row | league tables, top scorers, home, away and extra tables, rankings; outputLayout: rows writes one record per row instead |
| Any sport, any country | football, basketball, hockey, handball — the same league page format |
How this compares
| This Actor | Other 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 scorers | yes, 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 tables | yes | the Flashscore archive Actor returns the same table types |
| Proxies | direct first, then a datacenter proxy; no residential proxy needed | residential 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:
| Here | In a match record | Example (Premier League, 14 October 2026) |
|---|---|---|
tournamentId | tournamentId — the league, the same for every season | dYlOSQOD |
tournamentSeasonId | tournamentSeasonId — the season | SY30SsKF |
tournamentStageId | tournamentStageId — the stage | CfoA8Dmm |
teamId in a table row | home.id and away.id | Coventry GOvB22xg |
leagueUrl | tournamentUrl | https://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:
| Rows | Key |
|---|---|
| League tables, home, away and extra tables | tournamentStageId + table + venue + window + line + group + teamId |
| Top scorers | tournamentSeasonId + playerId |
| Rankings | rankingUrl + rankingDate + playerId |
| Elo tables | rankingUrl + 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=xlsxgives the League tables view as an Excel file, with column headers named after the fields (rank,team,points);view=topScorerRows,homeTableRows,awayTableRows,extraTableRowsandoverviewwork the same way,view=rankingRowson the rankings dataset. Addclean=trueto 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: rowswrites 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 ApifyClientclient = 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).itemsfor 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
onlyChangesit 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):
error | What happened |
|---|---|
invalid url | the value is not a web address; no request is made |
page not found | the source answered 404: check the address |
not a league page | the page loaded but is not a competition page |
no league table | the competition has no table or scoring chart, as with knockout cups such as the FA Cup |
no table after filters | overUnderLines and formWindows left no table of the league; details names the lines and windows it has |
no active season stage | the league has no active stage at the source, most likely the off-season |
season not found | none of the seasons named in seasons exists for the league; details names the nearest ones |
season not started | the new season is listed, but no team has played yet |
source unavailable | the page or its table feeds did not load after retries; the address may be fine |
ranking id not found, no data from ranking feed | the page carries no ranking table, or the table came back empty |
no data from elo report, unknown tour | the 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 () instead of a table of zeros, and nothing is billed for it. Pass the archive page (error: "season not started"
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) orW10+. - 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 unavailableand 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:
| Event | Price |
|---|---|
| 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.
Related Actors
- Matches of the same leagues, joined on
tournamentId,tournamentSeasonId,tournamentStageIdandteamId: Tennis Scraper — ATP, WTA | Live Scores, Odds, Point by Point; Football & Soccer Scraper — xG, Live Scores, Odds; Basketball Scraper — NBA, EuroLeague, Box Scores; Ice Hockey Scraper — NHL, KHL Box Scores & Odds; Baseball Scraper — MLB Box Scores, NPB, KBO, Odds; Multi-Sport Scraper — Table Tennis, Handball +21. - Tennis Player Career Scraper — Serve Stats, Surface Splits — a tennis player's career match by match, with serve and return statistics.
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.