# Soccer Fixtures Results Scraper - Football Live Scores, Lineups (`snow_leo_data/football-scraper`) Actor

Every football match on a date: 185 leagues and 746 matches in one request. A match card carries 1,415 fields with shot maps, expected goals, player ratings and lineups. Football standings for 17 seasons per league, 433 leagues in 94 countries. Soccer stats scraper, no API key.

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

## Pricing

$1.99 / 1,000 matches

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

## Football Scraper

Live scores, a whole day of fixtures, league tables, squads, shot maps and
player ratings. No API key, no proxy, no browser.

#### One request buys a whole day of football

Ask for a date and the source answers with every match being played anywhere
that day. Measured on 2026-09-12: **185 leagues and 746 matches in a single
request**. Not one request per league, not one per country -- one.

That matters because the same data is usually sold by the page. A scraper
that walks competitions one at a time spends hundreds of requests on what
this one spends one on, and you pay for the difference either in time or in
compute.

#### The day feed is poor on purpose, and that is the whole trick

Here is the number that decides whether a football scraper is worth paying
for. In the day feed a match carries **10 fields** from the source. A fully covered match
card carries **1,415**. A scraper that reads the day feed
and stops -- which is what the cheap ones do -- hands you under one percent
of what the source actually knows about that match.

This actor reads both. `includeDetails` pulls the card, and the card is
**one request that contains every block at once** -- all **9** of them on a
fully covered match: team statistics, lineups
and formations, the shot map with expected goals for every shot, the
momentum graph, per-player ratings, head-to-head, the league table as it
stood at kick-off, the live text ticker, goals, cards, substitutions,
highlights and insights. Asking for nine blocks costs exactly what asking
for one costs. That is a property of the source, and it is why detail here
is cheap where elsewhere it is the expensive part.

From the card the actor also lifts flat columns you can sort on without
opening a nested object: venue with city, country, capacity, surface and
**coordinates as real decimals**, referee, attendance, red cards per side,
and the player of the match with his rating.

#### Seventeen seasons, and the parameter is actually honoured

`season` takes a season the way the source writes it -- `2024/2025` -- and
the source has **17 of them** for the Premier League at about **380 matches**
each. `allSeasons` walks them all.

This is checked rather than assumed. A season parameter that is accepted and
silently ignored is the worst kind of bug: seventeen requests come back with
HTTP 200 and seventeen identical copies of the current season, and nothing
in the output says so. The live test asks for an older season and fails the
build unless the source returns that season's own years.

#### Five league tables, not one

The source keeps **5** tables per league: overall, home only, away only,
by recent form, and by expected goals. A naive parse takes the first and
loses four without noticing. `standingTypes` picks the ones you want and
tags every row with which table it came from, so they cannot collide in your
dataset. Measured on the Premier League: 20 rows in each of the five.

#### The calendar day is not the source's calendar day

The day feed is built in the source's own time zone. A 21:00 kick-off in
Europe belongs to the next day in Sydney and to the same day in London, so a
feed for "Saturday" always carries a tail of matches that are not your
Saturday. Left alone, you pay for them.

`utcOffsetMinutes` tells the actor which day you mean. Rows outside it are
dropped before billing and counted in the run report under `outside_date`,
so the trim is visible rather than silent.

#### Why this source and not the obvious one

The obvious source for football data answers every request from a laptop and
refuses every request from a datacenter. Measured by running a probe inside
Apify on 2026-09-12 (run `6CkradKAfDmNZsO1z`): its JSON API returned
`403 challenge` to **all 9 header variants tried**, including a mobile
user agent, an app user agent and no user agent at all. Its HTML pages load,
but the pages carry no match data -- the site renders on the client and the
client calls the same blocked API.

An actor built on it would pass every test on a developer's machine and hand
every buyer an empty dataset. This one is built on a source that was probed
from inside Apify first and answered 200 there.

#### What you can pull

Seven modes, chosen with one `mode` field.

- **day** -- every match on a date, one request per day, up to 14 days.
- **league** -- a season's fixtures and results, any season, optionally with
  the tables (`includeTable`).
- **standings** -- tables on their own, in any of the five flavours.
- **team** -- one club's matches, optionally with the squad
  (`includeSquad`): position, contract end, club.
- **player** -- player cards with career history, injuries and season stats.
- **match** -- full cards for the match URLs you paste. The id is the number
  after the `#` in a FotMob match link; a bare id works too.
- **search** -- find the id of a club, player or league by name.

The source's own catalogue, which `search` and the ids come from, holds
**433 leagues across 94 countries**.

#### Columns

**104 columns** across the dataset. A match row carries the ids and a
canonical URL; league, league id, parent league id and country code; round
and stage; kick-off as ISO time, as a Unix timestamp and as a calendar date
in your zone; the state as four booleans and as a short and long label; the
score as numbers and as the source's own text; the winner; both teams with
id, short name and crest. With the card it also carries venue, city,
country, capacity, surface, latitude, longitude, referee, attendance, red
cards, and the player of the match with his rating -- plus whichever blocks
you asked for, as nested columns.

