# Betting Odds API: Pinnacle, Unibet & Kambi Scraper (`s-r/sports-odds-scraper`) Actor

Betting odds API and sports odds scraper: pre-match and live moneyline, spread and totals from Pinnacle and Kambi bookmakers, in decimal and American prices, no API key needed.

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

## Pricing

$3.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

## Betting Odds API and Sports Odds Scraper for Pinnacle and Kambi

Looking for a betting odds API without a monthly plan or an API key? This sports odds scraper
collects pre-match and live moneyline, spread and totals from Pinnacle and Kambi bookmakers
(Unibet, Paf, BetRivers and others) and returns one clean dataset. Every price comes as decimal and
American odds with source event identities and retrieval timestamps.

Use the actor to compare bookmaker offerings, maintain your own market snapshots or prepare inputs
for a sports data model. The output keeps match context beside the prices so you can filter by sport,
competition, start time and live status without a second lookup. Each run collects the currently
available offering for one competition. It does not place bets or return a historical price archive.

### Betting odds API coverage

The actor reads Pinnacle and supported Kambi offerings, including Unibet regional offerings, Paf
and BetRivers regional offerings. Regional offerings are separate bookmaker selections because their
prices and availability can differ. Supported sports are soccer, basketball, American football,
baseball, ice hockey and tennis. The output uses `football` as the canonical name for soccer.
Availability depends on the competition and the bookmaker at the time of collection.

The bookmaker selector lists the available input values. For example, `unibet` selects the general
Unibet offering, `unibet_nl` selects Unibet NL, and `betrivers_nj` selects BetRivers NJ. The actor does
not support 888. The operator labels `leose`, `atg` and `svenskaspel` are preserved as supplied rather
than expanded into an unverified brand name. You can select up to eight offerings together.

### Input

Start with the following competition selection:

```json
{"sport":"soccer","league":"premier league","bookmakers":["pinnacle","unibet"],"markets":["moneyline","spread","total"]}
```

`league` selects a competition by name. Premier League and EPL select England. Use a country-qualified
name when a competition title is shared across countries. Ambiguous selections return candidate names
instead of choosing a competition for you. `sport` limits competition discovery to the selected sport.

`include_live` defaults to true and includes ongoing matches alongside pre-match events.
`include_alternate_lines` defaults to false. Enable it to place alternate handicaps and totals in the
same market row as the main line. `periods` defaults to `full_game`; `all` also includes periods whose
meaning can be verified. Unsupported period codes are omitted and reported in warnings.

`max_events` defaults to fifty and accepts values from one to two hundred. It limits source events
per bookmaker across the complete collection. It is not a dataset row limit: each event may produce
a match row and several market rows. Larger collections are followed automatically within the actor's
collection budget. Direct API users can pass the returned continuation cursor with unchanged options.

### Output

The dataset contains two record types identified by both `row_type` and `_type`:

- `match` contains the sport, stable `match_id`, participant names, competition, UTC start time and
  live status. Scores and a live clock are included only when the source provides them.
- `odds` contains a bookmaker market for one match and period. `outcomes` holds its selections,
  side-specific lines, decimal `current` prices, American prices and availability flags.

Moneyline markets use `home_away` for two outcomes or `1x2` when a draw is offered. Spreads use
`asian_handicap`; match totals use `over_under`. The `market_type` field preserves the simpler input
names moneyline, spread and total. Full-game markets use `period: "match"`.

A suspended selection has a null price, never zero. A market with no current priced selection is
omitted. Opening prices are not supplied by these feeds and remain null. `fetched_at` records when
the snapshot was retrieved. Kambi also supplies `price_time` when it publishes a price-change time;
Pinnacle's version counter is preserved separately and is not treated as a timestamp.

Pinnacle maximum risk stakes are included where present, without assuming a currency. Alternate-line
outcomes retain their own source market keys and limits. The row-level maximum stake describes its
main line. Kambi overtime inclusion is derived from the source's settlement lifetime; an unknown
settlement scope remains null instead of being inferred.

#### Example output

This is a real Pinnacle moneyline row from a Premier League run (the draw outcome is left out here
for length; the actual row carries all three):

```json
{
  "source": "sports-odds-scraper",
  "fetched_at": "2026-10-30T02:51:59.452Z",
  "sport": "football",
  "match_id": "pinnacle:1638686116",
  "home": "Chelsea",
  "away": "Manchester United",
  "start_time": "2026-10-31T12:30:00.000Z",
  "competition": "England - Premier League",
  "is_live": false,
  "_type": "odds",
  "row_type": "odds",
  "row_id": "pinnacle:1638686116:moneyline:match",
  "bookmaker": "Pinnacle",
  "bookmaker_id": "pinnacle",
  "bookmaker_event_id": "1638686116",
  "market": "1x2",
  "market_type": "moneyline",
  "period": "match",
  "outcomes": [
    {
      "name": "home",
      "line": null,
      "opening": null,
      "current": 1.84,
      "price_american": -119,
      "price_time": null,
      "status": "open",
      "is_main_line": true,
      "market_key": "s;0;m",
      "max_stake": 4500
    },
    {
      "name": "away",
      "line": null,
      "opening": null,
      "current": 4.14,
      "price_american": 314,
      "price_time": null,
      "status": "open",
      "is_main_line": true,
      "market_key": "s;0;m",
      "max_stake": 4500
    }
  ],
  "includes_overtime": null,
  "max_stake": 4500,
  "market_key": "s;0;m",
  "version": 3785569104
}
```

### Match identity and main lines

