Flashscore Scraper API - Tennis Live Scores, Results, Odds avatar

Flashscore Scraper API - Tennis Live Scores, Results, Odds

Pricing

$1.99 / 1,000 match rows

Go to Apify Store
Flashscore Scraper API - Tennis Live Scores, Results, Odds

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

Snow Leo Data

Maintained by Community

Actor stats

0

Bookmarked

15

Total users

6

Monthly active users

11 days ago

Last modified

Security

Limited permissions

Share

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

FieldWhat it does
sportsWhich sports to walk. Names or ids, or all for every one of the 31 that answer.
dayOffsetsDays relative to today. 0 today, -1 yesterday, 3 in three days. Range -7..+7.
matchStatusesKeep only scheduled, live or finished. Empty means all three.
tournamentContainsKeep matches whose tournament name contains any of these words.
countryContainsKeep matches from these countries or regions.
teamContainsKeep matches where either side matches, by full name or short name.
includeHeadToHeadAdd previous meetings and recent form. One extra request per delivered match.
includeOddsAdd 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.
includeRawFieldsAdd every raw feed key with the constant name Flashscore's own bundle gives it.
compactOutputDrop empty fields from every row.
onlyChangedDeliver only new or changed matches. Made for scheduled monitoring.
emitUnchangedWith incremental mode on, still write unchanged matches, tagged UNCHANGED.
monitorStoreNameName of the store holding incremental memory, so several schedules stay apart.
languageLanguage of tournament and participant names. Nine to choose from.
maxItemsUpper bound on delivered rows. 0 means no limit.
detailConcurrencyHow 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) and tiebreaks (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 as 2026-12-25 or a word such as tomorrow). 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-extractor returns 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-live returns 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_id values are not named. Only the three stage types were verified. The finer code is passed through as a number.
  • Odds are narrow on purpose. includeOdds adds 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_id says 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, not nulls, 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-fsign is a constant of the site's front end today. If Flashscore changes it, the feed answers 401 and the Actor says source-refused instead 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.