# Pinnacle Odds Scraper (`automatic/pinnacle-odds`) Actor

Collect Pinnacle pre-match odds for the NFL, NBA, NHL, MLB, college sports, soccer and UFC: moneyline, spread and total per game with American and decimal odds, implied probability and the published stake limit. Optional alternate lines, periods, team totals, props and futures.

- **URL**: https://apify.com/automatic/pinnacle-odds.md
- **Developed by:** [Philip Kirkbride](https://apify.com/automatic) (community)
- **Categories:** Sports
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 market records

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

## Pinnacle Odds Scraper

Collect Pinnacle's pre-match odds for the NFL, NBA, NHL, MLB, college football
and basketball, 18 soccer leagues and UFC. Each record is one market of one
game: the moneyline, the spread or the total, with both sides priced in
American and decimal odds, the implied probability, and the largest stake
Pinnacle shows for that market. Optional extras: every alternate line, halves,
quarters and periods, team totals, game and player props, and futures boards.
Built for bettors, modellers and odds-comparison builders who use Pinnacle as
the sharp reference price and want it as plain JSON on a schedule. No Pinnacle
account is needed.

Data only, not betting or financial advice. This is an unofficial tool: it is
not affiliated with or endorsed by Pinnacle.

### Use cases

- **Reference prices:** Pinnacle's main moneyline, spread and total for every
  listed game of a league, to compare against other sportsbooks, exchanges or
  prediction markets.
- **Line tracking:** schedule runs and chart how the main line and its price
  move before the start. A market at a new line is a new record, so a moved
  line is visible as such.
- **Fair-price models:** both sides of every market arrive in one record, so
  removing the margin is one step; stake limits show how firm a price is.
- **Alternate lines and periods:** the full ladder of spreads and totals, and
  first-half, quarter and period markets, each as its own record.
- **Cross-source joins:** every record carries the shared `canonical` envelope
  our other sports Actors use, with explicit home and away, league, sport and
  start time.

### Quick start

1. Open the Actor and click **Start**. The prefilled form collects the main
   moneyline, spread and total of every listed NFL and NBA game in the next
   14 days, capped at 100 records.
2. Read the rows in the **Output** tab, or export them as JSON, CSV or Excel.

One soccer league, main lines:

```json
{"leagues": ["epl"], "dateFrom": "2026-11-21", "dateTo": "2026-11-30", "maxResults": 20}
```

Upcoming NBA spread and total lines, including first-half markets (20-record cap):

```json
{"leagues": ["nba"], "marketFamilies": ["spreads", "totals"], "periods": ["game", "first_half"], "mainLineOnly": false, "maxResults": 20}
```

Player and game props and futures for one league:

```json
{"leagues": ["nhl"], "marketFamilies": ["props", "futures"], "maxResults": 20}
```

An empty input (`{}`) is a sample of at most 20 current main-line records.
The status message says "Sample run (...)" and the run summary has
`sampleRun: true`. It uses NFL and NBA in September–May, and MLB and EPL in
June–August, for today and the next 14 UTC days. If those leagues have no
markets, the run reports that honestly; source access failures fail the run.
Setting a filter, option or limit explicitly collects that requested scope
with `sampleRun: false`. Proxy settings alone still select a sample.

### Input

| Name | Type | Default | What it does |
| --- | --- | --- | --- |
| `leagues` | array | NFL and NBA | Leagues to collect: `nfl`, `ncaaf`, `nba`, `wnba`, `ncaab`, `ncaaw`, `mlb`, `nhl`, `epl`, `laliga`, `seriea`, `bundesliga`, `ligue_1`, `ucl`, `uel`, `mls`, `liga_mx`, `brasileirao`, `argentina_primera`, `eredivisie`, `primeira_liga`, `belgian_pro_league`, `scottish_premiership`, `super_lig`, `chile_primera`, `j1_league`, `ufc`. |
| `leagueIds` | array | none | Any other competition by Pinnacle's own numeric league id, for example `[382]` for basketball's Euroleague. These records have `league: null`. Leagues and league ids together: at most 12 per run. |
| `marketFamilies` | array | `["moneyline", "spreads", "totals"]` | Any of `moneyline` (includes the three-way price with a draw), `spreads`, `totals`, `team_totals`, `props`, `futures`. |
| `periods` | array | `["game"]` | Any of `game`, `first_half`, `first_period`, `second_period`, `third_period`. A "period" is a quarter in football and basketball. |
| `mainLineOnly` | boolean | `true` | Keep only the main spread, total and team total. Set `false` for every alternate line as its own record. |
| `includeRegulationTime` | boolean | `false` | Hockey only: also collect the regulation-time markets (three-way moneyline with a draw, its own puck line and total), marked `includesOvertime: false`. |
| `dateFrom` / `dateTo` | string | today to today + 14 | Start-time days as `YYYY-MM-DD` (UTC days). With only `dateFrom`, that single day. At most 31 days. Futures are not limited by the dates. |
| `maxResults` | integer | `1000` | Maximum dataset records per explicit run (1–10,000). Bare start: 20; Console prefill: 100. |
| `requestDelaySecs` | number | `1` | Seconds between requests (0.5–10). |
| `proxyConfiguration` | object | Apify Proxy | The proxy connection the run uses. Pinnacle refuses some connections; if a run fails with a 403 answer, pick a different proxy group or country here. |

Omit optional fields to use their defaults. Explicit `[]`, `""` or `null`
for a league list, market list, period list or date is an input error naming
the field. Dates must be exactly `YYYY-MM-DD`. Numeric league ids alone
collect only those ids. For local direct runs, set `proxyConfiguration` to
`{"useApifyProxy": false}`; selecting Apify Proxy locally fails before requests.

A misspelt input field, an unknown league or a window over the limits stops
the run with an error that names the problem. A league id that does not exist
is reported in the run's warnings; when it is the only league asked for, the
run fails and says so.

### Output

One record is **one market of one game**: "Giants v Commanders, full game,
spread −2.5". A game with a moneyline, a spread and a total is three records;
with alternate lines switched on, each extra line is one more record.

Every record has four parts:

- **`recordKey`**: `pinnacle:pinnacle:<game id>:<market key>`. It is stable
  while the market exists; a spread at another line has another key.
- **`quote`**: the market in a flat, source-independent shape:
  - `league`, `sport`, `eventNativeId`, `startTime` (UTC), `home`, `away`
    (Pinnacle's own team names);
  - `family`: `moneyline`, `moneyline_3way`, `spreads`, `totals`,
    `team_totals`, `props` or `futures`;
  - `period`: `game`, `first_half`, `first_period`, `second_period`,
    `third_period`, or `tournament` for futures;
  - `includesOvertime`: for full-game lines, `true` when the market settles
    including overtime, `false` when it settles on regulation time only,
    `null` when Pinnacle's data does not say (see below);
  - `line`: the home side's line for a spread, the total for totals and team
    totals, a prop's line when it has one, `null` for moneylines;
  - `subject`: the player or team a prop or team total is about, for example
    `{"type": "player", "name": "Jayden Daniels", "stat": "touchdown_passes"}`;
  - `selections`: every side with `role` (`home`, `away`, `draw`, `over`,
    `under`, `yes`, `no` or `other`), `name`, its own `line`, and the price as
    `american`, `decimal` and `impliedProbability`;
  - `limits.maxStake`: the largest stake shown for the market;
  - `book`, `bookName`, `source`, `firstParty`, `collectedAt`.
- **`canonical`**: the same market as the shared envelope our other sports
  Actors use, for joining across sources. Pinnacle's own detail is under
  `canonical.extensions.pinnacle`: market key, version counter, native period
  number, whether the line is an alternate, cutoff time, rotation number, the
  prop's title and category, and `pricesSnapshotAt` (see the FAQ).
- **`source`**, **`collectedAt`** and `sourceUrl`.

How to read the prices:

- `impliedProbability` is `1 / decimal` of that single price. It includes the
  bookmaker's margin, so the sides of a market add up to more than 1.
- A spread gives each side its own signed line: home `-2.5` means the home
  team gives 2.5 points and the away side shows `2.5`.
- The main line is marked `isAlternate: false` by Pinnacle; other
  lines are marked `isAlternate: true` in the extension.
- Overtime: hockey full-game lines are `includesOvertime: true` (the two-way
  moneyline); the regulation-time hockey lines you can switch on are `false`.
  Soccer match lines are `false`: the moneyline has a draw price and settles
  on regular time. For the NFL, NBA, UFC and other sports the value is `null`
  because nothing in the data states it. Never compare a `true` line with a
  `false` line as if they were the same market.
- Anything not published is `null`, never estimated: a side without a price
  keeps its place in `selections` with `american`, `decimal` and
  `impliedProbability` all `null`, and so do the sides of a market that is
  not open. `sourceUpdatedAt` and `url` are always `null` because no change
  time and no page address are published with the prices. The stake limit has
  no stated currency, so `limits.currency` is `null`.
- For props and futures the market's title is in
  `canonical.market.nativeName` (for example "AFC Conference Winner").
- UFC fighters are listed as `home` and `away` in Pinnacle's own order; there
  is no home fighter, and the extension says so (`homeAwayNominal: true`).

The run summary (`OUTPUT` in the key-value store) reports records per market
family, games, priced and unpriced selections, records per league, requests
made, what was left out and why, warnings, and the status `succeeded`,
`succeeded_empty`, `succeeded_partial` or `failed`.

### Output example

Selected fields of a real NFL spread recorded 2026-11-10 (full records also
carry `canonical`, `source`, `sourceUrl` and `collectedAt`):

```json
{"recordKey":"pinnacle:pinnacle:1639394521:s;0;s;-2.5","quote":{"quoteVersion":"1.1","source":"pinnacle","book":"pinnacle","bookName":"Pinnacle","firstParty":true,"league":"nfl","sport":"american_football","eventNativeId":"1639394521","startTime":"2026-11-13T01:15:00Z","home":"New York Giants","away":"Washington Commanders","family":"spreads","period":"game","includesOvertime":null,"line":-2.5,"subject":null,"selections":[{"role":"home","name":"New York Giants","line":-2.5,"american":-105,"decimal":1.9524,"impliedProbability":0.5122},{"role":"away","name":"Washington Commanders","line":2.5,"american":-107,"decimal":1.9346,"impliedProbability":0.5169}],"limits":{"maxStake":20000.0,"currency":null},"sourceUpdatedAt":null,"collectedAt":"2026-11-10T23:55:00Z","url":null}}
```

### Pricing

Pay per result: **$0.0025 per record** ($2.50 per 1,000 records) plus
**$0.00005 per run start**. Each dataset record is one market of one game.
See the Actor's **Pricing** tab on Apify for current rates. There is no
separate charge for compute or proxy traffic.

At these rates, 20 records cost $0.05005 including the start, 100 records
cost $0.25005, and 1,000 records cost $2.50005. An empty run costs $0.00005
and adds no record charge. Use `leagues`, `marketFamilies`, `mainLineOnly`,
the date window and `maxResults` to keep runs small. A total-charge limit
can stop delivery below `maxResults`; OUTPUT reports the stored count and
a warning when this happens.

### Scheduling and monitoring

- **Line movement:** schedule a league-scoped run (for example every 15
  minutes on game days) and key your history on `recordKey` plus
  `collectedAt`. Group the lines of one game with `quote.eventNativeId`,
  `quote.family` and `quote.period`.
- **Closing prices:** games leave the list when they start, so the last run
  before `quote.startTime` holds the closing price. Results and scores are not
  part of this Actor.
- Records are a fresh snapshot each run; nothing is remembered between runs.
  De-duplicate on `recordKey`.
- A run plans two requests per league at about one request start per second.
  Source retries and network latency can add requests and time.

### FAQ

**How fresh are the prices?** Each run reads Pinnacle at run time, but the
price list a visitor without an account receives is refreshed only every few
minutes: on 2026-11-11 it was up to about 15 minutes old. Each record says
when its price list was produced (`pricesSnapshotAt` in the extension) next
to when the run read it (`collectedAt`). This is not a feed for in-play or
last-second prices.

**Which line is the main line?** The one Pinnacle does not mark as an
alternate. There is exactly one per game, market and period.

**Why does a league return nothing?** Out of season it lists no games (MLB in
November), and most leagues are priced only a few days ahead (the NBA about a
day, the NFL the current week, the Premier League two rounds). Widen the date
window for leagues that play weekly. With named leagues an empty result is a
normal, uncharged run.

**Why did a run fail with "Pinnacle answered 403"?** Pinnacle refuses some
connections outright and answers others. The run says so and stops without
charging for records. Choose a different proxy group or country in
`proxyConfiguration` (for example residential proxies in the United States)
and run it again.

**Are live (in-play) prices included?** No. Only pre-match markets are
collected.

**Are hockey regulation-time prices included?** On request. By default the
hockey moneyline, puck line and total are the full game including overtime.
Set `includeRegulationTime` to also get the regulation-time markets (the
moneyline with a draw price, and their own puck line and total); they have
the same `period` (`game`) and `includesOvertime: false`.

**What are the stake limits?** The largest stake shown to a visitor without
an account for that market at that moment. Account limits can differ.

**Export formats?** JSON, CSV, Excel, XML or RSS from the dataset, or read it
through the Apify API in your own pipeline.

### Limits

- The data comes from an unofficial, undocumented guest interface of the
  sportsbook's website. It can change or be withdrawn without notice. Check a
  run's status and warnings before relying on it.
- One sportsbook. This is Pinnacle's price only, not a multi-book odds feed.
- Prices and limits are what a visitor without an account is shown, and can
  be several minutes old. No account features: no bet placement, balances,
  bet history or personal limits.
- Pre-match only. No live prices, scores, results or settled markets, and no
  historical series; build history by scheduling runs.
- Halves, quarters and periods are collected only where they map to a shared
  period name; second halves and later quarters are not, and baseball is full
  game only until its period numbers are confirmed in season.
- Prop subjects are read from Pinnacle's own titles: player totals name the
  player and the statistic; other props keep their title in
  `canonical.market.nativeName`.
- At most 12 leagues, 31 days and 10,000 records per run.
- You are responsible for using the data in line with the terms of the sites
  it comes from and the laws that apply to you, including any rules on
  sports-betting information where you live.

For support, contact Apify support and include the Actor run ID.

# Actor input Schema

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

Leagues to collect. Omit both league fields for NFL and NBA. A bare start samples at most 20 records (MLB and EPL in June–August). Explicit empty lists are invalid.

## `leagueIds` (type: `array`):

Optional. Pinnacle's own numeric league ids for competitions that are not in the list above, for example \[382] for basketball's Euroleague. Rows for these leagues carry Pinnacle's league name and a null league key. Leagues and league ids together are limited to 12 per run.

## `marketFamilies` (type: `array`):

moneyline: the match winner, including the three-way price with a draw. spreads and totals: point spread and over/under. team_totals: each team's own over/under. props: game and player props. futures: outright boards such as the conference winner.

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

Which part of the game the markets cover. game is the full game. first_period, second_period and third_period are quarters in football and basketball and periods in hockey. Futures are not affected by this setting.

## `mainLineOnly` (type: `boolean`):

Keep only the main spread, total and team total of each game and period. Turn this off to also collect every alternate line, each as its own record (about 10 to 25 lines per game and market).

## `includeRegulationTime` (type: `boolean`):

Hockey games are priced twice: including overtime (the default, two-way moneyline) and regulation time only (three-way moneyline with a draw, its own puck line and total). Turn this on to also collect the regulation-time markets; they are marked includesOvertime false.

## `dateFrom` (type: `string`):

First day of games to collect, as YYYY-MM-DD (UTC calendar day of the start time). Leave both dates empty for today and the next 14 days. With only this date set, that single day is collected. Futures are not limited by the dates.

## `dateTo` (type: `string`):

Last day of games to collect, as YYYY-MM-DD (UTC). A window may cover at most 31 days.

## `maxResults` (type: `integer`):

Maximum dataset records (one market per game), 1–10,000. A bare start samples at most 20; explicit inputs default to 1,000 if this field is omitted.

## `requestDelaySecs` (type: `number`):

Seconds between request starts. The default of 1 keeps the run to about one request per second.

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

Apify Proxy is used by default. Pinnacle refuses some connections with a 403 answer; if a run fails that way, choose a different proxy group or country here (for example residential proxies in the United States). You may also supply your own proxy or turn proxies off.

## Actor input object example

```json
{
  "leagues": [
    "nfl",
    "nba"
  ],
  "marketFamilies": [
    "moneyline",
    "spreads",
    "totals"
  ],
  "periods": [
    "game"
  ],
  "mainLineOnly": true,
  "includeRegulationTime": false,
  "maxResults": 100,
  "requestDelaySecs": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

## `summary` (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"
    ],
    "marketFamilies": [
        "moneyline",
        "spreads",
        "totals"
    ],
    "periods": [
        "game"
    ],
    "mainLineOnly": true,
    "includeRegulationTime": false,
    "maxResults": 100,
    "requestDelaySecs": 1,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("automatic/pinnacle-odds").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",
    ],
    "marketFamilies": [
        "moneyline",
        "spreads",
        "totals",
    ],
    "periods": ["game"],
    "mainLineOnly": True,
    "includeRegulationTime": False,
    "maxResults": 100,
    "requestDelaySecs": 1,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("automatic/pinnacle-odds").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"
  ],
  "marketFamilies": [
    "moneyline",
    "spreads",
    "totals"
  ],
  "periods": [
    "game"
  ],
  "mainLineOnly": true,
  "includeRegulationTime": false,
  "maxResults": 100,
  "requestDelaySecs": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call automatic/pinnacle-odds --silent --output-dataset

```

## MCP server setup

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

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/4dN720C6SkAaz45bH/builds/JaJ8AQSLscpZSrg0L/openapi.json
