Soccer Fixtures Results Scraper - Football Live Scores, Lineups avatar

Soccer Fixtures Results Scraper - Football Live Scores, Lineups

Pricing

$1.99 / 1,000 matches

Go to Apify Store
Soccer Fixtures Results Scraper - Football Live Scores, Lineups

Soccer Fixtures Results Scraper - Football Live Scores, Lineups

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.

Pricing

$1.99 / 1,000 matches

Rating

0.0

(0)

Developer

Snow Leo Data

Snow Leo Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 days ago

Last modified

Share

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.