# Sports Betting Odds Scraper (`mina_safwat/sports-odds-scraper`) Actor

Collects pre-match and live betting odds from many bookmakers for football, tennis, basketball and 20+ other sports from Flashscore's odds comparison, with opening odds and price movement, to compare prices and track how they move.

- **URL**: https://apify.com/mina_safwat/sports-odds-scraper.md
- **Developed by:** [Mina](https://apify.com/mina_safwat) (community)
- **Categories:** Sports, Automation, Developer tools
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 odds 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

### What does Sports Betting Odds Scraper do?

**Sports Betting Odds Scraper** collects **betting odds from many bookmakers at once** from [Flashscore](https://www.flashscore.com)'s public odds comparison, for **football, tennis, basketball, hockey, baseball, American football and 20 more sports**. For every match it gives each bookmaker's **current price, opening price and how far the price has moved**, for markets such as **1X2, home/away (moneyline), over/under, Asian handicap and both teams to score**, before the match and, if you turn it on, **live (in-play)** for matches being played.

Paste a match or competition link from Flashscore, or just pick a sport and a day. Try it with the prefilled input: it reads the 1X2 odds of three of today's football matches and finishes in a few seconds. Because it runs on the Apify platform, you also get API access, scheduled runs, integrations (Google Sheets, Make, Zapier, webhooks) and run monitoring.

### Why scrape betting odds?

- **Compare prices across bookmakers.** One row per bookmaker and market line puts every bookmaker's price for the same outcome side by side.
- **Track odds movement.** Each price comes with the bookmaker's opening price and the change in percent; schedule the Actor to take regular snapshots and see how the market moves towards kick-off.
- **Sports analytics and modelling.** Use market prices as a benchmark for prediction models, or study how closing prices relate to results. Finished matches keep their closing odds.
- **Research and journalism.** Collect what the market expected for a match, a league or a whole day.

The Actor only reads publicly displayed odds. It does not place bets, and it does not collect bonus offers or promotional content.

### How to scrape betting odds from Flashscore

1. Open a match on Flashscore (any country version) and copy the link from the address bar, or copy a league page such as `https://www.flashscore.com/football/england/premier-league/`.
2. Paste it into **Match links** or **Competition links**, one per line. To read a whole day instead, leave both empty and pick a **Sport** and **Day**.
3. Choose where the bookmakers should be licensed (**Bookmakers licensed in**), the **Markets** and **Periods** you need.
4. Click **Start**, then download the odds when the run finishes.

Odds follow the bookmakers Flashscore shows in each country: about 20 for the United Kingdom, fewer elsewhere, and by state in the US.

### Input

Everything is set on the Input tab. In JSON, the input looks like this:

```json
{
    "match_urls": [
        "https://www.flashscore.com/match/football/arsenal-hA1Zm19f/leeds-tUxUbLR2/?mid=xtmHKGT0"
    ],
    "league_urls": [
        "https://www.flashscore.com/basketball/usa/nba/"
    ],
    "bookmaker_country": "GB",
    "markets": ["1x2", "home_away", "over_under", "asian_handicap", "both_teams_to_score"],
    "periods": ["full_time"],
    "all_lines": false,
    "include_live_odds": false,
    "max_events": 20
}
```

| Field | What it does |
|---|---|
| `match_urls` | Flashscore match links from any country site (flashscore.co.uk, flashscore.de, livesport.cz…), with or without `https://`, or the bare 8-character match id. Upcoming, live and finished matches all work; finished ones give the closing odds. |
| `league_urls` | League or tournament pages (a team page works too). The Actor reads the upcoming matches in kick-off order. Names always come out in English, whichever country site the link is from. |
| `sport`, `day` | Used only when the input has no link at all (API callers may also send links as `startUrls` or `urls`; they are read as links, never ignored): every match of that sport on `today`, `tomorrow`, `yesterday` or a date such as `2026-10-12` (up to 7 days either way, UTC days). Matches still to be played or in play come first, then finished ones. |
| `bookmaker_country` | Where the bookmakers are licensed: a country code such as `GB`, `DE` or `NG`, a US state such as `US-NJ`, `CA-ON` for Ontario, or `INT` for the international set. Default: `GB`. Countries where Flashscore switches its odds comparison off (France, the Netherlands, Australia and most US states) are not offered. |
| `markets` | `1x2`, `home_away`, `over_under`, `asian_handicap`, `both_teams_to_score`, `double_chance`, `draw_no_bet`, `european_handicap`, `to_qualify`, `odd_even`, `correct_score`, `half_time_full_time` and `next_goal` (live only). Default: the first five. |
| `periods` | `full_time` (also covers prices that include overtime), `first_half`, `second_half`, `first_period`, `first_quarter`, `first_set`, `second_set`. Default: `full_time`. |
| `all_lines` | Off (default): only each over/under and handicap market's main line, the one quoted by the most bookmakers with the most even prices. On: every line. |
| `include_live_odds` | Off by default. On: for matches in play, also save each bookmaker's in-play prices, about 20 more rows per live match. |
| `max_events` | For each competition link and for the day: stop after this many matches with odds. Default: 20. Matches without odds don't count, and match links are always all read. |
| `proxy_configuration` | Usually not needed. The Actor connects directly and falls back to Apify's proxies on its own. |

### Output

Each row is one match, one bookmaker, one market and one line. Here are two real rows (bet365's 1X2 and over/under 2.5 prices for Arsenal v Leeds):

```json
[
    {
        "event_id": "xtmHKGT0",
        "event_url": "https://www.flashscore.com/match/football/arsenal-hA1Zm19f/leeds-tUxUbLR2/?mid=xtmHKGT0",
        "sport": "football",
        "country": "England",
        "league": "Premier League",
        "home_team": "Arsenal",
        "away_team": "Leeds",
        "start_time": "2026-10-10T11:30:00+00:00",
        "status": "scheduled",
        "bookmaker": "bet365",
        "bookmaker_id": 16,
        "bookmaker_country": "GB",
        "odds_type": "prematch",
        "market": "1x2",
        "period": "full_time",
        "home": 1.4,
        "home_opening": 1.25,
        "home_change_pct": 12,
        "draw": 4.75,
        "draw_opening": 5.5,
        "draw_change_pct": -13.6,
        "away": 8,
        "away_opening": 12,
        "away_change_pct": -33.3,
        "is_active": true,
        "scraped_at": "2026-10-10T07:33:14+00:00"
    },
    {
        "event_id": "xtmHKGT0",
        "home_team": "Arsenal",
        "away_team": "Leeds",
        "bookmaker": "bet365",
        "odds_type": "prematch",
        "market": "over_under",
        "period": "full_time",
        "line": 2.5,
        "line_unit": null,
        "over": 1.8,
        "over_opening": 1.67,
        "over_change_pct": 7.8,
        "under": 2,
        "under_opening": 2.2,
        "under_change_pct": -9.1,
        "is_active": true
    }
]
```

The second row is shortened; every row carries the full match details. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel. The Output tab has two views: **Odds** (prices side by side) and **Odds movement** (current, opening and change for each outcome).

### Data fields

| Field | Description |
|---|---|
| `event_id`, `event_url` | Flashscore's match id and the match page. |
| `sport`, `country`, `league` | The sport, the country (or tour, such as `ATP - SINGLES`) and the competition. |
| `home_team`, `away_team` | Team or player names; doubles pairs are joined with `/`. |
| `start_time` | Kick-off time in UTC (ISO 8601). |
| `status` | `scheduled`, `live`, `finished`, or how an unplayed match ended (`postponed`, `cancelled`, `walkover`…). |
| `bookmaker`, `bookmaker_id` | The bookmaker as Flashscore names it, and its id. |
| `bookmaker_country` | The `bookmaker_country` setting the row was collected with. |
| `odds_type` | `prematch` (before kick-off; once a match has started these are the closing prices) or `live` (in-play). |
| `market` | The market, as named in the `markets` input. |
| `period` | `full_time`, `full_time_incl_overtime`, `first_half`, `second_half`, `first_period`, `first_quarter`, `first_set` or `second_set`. |
| `line` | Over/under and handicap markets only: the total, or the home side's handicap (`-1.5` means the home side gives 1.5). |
| `line_unit` | Over/under and handicap markets only: `games`, `sets`, `points`… when Flashscore names the unit; `null` when it does not (most football lines). |
| `outcome` | Correct score and half time/full time only: the score (`2:1`) or result pair (`X/1`). These markets have one row per outcome. |
| `home`, `draw`, `away` | Decimal odds for 1X2, home/away, draw no bet, to qualify, handicaps and (live) next goal. |
| `over`, `under`, `yes`, `no`, `odd`, `even` | Decimal odds for over/under, both teams to score and odd/even. |
| `home_or_draw`, `home_or_away`, `draw_or_away` | Decimal odds for double chance. |
| `no_goal` | Next goal (live) only: the price of no further goal. |
| `odds` | Correct score and half time/full time: the price of `outcome`. |
| `<outcome>_opening` | The opening price for that outcome, such as `home_opening`. For live rows, the price when in-play betting began, close to the pre-match closing price. |
| `<outcome>_change_pct` | Change from the opening price in percent: negative means the price has shortened. |
| `is_active` | `false` when the bookmaker has suspended or closed any outcome of the row. |
| `scraped_at` | When the row was collected, in UTC. |

Only the fields that apply to a row's market are present: a 1X2 row has no `line`, an over/under row has no `home`.

### How much does it cost to scrape betting odds?

The Actor uses **pay-per-event** pricing: you pay for the rows saved (see the **Pricing** tab for the current price), plus a small amount of Apify platform usage. Runs are quick: three matches take about 5 seconds, forty matches about 10.

A row is one bookmaker's prices for one market of one match (one line of it, for over/under and handicaps), so the cost of a match is roughly its number of bookmakers times the markets you pick. Examples at **$1.50 per 1,000 rows**:

| What you read | Rows | Cost at $1.50 per 1,000 rows |
|---|---|---|
| 1X2 only, a football match with UK bookmakers (the prefilled input reads three) | about 20 per match | about $0.03 per match |
| The five default markets, a Premier League match with UK bookmakers | 67 | about $0.10 |
| The same with live odds on, while the match is in play | 75 to 87 | about $0.12 |
| The five default markets, an NBA game with New Jersey bookmakers | 12 | about $0.02 |
| 20 football matches of a day, default markets (a default run) | about 1,000 | about $1.50 |

Smaller competitions have fewer bookmakers: 40 football matches with the default markets gave 2 to 71 rows each (52 on average). Rows are never saved for links or matches without odds, so you are not charged for them, and if you set a maximum cost for a run, the Actor stops as soon as it is reached.

### Tips for scraping odds

- **Pick only the markets you need.** Each market adds a row per bookmaker and line; correct score adds 30 to 40 rows per bookmaker. Live odds add about 20 rows per match in play.
- **Keep `all_lines` off** unless you need the whole ladder of over/under and handicap lines: it can multiply rows five to tenfold.
- **Track movement with a schedule.** Run the same links every hour or every day; compare prices across runs with `scraped_at`.
- **Compare regions** by running the same matches with different `bookmaker_country` values: each country sees different bookmakers.
- **Closing odds:** run on `yesterday` or paste links to finished matches to get the last prices before kick-off.
- Pasting the same match twice, or a match that is also in a pasted league, reads it once.

### FAQ, disclaimers and support

**Which bookmakers are covered?** The ones Flashscore shows for the chosen country, such as bet365, Betfred, Ladbrokes, Sky Bet and William Hill in the United Kingdom, or FanDuel, DraftKings and BetMGM in New Jersey. The list changes with Flashscore's partnerships and local licensing.

**Why do some matches return no odds?** Smaller competitions are often not priced by the bookmakers of every country, and matches that were cancelled or ended in a walkover usually have none (a postponed match can keep its odds). Links that give nothing are listed with the reason in the `FAILED_INPUTS` record of the run's key-value store, and the run's status message gives the counts. A run fails only when Flashscore could not be reached for any input.

**How fresh are the odds?** They are the prices Flashscore showed when the row was collected (`scraped_at`). Bookmakers change prices often, especially in play, so always check the current price before relying on it.

**Is it legal to scrape betting odds?** This Actor reads odds that Flashscore displays publicly to every visitor and collects no personal data: only match, team and bookmaker information. You are responsible for making sure your use complies with Flashscore's terms and with the laws that apply to you, including local rules on betting. The data is meant for analysis and research. If in doubt, ask a lawyer.

**Found a problem or need a custom solution?** Open an issue on the **Issues** tab and we will look into it. Custom data feeds are available on request.

# Actor input Schema

## `match_urls` (type: `array`):

Flashscore match links, one per line, from flashscore.com or any of its country sites (flashscore.co.uk, flashscore.de, livesport.cz, diretta.it…). Open a match and copy the address bar: links ending in ?mid=… and older /match/<id>/ links both work, and so does the bare 8-character match id. Upcoming, live and finished matches all work (finished ones give the closing odds).

## `league_urls` (type: `array`):

Flashscore league or tournament pages, such as https://www.flashscore.com/football/england/premier-league/ or https://www.flashscore.com/basketball/usa/nba/. The Actor reads the competition's upcoming matches in kick-off order, up to "Max events" per link.

## `sport` (type: `string`):

The sport whose matches to read for the day below.

## `day` (type: `string`):

today, tomorrow, yesterday or a date such as 2026-10-12, up to 7 days either way. Days run midnight to midnight UTC. Matches still to be played (or in play) are read first, in kick-off order, then finished ones.

## `bookmaker_country` (type: `string`):

Flashscore shows the bookmakers licensed where its visitor is, so this picks the set: about 20 for the United Kingdom, a handful for most countries. In the US and Canada it goes by state or province. Countries where Flashscore switches its odds comparison off (France, the Netherlands, Australia and most US states) are not listed. International is the set shown to visitors from unlisted countries.

## `markets` (type: `array`):

Which betting markets to save. A sport only has some of them: football has 1X2 but no Home/Away, tennis and basketball the other way round. Each market adds a row per bookmaker (and per line), so fewer markets means fewer rows; left empty, the five main markets are read. Correct score adds 30 to 40 rows per bookmaker and match.

## `periods` (type: `array`):

Which part of the match the odds are for. Full time also covers markets priced including overtime, such as the hockey and basketball moneyline.

## `all_lines` (type: `boolean`):

Over/under and handicap markets come in many lines (0.5, 1.5, 2.5 goals…). Off: only each market's main line, the one quoted by the most bookmakers with the most even prices, so you get one comparable row per bookmaker. On: every line, often 5 to 10 times the rows.

## `include_live_odds` (type: `boolean`):

For matches in play when the run reaches them, also save each bookmaker's in-play prices (rows with odds_type live): about 20 more rows per live match with the default markets. Off by default. Pre-match odds (the closing prices, once a match has started) are saved either way.

## `max_events` (type: `integer`):

For each competition link and for the day: stop after this many matches with odds. Matches without odds are skipped and don't count. Match links are always all read.

## `proxy_configuration` (type: `object`):

Leave this off: Flashscore answers the Actor directly, and if it ever refuses, the Actor retries through Apify's datacenter and then residential proxies on its own.

## Actor input object example

```json
{
  "match_urls": [],
  "league_urls": [],
  "sport": "football",
  "day": "today",
  "bookmaker_country": "GB",
  "markets": [
    "1x2",
    "home_away"
  ],
  "periods": [
    "full_time"
  ],
  "all_lines": false,
  "include_live_odds": false,
  "max_events": 3,
  "proxy_configuration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# 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 = {
    "sport": "football",
    "day": "today",
    "bookmaker_country": "GB",
    "markets": [
        "1x2",
        "home_away"
    ],
    "periods": [
        "full_time"
    ],
    "max_events": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("mina_safwat/sports-odds-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 = {
    "sport": "football",
    "day": "today",
    "bookmaker_country": "GB",
    "markets": [
        "1x2",
        "home_away",
    ],
    "periods": ["full_time"],
    "max_events": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("mina_safwat/sports-odds-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 '{
  "sport": "football",
  "day": "today",
  "bookmaker_country": "GB",
  "markets": [
    "1x2",
    "home_away"
  ],
  "periods": [
    "full_time"
  ],
  "max_events": 3
}' |
apify call mina_safwat/sports-odds-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mina_safwat/sports-odds-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/Y4qxXqlFp4wyvv3nr/builds/eV3MIZkufIU5rij9b/openapi.json
