# Flashscore Sports Results Scraper - Live Scores, 31 Sports (`snow_leo_data/flashscore-scraper`) Actor

Flashscore scraper for 31 sports: one football day feed returned 460 tournaments and 1,815 matches in a single request. Football live scores API with tennis and basketball live scores over a 15-day window, 76 columns, field names from the site's own bundle. No browser, no proxy.

- **URL**: https://apify.com/snow\_leo\_data/flashscore-scraper.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Sports, News, MCP servers
- **Stats:** 3 total users, 2 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.99 / 1,000 match rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 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 the 1/X/2, handicap, over-under and moneyline numbers that travel in the same feed. |
| `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 **76 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 nine odds columns.

`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.
- `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.
- `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 four 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.

#### 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 a by-product, not a product.** The nine odds columns are whatever
  travels in the same feed row. This is not an odds-comparison Actor and it does
  not poll bookmakers.
- **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.

# Actor input Schema

## `sports` (type: `array`):

Which sports to collect. 31 sports return a feed - measured by probing ids 1..45 from Apify servers. Pick 'all' for every one of them.

## `dayOffsets` (type: `array`):

Days relative to today: 0 is today, -1 yesterday, 3 in three days. The source serves -7..+7 and returns an empty body outside that - measured; days outside are dropped before any request.

## `matchStatuses` (type: `array`):

Keep only these. Leave empty for all three.

## `tournamentContains` (type: `array`):

Keep matches whose tournament name contains any of these words, e.g. 'Premier League'.

## `countryContains` (type: `array`):

Keep matches from these countries or regions, e.g. 'England'.

## `teamContains` (type: `array`):

Keep matches where either side matches, e.g. 'Arsenal' or 'Djokovic'.

## `includeHeadToHead` (type: `boolean`):

Read previous meetings and recent form for every delivered match. One extra request per match, so it is off by default.

## `includeOdds` (type: `boolean`):

Add the 1/X/2, handicap, over-under and moneyline numbers that travel in the same feed. Off by default.

## `includeRawFields` (type: `boolean`):

Add every raw feed key with the constant name Flashscore's own bundle gives it. Useful for sport-specific values this Actor does not name yet.

## `compactOutput` (type: `boolean`):

Drop empty fields from every row. Smaller CSV, friendlier to AI agents.

## `onlyChanged` (type: `boolean`):

Remember what was delivered and write only new or changed matches. Made for scheduled monitoring.

## `emitUnchanged` (type: `boolean`):

With incremental mode on, still write matches that did not change, tagged UNCHANGED.

## `monitorStoreName` (type: `string`):

Named key-value store that holds the incremental memory. Change it to keep several schedules apart.

## `language` (type: `string`):

Language of tournament and participant names.

## `maxItems` (type: `integer`):

Upper bound on DELIVERED rows, not on rows scanned. 0 means no limit.

## `detailConcurrency` (type: `integer`):

Parallel head-to-head requests.

## Actor input object example

```json
{
  "sports": [
    "football"
  ],
  "dayOffsets": [
    "0"
  ],
  "matchStatuses": [],
  "tournamentContains": [],
  "countryContains": [],
  "teamContains": [],
  "includeHeadToHead": false,
  "includeOdds": false,
  "includeRawFields": false,
  "compactOutput": false,
  "onlyChanged": false,
  "emitUnchanged": false,
  "language": "en",
  "maxItems": 200,
  "detailConcurrency": 8
}
```

# Actor output Schema

## `results` (type: `string`):

All collected rows

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "sports": [
        "football"
    ],
    "dayOffsets": [
        "0"
    ],
    "maxItems": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/flashscore-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "sports": ["football"],
    "dayOffsets": ["0"],
    "maxItems": 200,
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/flashscore-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sports": [
    "football"
  ],
  "dayOffsets": [
    "0"
  ],
  "maxItems": 200
}' |
apify call snow_leo_data/flashscore-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/flashscore-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/llSggatvmwFNYGx5w/builds/OYQpWt8brTXJocdPR/openapi.json
