Flashscore Scraper API - Tennis Live Scores, Results, Odds
Pricing
$1.99 / 1,000 match rows
Flashscore Scraper API - Tennis Live Scores, Results, Odds
Flashscore scraper for 31 sports: one football day feed returned 460 tournaments and 1,815 matches in a single request. Tennis live scores (ATP, WTA, ITF) and football live scores API over a 15-day window, 76 columns incl. set scores and tiebreaks. No browser, no proxy.
Pricing
$1.99 / 1,000 match rows
Rating
0.0
(0)
Developer
Snow Leo Data
Maintained by CommunityActor stats
0
Bookmarked
15
Total users
6
Monthly active users
11 days ago
Last modified
Categories
Security
Limited permissionsShare
Flashscore Scraper: 31 Sports, Live Scores, Fixtures and Results
Scrape Flashscore live scores, fixtures and finished results for 31 sports over a 15-day window, without a browser, without a proxy and without an API key. One request returns an entire day of a sport: on 12 September 2026 the football day feed carried 460 tournaments and 1,815 matches in a single request weighing 2,788,777 bytes.
This is a sports data API you can schedule. It returns tournaments, countries, kick-off times, live minute, period scores, tennis tiebreaks, red cards, participant ids, team logos, head-to-head history and league metadata as structured JSON, CSV, Excel or XML.
What this Actor gives you that a hand-rolled scraper does not
Three things, and every one of them is a number you can reproduce with
python3 tools/measure.py.
1. The signature header. The Flashscore feed is not open. It
answers 401 when the x-fsign header is missing and 401 again when the
header carries a wrong value — both measured from Apify servers, not from a laptop. A naive
scraper that simply requests the feed URL gets nothing at all. This Actor sends
the header the site's own front end sends.
2. Field names lifted from the source's own build. Flashscore ships its feed
as two-letter keys: AE, AG, ZC. Most scrapers guess what those mean. This
one does not guess. The names come out of Flashscore's own JavaScript bundle,
where the webpack module that defines the keys and the core-debugger chunk
that names them can be joined: AE is FULLFEEDINDEXES_HOME_PARTICIPANT_NAME,
AG is SHAREDINDEXES_HOME_CURRENT_RESULT. That join yields 252 feed keys
carrying 265 constant names, and 9 keys that mean different things in
different sports are kept with every meaning rather than flattened to one.
Three of the ids were cross-checked against the live Premier League page, whose
own config object publishes tournamentId: "SY30SsKF",
tournamentStageId: "CfoA8Dmm" and tournamentTemplateId: "dYlOSQOD" — exactly
the values of feed keys ZE, ZC and ZEE.
3. Coverage measured, not claimed. Sport ids 1 to 45 were probed from Apify servers across three different days. 31 of them return a feed: football, tennis, basketball, hockey, american football, baseball, handball, rugby union, rugby league, floorball, bandy, futsal, volleyball, cricket, darts, snooker, boxing, aussie rules, badminton, water polo, golf, field hockey, table tennis, beach soccer, mma, pesapallo, auto racing, moto racing, cycling, horse racing and esports.
The 15-day window, and why it matters
The feed serves day offsets -7 to +7, which is 15 days in total. This is
not a guess either: at offset -8 and at offset +8 the server answers HTTP 200
with a body of 0. That is the worst kind of failure, because it is not a
failure — a scraper that checks only the status code records a successful
request and an empty result, and the buyer never learns why.
This Actor refuses an out-of-window day before sending a single request,
names the dropped offsets back to you in the run report, and writes the named
reason days-out-of-window so that a monitoring watchdog can tell a legitimate
empty run from a broken one.
Every sport gets a share of your row limit
maxItems limits delivered rows, not rows walked. That distinction is the
difference between a working Actor and one that charges for nothing.
There is a second trap behind it. Football is by far the largest feed — 1,815 matches on a normal Tuesday against 138 for tennis. Ask for all sports with a limit of 60 rows and a naive implementation hands you 60 football matches and calls it multi-sport coverage. This Actor gives each selected feed its own quota and then interleaves the feeds round-robin, so a 60-row run across all sports returns at least eight different sports, none of them taking more than a fifth of the rows. Empty feeds do not waste their slot; their share flows to the sports that have matches.
Input
| Field | What it does |
|---|---|
sports | Which sports to walk. Names or ids, or all for every one of the 31 that answer. |
dayOffsets | Days relative to today. 0 today, -1 yesterday, 3 in three days. Range -7..+7. |
matchStatuses | Keep only scheduled, live or finished. Empty means all three. |
tournamentContains | Keep matches whose tournament name contains any of these words. |
countryContains | Keep matches from these countries or regions. |
teamContains | Keep matches where either side matches, by full name or short name. |
includeHeadToHead | Add previous meetings and recent form. One extra request per delivered match. |
includeOdds | Add pre-match odds to tennis and football rows: home and away (plus draw in football) from the first of four bookmakers that quotes the match. One to four extra requests per match. |
includeRawFields | Add every raw feed key with the constant name Flashscore's own bundle gives it. |
compactOutput | Drop empty fields from every row. |
onlyChanged | Deliver only new or changed matches. Made for scheduled monitoring. |
emitUnchanged | With incremental mode on, still write unchanged matches, tagged UNCHANGED. |
monitorStoreName | Name of the store holding incremental memory, so several schedules stay apart. |
language | Language of tournament and participant names. Nine to choose from. |
maxItems | Upper bound on delivered rows. 0 means no limit. |
detailConcurrency | How many head-to-head requests run in parallel. |
16 input fields, and every one of them is read by the code — a check enforces that, so the schema can never advertise a switch that does nothing.
Output
The dataset declares 71 columns. A football match on a normal day fills
about 50 of them; a tennis match fills 53, because tiebreaks and doubles
partners only exist there. Fields that a sport does not have are null, never
an empty string — an empty string in a numeric column breaks Excel, BigQuery and
pandas alike.
Groups of columns:
- Identity —
event_id,url,sport,sport_id,day_offset. - Tournament —
tournament_name,tournament_header,country,country_id,tournament_id,tournament_stage_id,tournament_template_id,tournament_url,tournament_stage_type,tournament_stats_type,tournament_source_type,tournament_stages_count. - Time and state —
start_timestamp,start_time_utc,date,end_timestamp,period_start_timestamp,status,stage_type_id,stage_id,stage_time,game_time,is_live,round_name. - Participants —
home_name,away_name, short names, country names and ids,home_participant_ids,away_participant_ids, event-participant ids, logos, slugs, and the four fields that carry doubles partners in tennis:home_first_participant,home_second_participant,away_first_participant,away_second_participant. - Scores —
home_score,away_score, full-time scores,winner,home_red_cards,away_red_cards,period_scores(up to nine periods plus an extra one) andtiebreaks(up to five). - Flags —
is_national_event,has_lineups,has_live_centre,has_highlights,has_odds_comparison,info_notice. - Extras —
head_to_head,change_type,raw, and four odds columns (odds_home,odds_draw,odds_away,odds_bookmaker_id) that exist only on rows a bookmaker really quoted.
status is derived from the feed's stage type, and the derivation was measured
rather than assumed: on a past day all 480 football matches carried stage type
3, on a day two ahead 226 of 227 carried type 1, and on the current day 98
matches carried type 2 together with the live flag. Hence exactly three values:
scheduled, live, finished. The finer stage_id is passed through as a
number without an invented name, because its values were not verified.
Scheduled monitoring
Sports data is a monitoring niche. Across 471 Actors in this niche the store reports 20,295 users and 655,998 runs — 32.3 runs per user — and the strongest monitoring neighbour runs 289.0 times per user. People put these on a schedule; they do not export once.
Monitoring only works if a repeat run is cheap. Turn on onlyChanged and the
Actor remembers what it delivered in a named key-value store — named,
because the default store is wiped at the start of every run, so incremental
mode built on it would silently degrade into a full export. Memory holds
60,000 keys, roughly 2 MB against a 9 MB record ceiling.
The fingerprint covers only fields the day feed always carries: status, stage, both scores, kick-off time, winner and clock. Head-to-head and odds are deliberately excluded — otherwise the same match fetched with and without extras would look changed every single run.
Named reasons for an empty dataset
A green run with an empty dataset is the worst thing an Actor can do to a buyer. In a monitoring niche it is also legitimate: a scheduled run that finds nothing changed should write zero rows, or you pay twice for the same data.
So this Actor never stays silent about it. Every empty outcome carries a name in the run report and a sentence in the log:
days-out-of-window— every requested day is outside -7..+7. No request sent.days-unreadable— the day values are not whole numbers (a date such as2026-12-25or a word such astomorrow). No request sent.status-impossible-for-days— a past day cannot contain scheduled matches and a future day cannot contain finished ones. No request sent.no-sports-selected— nothing recognisable in the sports list. No request sent.charge-limit-reached— the run's charge limit is already spent. No request sent.unknown-input-fields— every field in the input is one this Actor does not have (a name carried over from another scraper, see below). No request sent, and the demo default below does not run, so you are never billed for rows you never asked for.nothing-changed— incremental mode working as asked.filtered-out— rows were collected and your filters removed all of them.source-returned-nothing— that sport has no matches on that day.source-refused— the feed stopped answering.nothing-collected— matches were seen but none survived to delivery.
The first six are decided from your input alone, before the first byte is downloaded. A run whose result is already determined by its own input should not cost you the wait, and a test enforces that those paths issue zero requests.
The same sentence is the run's status message in the Console, so the run list
already tells you what happened. A run that delivered rows says how many and
for what (Delivered 5 match row(s) (tennis, day +1).); with includeOdds on it
adds how many rows got odds and why the rest did not. A run that delivered
nothing names the reason and says nothing was charged.
Field names carried over from other scrapers
Paste an input built for a different Flashscore or sports-data Actor and this
one still reads it. days, date and dates land in dayOffsets; status
and statuses in matchStatuses; country, countries, region and
regions in countryContains; league, leagues, competition,
competitions, tournament and tournaments in tournamentContains;
team, teams, player, players, participant and participants in
teamContains; sport and sportName in sports; limit, maxResults,
resultsLimit and maxRows in maxItems. A field this Actor truly has no
equivalent for - a per-match URL list, or a mode switch some competitors
use to pick between fixtures and odds - is named back to you instead of being
silently dropped: this Actor has no per-URL target at all, it walks whole
sport/day feeds, so there is no honest field to route one onto, and an input
that is only unrecognised fields never falls back to the demo default and
bills you for football you never asked for.
Honest limitations
Rule of the house: anything a competitor does that this Actor does not gets
named here rather than quietly omitted. The line-by-line comparison against the
strongest Flashscore Actor in the store lives in COMPARISON.md.
- No league standings tables. Flashscore moved standings off the feed host
this Actor uses; eleven endpoint spellings were tried from Apify servers and
all returned an empty body.
extractify-labs/flashscore-extractorreturns standings; this one does not. - No per-match statistics, incidents or lineups. The day feed flags whether
a match has lineups and a live centre, but the blocks themselves are not
fetched.
statanow/flashscore-scraper-livereturns them in its history mode. - No deep history. The window is 15 days, full stop. An Actor that claims seasons of history is reading a different set of endpoints.
- No standby HTTP endpoint. Runs are started the normal way, through the Apify API or a schedule.
stage_idvalues are not named. Only the three stage types were verified. The finer code is passed through as a number.- Odds are narrow on purpose.
includeOddsadds the pre-match winner price for tennis (home, away) and football (home, draw, away) only, taken from the first of four bookmakers that quotes every side of the market (odds_bookmaker_idsays which one). Measured on 60 matches of day +1: 44 of 60 tennis matches and 32 of 60 football matches had a price. A match nobody quotes carries no odds columns at all, notnulls, and the run status line counts them. Other sports get none, and there is no handicap, over-under, moneyline, price history or line movement. Finished and live matches show the pre-match price. The day feed itself carries no odds, so they come from one extra request per bookmaker tried, to the odds service the Flashscore web client uses; an unanswered odds request leaves the row without odds and the status line says so. The price of a row is the same with or without odds. - The signature can be rotated.
x-fsignis a constant of the site's front end today. If Flashscore changes it, the feed answers 401 and the Actor sayssource-refusedinstead of pretending everything is fine.
FAQ
Do I need a proxy? No. Every measurement in this document was taken from Apify servers with the platform's ordinary outbound address and no proxy. The feed does not challenge datacenter addresses; it checks the signature header.
Do I need a browser? No. The Actor is pure Python standard library. No Playwright, no Puppeteer, no Chrome. That is why it fits in 1,024 MB.
How many requests does a run cost? One per sport per day, plus one per
delivered match if includeHeadToHead is on. All 31 sports for today is 31
requests. That is the whole point of the day feed.
Why is the football number so much larger than the others? Because it is. 1,815 football matches against 138 tennis matches on the same day is the shape of the source, not a sampling artefact. It is also why per-sport quotas exist.
Can I get yesterday's results? Yes — dayOffsets: ["-1"]. Anything older
than seven days is outside what the source serves, and the Actor will tell you
so before spending your time.
Can I follow one team? Put its name in teamContains and schedule the run
with onlyChanged. You will get a row when the team's next match is added and
another when its score moves.
What language are the names in? English by default; nine languages are available. The ids stay identical across languages, so you can switch language without breaking a downstream join.
What happens if the run is interrupted? Rows are pushed to the dataset first, and only then marked as delivered in memory. An interrupted run may repeat a few rows on the next pass; it will never drop them.
Is the data free to use? The Actor reads publicly visible pages of a public website. What you may do with the data afterwards depends on your jurisdiction and your purpose — that part is yours to check.