Standing rows, team rows, player rows and search rows live in the same
dataset with their own columns and a `row_type` that tells them apart.

#### Monitor mode: you are not billed twice for yesterday

Football data is a monitoring niche. Across the football and live-score
actors in the Apify store the busiest neighbour runs **178 times per user**
\-- people put these on a schedule and read them every few minutes, not
once.

`onlyChanged` remembers what it has delivered, in a **named** key-value
store that survives between runs, and writes only rows that are new or whose
score, state or kick-off time moved. Everything else is skipped and never
charged. Rows are tagged `NEW` or `UPDATED`; `emitUnchanged` writes the rest
too, tagged `UNCHANGED`.

The fingerprint deliberately ignores the card blocks. If it hashed lineups
or statistics, the same match fetched with details and without them would
look changed every run and the monitor would bill you for all of it. Memory
holds **60,000 keys** and drops the oldest first.

An empty dataset from a monitor is correct behaviour, not a failure, so the
run report always names the reason: `nothing-changed`, `filtered-out`,
`source-returned-nothing`, `no-targets`, `source-refused` or
`charge-limit-reached`. The reason also goes to the log as a warning, in
words.

#### Filters that cannot empty your dataset

`maxItems` limits **delivered** rows, not crawled ones. With a filter on,
the crawl keeps going until it has that many rows that actually pass.

This sounds obvious and it is the single most common way a sports scraper
returns nothing on a green run: it stops at thirty collected rows, the
filter removes all thirty, and the buyer pays the start fee for an empty
dataset. A filter here also never drops a row whose field is empty --
absence of data is not a mismatch, so a standings row is not thrown away by
a match-state filter.

#### Inputs

**26** of them. `mode`, `date`, `days`, `utcOffsetMinutes`, `leagueIds`,
`season`, `allSeasons`, `includeTable`, `standingTypes`, `teamIds`,
`includeSquad`, `playerIds`, `startUrls`, `searchQueries`, `includeDetails`,
`statuses`, `leagueContains`, `countryContains`, `teamContains`, `maxItems`,
`onlyChanged`, `emitUnchanged`, `monitorStoreName`, `compactOutput`,
`includeRawData`, `detailConcurrency`.

Anything the actor does not recognise is named in the log and in the report
under `ignored_input` -- never silently replaced with a default, because a
buyer who asked for something that does not exist should not be handed
something else and a bill for it.

#### Honest limitations

Everything a richer rival does that this actor does not:

- **Football only.** Rivals that cover twenty sports cover them by using a
  source that this actor cannot use from a datacenter. The trade was
  deliberate: one sport that works everywhere beats twenty that work only on
  a laptop.
- **No odds.** The source does not publish them on these routes.
- **No search type filter.** `search` returns clubs, players and leagues
  together; you pick from the `row_type` column.
- **No resume-from-run-id.** Restarting one interrupted run by its id is not
  supported. Incremental mode covers the same need differently: it remembers
  across all runs, not within one.
- **No expired-entity events.** Rows that vanish from the source are not
  emitted as a separate type.
- **No MCP or Notion export.** Results land in the dataset and the Apify
  API; piping them onward is your pipeline's job.
- **No proxy option**, because none is needed. If the source ever starts
  refusing datacenter requests, the actor says so in the report rather than
  quietly returning nothing.

#### FAQ

**Do I need an API key or an account anywhere?**
No. Every route this actor uses is the public one the website itself calls.

**Does it need a proxy?**
No. The whole thing runs on the Python standard library against plain
datacenter addresses. That is also why it fits in 1,024 MB.

**How do I find a league, team or player id?**
They are the numbers in the FotMob URL: `/leagues/47/...` is league 47,
`/teams/8650/...` is team 8650. Or run `search` mode with the name and read
the id off the result.

**What is the id of a match?**
The number after the `#` in a match URL. You can paste the whole URL.

**Why did I get matches from the wrong day?**
You should not -- the actor trims them. If a match looks like the wrong day,
check `utcOffsetMinutes`: a late kick-off genuinely belongs to different
calendar days in different zones, and the default is UTC.

**Why is my dataset empty?**
Open the `REPORT` record in the key-value store. It names the reason in one
field and counts what each filter removed. An empty result from a monitor
with `nothing-changed` means it worked.

**How many requests does a day cost?**
One, plus one per match card if you asked for details. A day with details on
every match is therefore one plus the number of matches -- which is why
`maxItems` exists and why cards are read only for rows that will actually be
delivered.

**Can I get a whole season, or every season?**
Yes -- `league` mode with `season`, or `allSeasons` for all 17. That is a
large job, so set `maxItems`.

