# Sports Odds Scraper: book-by-book betting odds (`thatmike1/sports-odds-scraper`) Actor

Scrape pre-match betting odds bookmaker by bookmaker across seven sports: 1X2 or moneyline, Asian handicap and totals, with the event, league and start time.

- **URL**: https://apify.com/thatmike1/sports-odds-scraper.md
- **Developed by:** [Michal Pšenčík](https://apify.com/thatmike1) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.005 / actor start

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Sports Odds Scraper (book-by-book pre-match odds)

### What does Sports Odds Scraper do?

This Actor scrapes **pre-match betting odds, one row per bookmaker**, from
**[BetExplorer](https://www.betexplorer.com/)**, across seven sports: football, basketball, tennis, ice hockey,
baseball, volleyball and handball. For every event it returns the **sport**, the **league** split into country and
competition, the exact **start time in UTC**, the **home** and **away** sides, the event's **status**, a link back
to the event page, a stable **event id**, the **scrape timestamp**, and then, for each bookmaker quoting it, the
**moneyline** (1X2 with a draw on three-way sports, home/away on two-way ones), the **spread** (Asian handicap,
with the handicap value) and the **total** (over/under, with the line).

Because every item carries a `scrapedAt` timestamp and a stable `eventId`, running it twice an hour apart gives you
**line movement** without any extra work: join on `eventId` plus `bookmaker` and diff the prices.

**You choose which country's bookmakers you get**, by setting a country on Apify Proxy in `proxyConfiguration`.
Ask for `GB` and you get the UK books (William Hill, bet365, Paddy Power, Betfred, Betway, bwin and the rest); ask
for `DE` and you get the German ones. Full worked example in
[Choosing which bookmakers you get](#choosing-which-bookmakers-you-get) below.

Try it from the **Input** tab: leave everything at its default and hit Start for today's and tomorrow's football
card with every bookmaker on it.

### Why use Sports Odds Scraper?

- **Line shopping.** One item holds every bookmaker's price for the same event, so the best available price and the
  spread between books fall straight out of the data.
- **Line movement and steam detection.** Schedule it every few minutes and diff on `scrapedAt`. The Actor is cheap
  per run precisely so that repeated runs are affordable.
- **Arbitrage and value screens.** Moneyline, Asian handicap and totals for the same event, from the same snapshot,
  in one row.
- **Model inputs.** Closing odds are the standard benchmark for a sports model. Set `status` to `all` to pick up
  events that have already started or finished.
- **Any bookmaker market you want.** Set a country on Apify Proxy and you get that country's licensed books: 15 of
  them on a GB run, against 4 on an unproxied one.
- **Seven sports from one Actor.** No separate integration per sport; change the `sport` input.
- **No odds-feed contract.** Commercial odds APIs are priced for sportsbooks. This reads a public comparison site.

### How to use Sports Odds Scraper

1. Click **Try for free** (or open the Actor in Apify Console).
2. On the **Input** tab, pick a **Sport**.
3. Set **Days** to the day offsets you want. `[0, 1]` is today and tomorrow, `[-1]` is yesterday. The source serves
   seven days either side of today.
4. Pick the **Markets** you need. Dropping Spread and Total makes the run about three times cheaper in platform
   time; the moneyline is always included.
5. Leave **Event status** on `Scheduled` for pre-match odds, or switch to `All events` for closing odds too.
6. Set **Maximum results** to cap the run. The default is 50 events.
7. Click **Start**. A 50-event football run takes about 15 seconds.
8. Open the **Output** tab (or **Storage → Dataset**) and export as JSON, CSV, Excel or HTML, or pull it through
   the [Apify API](https://docs.apify.com/api/v2).

To track line movement, use **Schedules** in Apify Console to run the Actor on a fixed interval and let the dataset
accumulate.

### Input

All fields are optional; the defaults return today's and tomorrow's football card.

| Field                | Type    | Default                          | Description                                                                                                                         |
| -------------------- | ------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `sport`              | string  | `football`                       | One of `football`, `basketball`, `tennis`, `hockey`, `baseball`, `volleyball`, `handball`. One run covers one sport.                |
| `days`               | array   | `[0, 1]`                         | Day offsets from today. `0` today, `1` tomorrow, `-1` yesterday. Seven days either side of today are available.                     |
| `markets`            | array   | `["moneyline","spread","total"]` | Which markets to return per bookmaker. `moneyline` is always included. Each of the other two adds one request per event.            |
| `status`             | string  | `scheduled`                      | One of `scheduled`, `all`, `live`, `finished`. `scheduled` is what "pre-match" means.                                               |
| `maxItems`           | integer | `50`                             | Stop after this many events with odds. `0` means no limit.                                                                          |
| `proxyConfiguration` | object  | Apify proxy disabled             | Optional, but it is what selects the bookmaker market. See [Choosing which bookmakers you get](#choosing-which-bookmakers-you-get). |

Example input:

```json
{
    "sport": "football",
    "days": [0, 1],
    "markets": ["moneyline", "spread", "total"],
    "status": "scheduled",
    "maxItems": 50
}
```

### Output

Each event is one dataset item, with every bookmaker nested inside it. Download the dataset as JSON, HTML, CSV or
Excel.

```json
{
    "eventId": "GzrJ32nL",
    "sport": "football",
    "country": "Spain",
    "competition": "LaLiga",
    "league": "Spain: LaLiga",
    "home": "Real Sociedad",
    "away": "Celta Vigo",
    "startTime": "2026-09-03T19:00:00.000Z",
    "status": "scheduled",
    "eventUrl": "https://www.betexplorer.com/football/spain/laliga/real-sociedad-celta-vigo/GzrJ32nL/",
    "moneylineType": "1x2",
    "bookmakerCount": 4,
    "bookmakers": [
        {
            "bookmaker": "BetMGM.us",
            "moneyline": { "type": "1x2", "home": 1.77, "draw": 3.8, "away": 4.4 },
            "spread": { "handicap": -0.5, "home": 1.7, "away": 2 },
            "total": { "line": 2.5, "over": 1.77, "under": 1.95 }
        },
        {
            "bookmaker": "Fanduel",
            "moneyline": { "type": "1x2", "home": 1.77, "draw": 3.7, "away": 4.6 },
            "spread": null,
            "total": null
        },
        {
            "bookmaker": "Stake.com",
            "moneyline": { "type": "1x2", "home": 1.79, "draw": 3.8, "away": 4.6 },
            "spread": { "handicap": -0.5, "home": 1.79, "away": 2.05 },
            "total": { "line": 2.5, "over": 1.8, "under": 1.98 }
        },
        {
            "bookmaker": "bet365.us",
            "moneyline": { "type": "1x2", "home": 1.75, "draw": 3.9, "away": 4.75 },
            "spread": { "handicap": -0.5, "home": 1.73, "away": 2.08 },
            "total": { "line": 2.5, "over": 1.8, "under": 1.95 }
        }
    ],
    "scrapedAt": "2026-09-03T17:15:51.597Z"
}
```

A smaller competition is often quoted by one bookmaker, and a bookmaker that only posts a moneyline gets `null` for
the markets it does not price:

```json
{
    "eventId": "YVt4vuKl",
    "sport": "football",
    "country": "Algeria",
    "competition": "Ligue 1",
    "league": "Algeria: Ligue 1",
    "home": "El Biar",
    "away": "Olympique Akbou",
    "startTime": "2026-09-03T20:00:00.000Z",
    "status": "scheduled",
    "eventUrl": "https://www.betexplorer.com/football/algeria/ligue-1/js-el-biar-olympique-akbou/YVt4vuKl/",
    "moneylineType": "1x2",
    "bookmakerCount": 1,
    "bookmakers": [
        {
            "bookmaker": "BetMGM.us",
            "moneyline": { "type": "1x2", "home": 2.45, "draw": 2.87, "away": 2.7 },
            "spread": null,
            "total": { "line": 2.5, "over": 2.4, "under": 1.49 }
        }
    ],
    "scrapedAt": "2026-09-03T17:15:51.597Z"
}
```

On tennis, basketball, baseball and volleyball there is no draw, so `moneylineType` is `home-away` and `draw` is
always empty.

#### Data fields

| Field            | Type           | Description                                                                               |
| ---------------- | -------------- | ----------------------------------------------------------------------------------------- |
| `eventId`        | string         | The source's event id. Stable across days and reruns, so use it as your primary key.      |
| `sport`          | string         | The sport this run covered.                                                               |
| `country`        | string | null | Country or region of the competition, e.g. `Spain`.                                       |
| `competition`    | string | null | Competition name, e.g. `LaLiga`.                                                          |
| `league`         | string | null | The two joined as the source prints them, e.g. `Spain: LaLiga`.                           |
| `home`           | string         | Home side.                                                                                |
| `away`           | string         | Away side.                                                                                |
| `startTime`      | string | null | Scheduled start, ISO 8601 in UTC.                                                         |
| `status`         | string         | `scheduled`, `live` or `finished`. Postponed and cancelled events count as `finished`.    |
| `eventUrl`       | string         | Link to the event page on BetExplorer.                                                    |
| `moneylineType`  | string         | `1x2` on three-way sports, `home-away` on two-way ones.                                   |
| `bookmakerCount` | number         | How many bookmakers quoted the event. Always 1 or more; unquoted events are not returned. |
| `bookmakers`     | array          | One object per bookmaker, see below.                                                      |
| `scrapedAt`      | string         | When this run read the odds, ISO 8601 in UTC. Diff on this to get line movement.          |

Each entry of `bookmakers`:

| Field       | Type           | Description                                                                                              |
| ----------- | -------------- | -------------------------------------------------------------------------------------------------------- |
| `bookmaker` | string         | Bookmaker name as the source prints it.                                                                  |
| `moneyline` | object | null | `{ type, home, draw, away }` as decimal odds. `draw` is null on two-way sports.                          |
| `spread`    | object | null | `{ handicap, home, away }`. `handicap` is applied to the home side: `-0.5` means home gives half a goal. |
| `total`     | object | null | `{ line, over, under }`, the over/under line and both prices.                                            |

All odds are **decimal**. Divide 1 by an odd to get the implied probability before the bookmaker's margin.

### Choosing which bookmakers you get

The source shows a visitor the bookmakers licensed in their own country, and it decides that from the IP making the
request. This Actor passes your proxy settings straight through, so **the country you set on Apify Proxy is the
bookmaker market you get back**.

Set it in `proxyConfiguration`:

```json
{
    "sport": "football",
    "days": [0, 1],
    "markets": ["moneyline"],
    "maxItems": 5,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "GB"
    }
}
```

Three runs of the same fixture, Real Sociedad v Celta Vigo on 2026-09-03, differing only in that setting:

| `apifyProxyCountry`              | Bookmakers returned                                                                                                                                   |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| none (Apify's own datacenter IP) | Stake.com, BetMGM.us, bet365.us, Fanduel                                                                                                              |
| `GB`                             | 10bet, 7BetUK, AllBritishCasino, BetUK, BetVictor, Betano.uk, Betfred, Betway, LeoVegasUK, Midnite, Paddy Power, Unibetuk, William Hill, bet365, bwin |
| `DE`                             | Bet365.de, Betano.de, Interwetten.de, Neobet, bet-at-home.de                                                                                          |

The GB run is the one to copy if you want depth: **15 bookmakers on one event**, quoting the home side anywhere
between 1.73 and 1.80, which is exactly the spread a line-shopping screen exists to find. Note the price is still
$0.0025 for that event, the same as the four-bookmaker one, because you are charged per event and not per quote.

Only `GB` and `DE` were tested through Apify Proxy. The mechanism is the same for any country code, but how deep
the list goes is the source's business and varies a lot by market, as the GB and DE rows show. Running the same
request from a Czech ISP (no proxy) returns Betano.cz, Chance.cz, iFortuna.cz and Tipsport.cz, which is the same
behaviour arriving by a different route.

Two practical notes. Country selection needs Apify Proxy's **residential** group, which is metered by traffic on
your Apify plan, so keep `markets` short and `maxItems` sensible when you use it. And if a country turns out to be
served no bookmakers at all, every event is dropped and the run logs a warning naming this as the cause rather than
failing silently.

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

This Actor uses **pay per event** pricing: **$0.0025 per event** returned, plus **$0.005 per run start**. Platform
usage is included, so those two numbers are the whole bill.

An event here is a whole odds grid: every bookmaker, and up to three markets each. The run behind the examples
returned 50 events carrying 92 bookmaker rows and 255 individual market quotes, for **$0.13**. Comparable Store
Actors charge $0.0025 to $0.012 **per odds row**, so the same data costs an order of magnitude more there. A GB run
widens that further, because 15 bookmakers on an event costs the same $0.0025 as 4 do.

**You are never charged for an event no bookmaker quotes.** Those are dropped before they reach the dataset, so
`maxItems` counts events that actually carry odds.

To cap spending on a single run, set a maximum cost in the run options, or use `maxItems`.

### Tips and advanced options

- **Line movement needs two runs, not a special setting.** Schedule the same input every 5 or 15 minutes. Join the
  dataset on `eventId` + `bookmaker` and order by `scrapedAt`.
- **Cut the bill.** Set `markets` to `["moneyline"]` for a pure line-shopping feed. It is one request per event
  instead of three, and the item is smaller.
- **Closing odds.** Set `status` to `finished` and `days` to `[-1]` for yesterday's closing prices.
- **One run covers many days.** `days: [0, 1, 2]` costs one actor start, not three.
- **Which line you get on spreads and totals.** Bookmakers quote a dozen handicaps and totals per event. The Actor
  publishes the **main line**: the one the most bookmakers quote on both sides, with ties going to the most
  balanced pair of prices. That is the line a comparison site shows by default.
- **Not every event has every market.** Small competitions frequently have a moneyline and nothing else, and the
  matching fields come back `null` rather than guessed.
- **Pick the country before you pick the sport.** A GB proxy turns a four-bookmaker event into a fifteen-bookmaker
  one at the same price per event, so it is the single biggest lever on how much data a run returns.

### FAQ, disclaimers and support

**Does this Actor need a login or an API key?**
No. It reads public pages, with no account of any kind.

**Are the odds live in-play odds?**
No. These are the pre-match prices published for an upcoming event, which is what `status: scheduled` selects. With
`status: live` you get events already under way, but the prices are still the ones the comparison site publishes,
not a live in-play feed.

**Why do two runs minutes apart show different prices?**
Because bookmakers move their lines. That is the point of `scrapedAt`. If the *bookmakers themselves* changed
between two runs, that is the proxy country changing, not the odds: see
[Choosing which bookmakers you get](#choosing-which-bookmakers-you-get).

**Can I get a specific bookmaker, like bet365 or William Hill?**
Indirectly. Set the proxy country to a market that bookmaker is licensed in and it appears in `bookmakers`; there is
no per-bookmaker filter, and filtering the output on your side is cheaper anyway, since the price is per event.

**Are the odds decimal, American or fractional?**
Decimal, always, whatever the bookmaker's own site shows.

**Is scraping betting odds legal?**
This Actor reads publicly available pages and collects match facts and published prices, not personal data.
Scraping publicly accessible information is generally legal in the EU and the US, but you are responsible for how
you use the output, including the source's Terms of Service and any database rights that apply where you operate,
and for the gambling regulations in your jurisdiction. Talk to a lawyer if you plan to redistribute the data
commercially.

**Why do I get fewer events than I asked for?**
Events no bookmaker quotes are dropped and not billed, and `maxItems` caps the rest. Small tennis and basketball
circuits in particular are often listed with no odds at all.

**Found a bug, or need something else?**
Open an issue on the **Issues** tab of this Actor and it will be picked up. If you need a market this Actor does
not return (double chance, both teams to score, draw no bet), a different sport, or every handicap line rather
than the main one, say so in an issue.

# Actor input Schema

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

Which sport to price. One run covers one sport.

## `days` (type: `array`):

Which days to scrape, as offsets from today: 0 is today, 1 is tomorrow, -1 is yesterday. The source serves 7 days either side of today.

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

Which markets to return per bookmaker. Moneyline is always included, because it is what identifies a bookmaker as quoting the event at all. Spread and total add one request per event each.

## `status` (type: `string`):

Pre-match odds live on scheduled events. Pick 'All' to also get closing odds on finished ones.

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

Stop after this many events. Set to 0 for no limit. Each event costs one dataset item.

## `proxyConfiguration` (type: `object`):

This is what selects which country's bookmakers you get, because the source shows a visitor the books licensed where they are. Turn on Apify Proxy, pick the RESIDENTIAL group and set a country: GB returned 15 UK bookmakers on a test event, DE returned 5 German ones, and no proxy returns 4 US-facing ones. It is not needed to get through: the source has no anti-bot challenge.

## Actor input object example

```json
{
  "sport": "football",
  "days": [
    0,
    1
  ],
  "markets": [
    "moneyline",
    "spread",
    "total"
  ],
  "status": "scheduled",
  "maxItems": 50,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `odds` (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",
    "days": [
        0,
        1
    ],
    "markets": [
        "moneyline",
        "spread",
        "total"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("thatmike1/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",
    "days": [
        0,
        1,
    ],
    "markets": [
        "moneyline",
        "spread",
        "total",
    ],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("thatmike1/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",
  "days": [
    0,
    1
  ],
  "markets": [
    "moneyline",
    "spread",
    "total"
  ],
  "maxItems": 50
}' |
apify call thatmike1/sports-odds-scraper --silent --output-dataset

```

## MCP server setup

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