# Sports Odds API: Pinnacle, Bovada, Kambi + Fair Odds & EV (`meedcaio/sports-odds-api`) Actor

Pre-match odds from Pinnacle, Bovada, Kambi and LeoVegas, matched across books, with no-vig fair odds, EV vs Pinnacle, arbitrage and line movement.

- **URL**: https://apify.com/meedcaio/sports-odds-api.md
- **Developed by:** [Caio Medeiros de Paula](https://apify.com/meedcaio) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.00 / 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

## Sports Odds API: Pinnacle, Bovada, Kambi + Fair Odds & EV

Pre-match odds from **Pinnacle, Kambi, LeoVegas and Bovada**, with the same game matched across books, plus **no-vig fair odds, EV against Pinnacle, arbitrage flags and line movement** between runs.

Built for European and Brazilian football as much as for US sports: Premier League, La Liga, Serie A, Bundesliga, Ligue 1, Champions League, **Brasileirão Série A and B**, MLS, NFL, NCAA football, NBA, MLB and NHL.

### Coverage

| league | Pinnacle | Kambi | LeoVegas | Bovada (opt-in, best effort) |
|---|---|---|---|---|
| NFL, NCAA football | yes | yes | yes | no |
| NBA, NHL, MLB | yes | yes | yes | best effort |
| Premier League, La Liga, Serie A, Bundesliga, Ligue 1, Champions League, MLS | yes | yes | yes | no |
| Brasileirão Série A and B | yes | yes | yes | best effort |

Bovada is off by default. From cloud servers it sometimes sends an empty board for a league it served minutes earlier; when you turn it on, the `STATUS` record tells you on every run whether it answered.

### What you get

One row per **game × market × outcome × bookmaker**:

```json
{
  "eventKey": "nfl|2026-10-02T00:15Z|cleveland-browns|pittsburgh-steelers",
  "startTime": "2026-10-02T00:15:00Z",
  "league": "nfl",
  "home": "Cleveland Browns",
  "away": "Pittsburgh Steelers",
  "book": "bovada",
  "market": "moneyline",
  "outcome": "away",
  "line": null,
  "priceDecimal": 1.69,
  "priceAmerican": -145,
  "impliedProb": 0.5917,
  "fairProb": 0.58,
  "fairSource": "pinnacle",
  "bookMarginPercent": 3.62,
  "isBestPrice": true,
  "evPercent": -1.99,
  "arbitrage": false,
  "previousPriceDecimal": null,
  "isChanged": false,
  "booksInGame": 4
}
```

| field | meaning |
|---|---|
| `eventKey` | Same value for the same game at every book, so you can group by it |
| `market` | `moneyline` (1X2 with draw in soccer), `spread` (Asian handicap in soccer), `total` |
| `line` | Spread points for that side, or the total. `null` for moneyline |
| `fairProb` | Probability with the bookmaker margin removed. Taken from **Pinnacle** when Pinnacle quotes the same line, otherwise from the row's own book |
| `evPercent` | Expected value of the price against Pinnacle's fair probability. Only filled when Pinnacle quotes the same line: no reference, no EV |
| `isBestPrice` | Highest price for that outcome among the books, **on the same line only** (a -3.0 spread is never compared with a -2.5) |
| `arbitrage`, `arbMarginPercent` | Set on the best-priced rows when the best prices across books add up to a guaranteed margin |
| `bookMarginPercent` | The book's overround on that line |
| `previousPriceDecimal`, `previousLine`, `isChanged`, `isNew` | Movement since your previous run (see below) |
| `maxStake` | Pinnacle's maximum risk stake on that line, when published |

### Line movement

The actor keeps the last price of every line in a named key-value store in your account (`sports-odds-line-state` by default). Each run reports what moved since the previous one, including when the main line itself moves (for example from -3.0 to -2.5).

Schedule it every 10 minutes with **Only new or changed lines** on and it becomes a line-movement feed. The first run returns everything.

### Examples

**Every NFL line with EV, next 7 days**

```json
{ "leagues": ["nfl"], "daysAhead": 7 }
```

**Premier League and Brasileirão, moneyline only, next 10 days**

```json
{ "leagues": ["epl", "brasileirao_a"], "markets": ["moneyline"], "daysAhead": 10 }
```

**NBA movement feed (schedule every 10 minutes)**

```json
{ "leagues": ["nba"], "onlyChanges": true }
```

### Reliability

- Every run writes a **`STATUS`** record (key-value store of the run) with one entry per league and book: rows returned, HTTP status, error. A source that returns nothing is reported as a problem, never as an empty success.
- If no odds come back and a source failed, **the run fails**, so Apify's failure notification reaches you when you schedule it.
- One broken source does not stop the others.

### Honest limitations

- **Kambi counts as two books, not ten.** Unibet, Paf, BetRivers and other Kambi brands return identical prices (measured), so they are served as one `kambi` book. LeoVegas runs on Kambi with its own margin and is a separate book.
- **Bovada is opt-in and best effort.** It sends an empty board to cloud servers for the NFL, NCAA, the big European leagues, the Champions League and MLS, so it is never asked for them, and it is intermittent for the rest.
- DraftKings, FanDuel and ESPN are not included in this version.
- Pre-match main lines only: no player props, no alternate lines, no in-play.
- Kambi needs one request per game for spreads and totals, so long `daysAhead` windows make runs slower.
- Pinnacle and Bovada answer only from some countries. The default **US datacenter proxy** handles that; residential proxies are not needed. If your Apify plan does not include the proxy country you pick, the status record says so (HTTP 407).

### Pricing

Pay per result: you are charged per odds row returned. Status records are free.

### Disclaimer

Data comes from the public JSON feeds the bookmakers' own websites load. It is provided for information and research. Not affiliated with any bookmaker. Check the rules that apply where you live before betting.

# Actor input Schema

## `leagues` (type: `array`):

Leagues to collect. Each book is asked only for leagues it carries; the rest show up as a note in the status rows.

## `books` (type: `array`):

Pinnacle is the sharp reference used for fair odds and EV. Kambi and LeoVegas both run on the Kambi platform; LeoVegas carries its own margin. Bovada is best effort: from cloud servers it sometimes sends an empty board, so it is off by default; the STATUS record shows when that happens.

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

Main full-game lines only. Soccer moneyline is 1X2 with a draw; soccer spreads are Asian handicap.

## `daysAhead` (type: `integer`):

Only games starting within this many days. Larger windows mean more requests to Kambi, which needs one request per game for spreads and totals.

## `onlyChanges` (type: `boolean`):

Return only lines that are new or whose price or line moved since your previous run. Schedule the actor (e.g. every 10 minutes) to use it as a line-movement feed. The first run returns everything.

## `includeDerived` (type: `boolean`):

Add no-vig fair probability, best price across books, EV vs Pinnacle and arbitrage flags.

## `maxEventsPerLeague` (type: `integer`):

Earliest games first.

## `stateStoreName` (type: `string`):

Named key-value store in your account that keeps the last price of every line between runs. Use different names for different schedules.

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

Pinnacle and Bovada only answer from some countries (they refuse requests from Germany, for example). A US datacenter proxy is enough; residential proxies are not needed.

## Actor input object example

```json
{
  "leagues": [
    "nfl",
    "nba",
    "epl",
    "mlb",
    "brasileirao_a"
  ],
  "books": [
    "pinnacle",
    "kambi",
    "leovegas"
  ],
  "markets": [
    "moneyline",
    "spread",
    "total"
  ],
  "daysAhead": 3,
  "onlyChanges": false,
  "includeDerived": true,
  "maxEventsPerLeague": 100,
  "stateStoreName": "sports-odds-line-state",
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `odds` (type: `string`):

No description

## `status` (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 = {
    "leagues": [
        "nfl",
        "nba",
        "epl",
        "mlb",
        "brasileirao_a"
    ],
    "books": [
        "pinnacle",
        "kambi",
        "leovegas"
    ],
    "daysAhead": 3,
    "proxyConfiguration": {
        "useApifyProxy": true,
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("meedcaio/sports-odds-api").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 = {
    "leagues": [
        "nfl",
        "nba",
        "epl",
        "mlb",
        "brasileirao_a",
    ],
    "books": [
        "pinnacle",
        "kambi",
        "leovegas",
    ],
    "daysAhead": 3,
    "proxyConfiguration": {
        "useApifyProxy": True,
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("meedcaio/sports-odds-api").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 '{
  "leagues": [
    "nfl",
    "nba",
    "epl",
    "mlb",
    "brasileirao_a"
  ],
  "books": [
    "pinnacle",
    "kambi",
    "leovegas"
  ],
  "daysAhead": 3,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyCountry": "US"
  }
}' |
apify call meedcaio/sports-odds-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,meedcaio/sports-odds-api"
        }
    }
}
```

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/R5mvABfr2I748xShD/builds/0wahhcOfjhYmziNYU/openapi.json