**What happens if the source stops answering mid-run?**
Everything already collected is written, the run reports `source-refused`,
and the failure is recorded per host. Rate limiting is handled per host
across all threads, not per request.

**Is the data live?**
Matches in progress carry their running score and state, and with
`includeDetails` the live ticker and momentum as well.

#### Every number in this file is checked

`tools/verify_readme.py` reads each figure above back out of the code, the
input schema, the dataset schema or `tools/measurements.json` and fails if
any of them disagrees. `tools/prove.py` then breaks the code on purpose in
dozens of places and requires the test suite to catch every break, so the
checks cannot quietly stop checking.

# Actor input Schema

## `mode` (type: `string`):

day = every match played anywhere on a date, in one request. league = a league season's fixtures and results, any season the source knows. standings = league tables. team = one club's matches and squad. player = player cards. match = full match cards for the URLs you paste. search = find teams, players and leagues by name.

## `date` (type: `string`):

Day to read: YYYY-MM-DD or YYYYMMDD, or today / tomorrow / yesterday, or an offset in days such as +3.

## `days` (type: `integer`):

Read this many consecutive days starting from the date above. One request per day. Up to 14.

## `utcOffsetMinutes` (type: `integer`):

Your time zone, in minutes east of UTC (London summer = 60, New York = -240). A 21:00 kick-off in Europe is a different calendar day in Sydney, and the day feed is built in the source's own zone, so matches that fall outside your day are trimmed before you are charged.

## `leagueIds` (type: `array`):

For league and standings mode. The number in a FotMob league URL - 47 is the Premier League. Full URLs work too.

## `season` (type: `string`):

One season, written the way the source writes it: 2024/2025. Leave empty for the current one. The source honours this parameter - verified, because a parameter that is silently ignored turns seventeen seasons into seventeen copies of this one.

## `allSeasons` (type: `boolean`):

Walk every season the source has for the league instead of the current one. The Premier League has 17, about 380 matches each, so set maxItems.

## `includeTable` (type: `boolean`):

In league mode, also write standings rows for the season.

## `standingTypes` (type: `array`):

The source keeps five tables per league, not one: all, home, away, form and xg (expected goals). A naive parse takes the first and silently loses four.

## `teamIds` (type: `array`):

For team mode. The number in a FotMob team URL. Full URLs work too.

## `includeSquad` (type: `boolean`):

In team mode, also write one row per player with position, contract end and club.

## `playerIds` (type: `array`):

For player mode. The number in a FotMob player URL. Full URLs work too.

## `startUrls` (type: `array`):

For match mode. Paste FotMob match links - the id is the number after the # - or bare match ids.

## `searchQueries` (type: `array`):

For search mode. Names of clubs, players or leagues.

## `includeDetails` (type: `array`):

The match card is ONE request and carries every block, so asking for nine costs exactly what asking for one costs. stats, lineup, shotmap (shot by shot with expected goals), momentum, playerStats (per-player ratings), h2h, table, liveticker, matchFacts. Blocks the source does not have for a match are left out instead of failing the run.

## `statuses` (type: `array`):

Keep only matches in these states: upcoming, live, finished, cancelled. Rows with no state, such as standings and players, are never dropped by this.

## `leagueContains` (type: `array`):

Keep only matches whose league name contains one of these words.

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

Keep only rows whose country or three-letter code contains one of these words.

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

Keep only matches where one of the teams matches one of these words.

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

Upper bound on DELIVERED rows, not on rows crawled. With a filter on, the crawl keeps going until it has this many rows that actually pass, so a small limit plus a narrow filter cannot hand you an empty dataset.

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

Remember delivered rows between runs and write only new ones and ones whose score, state or kick-off time moved. Built for scheduled monitoring: without it a daily run pays for yesterday's results again.

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

In incremental mode, write unchanged rows too, tagged UNCHANGED.

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

Separate memory for separate watches. Two schedules on two leagues should not share one memory.

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

Drop empty fields from every row. Smaller files and less context for AI agents.

## `includeRawData` (type: `boolean`):

In match mode, attach the untouched JSON the source returned.

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

Eight read a hundred cards without a single rate limit; sixteen was already throttled.

## Actor input object example

```json
{
  "mode": "day",
  "date": "today",
  "days": 1,
  "utcOffsetMinutes": 0,
  "allSeasons": false,
  "includeTable": false,
  "standingTypes": [
    "all"
  ],
  "includeSquad": false,
  "includeDetails": [],
  "maxItems": 100,
  "onlyChanged": false,
  "emitUnchanged": false,
  "compactOutput": false,
  "includeRawData": false,
  "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 = {
    "date": "today"
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/football-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 = { "date": "today" }

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/football-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 '{
  "date": "today"
}' |
apify call snow_leo_data/football-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,snow_leo_data/football-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/GayjlX14V2qfjWud9/builds/MehkXHvVc1FY4CIdq/openapi.json