Matches from different feeds remain separate records because the feeds do not share event IDs.
`match_key` is a grouping hint based on the UTC start quarter-hour and normalized participant names.
It is not proof that two matches are identical. Unmatched names remain visible under their own source
identities. Pinnacle live child events retain the parent match ID, while their actual retrieval ID
remains available as `bookmaker_event_id`.

Pinnacle marks alternate lines explicitly. For Kambi, the actor prefers the ordinary handicap and
total offerings, then a source main-line tag. If that tag is absent, it selects the line whose two
prices are closest together. `is_main_line` distinguishes that selection from the alternate lines.
Corners, cards, shots, player props, team totals and draw-no-bet markets are excluded.

### Use cases

- **Betting models:** feed Pinnacle's sharp line into a pricing model as a closing-line reference,
  with decimal prices ready to use and American prices alongside.
- **Odds comparison and affiliate sites:** show Unibet, Paf and BetRivers regional prices next to the
  sharp line for the same competition, grouped through `match_key`.
- **Line shopping and arbitrage research:** compare the same market across bookmakers in one
  dataset, with maximum stakes from Pinnacle where the source provides them.
- **Market snapshots:** schedule repeated runs and build your own price history, using
  `fetched_at` and `price_time` to order the snapshots.
- **Live monitoring:** keep `include_live` on to capture ongoing matches with their live status.

### How this compares with other odds APIs and scrapers

Hosted betting odds APIs usually sell a subscription with request quotas and an API key per
account. This actor works per run and charges per delivered market row, so a small competition costs
a small amount and an empty result costs nothing. The free tier lets you test with real rows first.

Compared with other odds scrapers on the Store, the focus here is the sharp line: Pinnacle together
with the Kambi operator network in one normalized format, rather than US sportsbooks only. Match
rows and market rows are separate record types, so you can join prices to fixtures without parsing
event names. The actor is honest about its limits: it does not supply opening prices, a price
history or bookmakers it cannot read, and it reports a partial run instead of hiding it.

### Pricing

One billable result is one delivered `odds` row: a bookmaker market for a match and period, including
its nested outcomes. Match context rows are free. Adding alternate outcomes does not create additional
market rows. The current monetary price is shown in the actor's Pricing tab. Empty results do not
create an odds charge, and the actor has no separate custom start event.

Free usage is capped at ten dataset rows, including match context and odds rows. This usually covers
only part of a competition. The selection limit and free dataset cap describe different quantities:
requesting ten events does not guarantee delivery of all their market rows under the free allowance.

### Collection status

The run output includes warnings and a partial flag. If one bookmaker is unavailable while another
returns data, the available results are delivered and the missing source is identified. Review these
notes before treating a snapshot as a complete comparison. A competition can be temporarily empty,
and live prices may change between requests. Repeated runs create separate snapshots that you can
retain and compare yourself.

### FAQ

#### Can I collect live odds?

Yes. Keep `include_live` enabled. Live rows are marked explicitly. A competition without ongoing
matches returns its available pre-match offering; enabling live does not create live events.

#### Why can the same match appear twice?

Pinnacle and Kambi use separate identities. Each feed gets its own match row, while Kambi operators
share a match identity. Use the source IDs and grouping hint according to your matching requirements.

#### Are decimal and American prices both included?

Yes, when a source price is available. Pinnacle American prices are converted to decimal and rounded
to two decimal places. Kambi decimal prices retain the precision supplied in its offering. Suspended
selections remain null rather than receiving a calculated or invented quote.

#### Does this actor provide opening odds or history?

No. Opening price fields remain null. You can schedule repeated runs and build a history from the
retrieved snapshots and their timestamps.

# Actor input Schema

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

Sport. soccer is an alias for the canonical football output value.

## `league` (type: `string`):

Competition name. Premier League and EPL select England; NBA, NHL and country-qualified names are accepted. Ambiguous names return candidates.

## `bookmakers` (type: `array`):

Up to eight bookmaker offerings. 888 is not a supported Kambi operator. Operator codes leose, atg and svenskaspel retain their source labels.

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

Moneyline, two-way spread and match totals. Player props, corners, cards and team totals are excluded.

## `include_live` (type: `boolean`):

Include ongoing events and their current markets alongside pre-match events.

## `include_alternate_lines` (type: `boolean`):

Include alternate lines as additional nested outcomes in each market row; is_main_line identifies the selected main line.

## `periods` (type: `string`):

full_game returns match markets. all also returns periods with verified mappings. Unsupported period codes are omitted with a warning.

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

Maximum source events per bookmaker for the whole collection. Each request handles up to twenty per bookmaker; continue using cursor. This is not a row limit.

## `cursor` (type: `string`):

Opaque continuation token. Reuse all input options unchanged. Apify follows it automatically; direct API clients follow it themselves.

## Actor input object example

```json
{
  "sport": "soccer",
  "league": "premier league",
  "bookmakers": [
    "pinnacle",
    "unibet"
  ],
  "markets": [
    "moneyline",
    "spread",
    "total"
  ],
  "include_live": true,
  "include_alternate_lines": false,
  "periods": "full_game",
  "max_events": 50,
  "cursor": ""
}
```

# Actor output Schema

## `results` (type: `string`):

Match rows and bookmaker market rows with nested selections.

## `output` (type: `string`):

Run counts, partial status and warnings.

## `errors` (type: `string`):

English error messages with stable error codes.

# 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 = {
    "league": "premier league"
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = { "league": "premier league" }

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "league": "premier league"
}' |
apify call s-r/sports-odds-scraper --silent --output-dataset

```

## MCP server setup

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