What the Pinnacle odds API is

The Pinnacle odds API this guide documents is a third-party feed that serves Pinnacle's live and prematch prices as JSON over REST, pushes price drops over server-sent events and, as a paid add-on, relays raw WebSocket frames. It is a data product: it reads prices and it does not place bets or touch a Pinnacle account.

It is not Pinnacle's own API. Pinnacle closed public sign-up for its API on 23 July 2025 and now grants access one application at a time to business partners and some individual and research uses. The Pinnacle API alternative page on pnclFEED walks through what changed and which route fits which use; this guide covers the feed.

Everything below comes from the feed's published documentation, checked 26 September 2026. Where the documentation disagrees with itself, the guide says so. Nothing here is a measured result unless the section says how it was measured.

Authentication and the first request

Every request carries your key in the x-portal-apikey header. The streaming endpoints also accept it as a ?key= query parameter for clients that cannot set headers, but a key in a URL ends up in logs, so keep the header form on servers. The base URL comes from your account and the documentation; the samples on this page read it from an environment variable.

A free key is issued on sign-up, with no card and no expiry, and doubles as the account login. It allows 20 requests a minute and 100 a day on REST, which is enough to validate a parser, run every sample on this page and poll one board every fifteen minutes all day. A three-day full demo that includes SSE, raw WebSocket and 10 requests a second is available on request (checked 26 September 2026).

export ODDS_API_KEY="your key"
export ODDS_BASE_URL="https://..."   # from your account

curl -s -H "x-portal-apikey: $ODDS_API_KEY" \
  "$ODDS_BASE_URL/kit/v1/markets?sport_id=1" \
  | jq '{last, events: (.events | length)}'

A 200 with a last cursor and a non-zero event count means the key works and the soccer board is live. The curl and jq quickstart on pnclFEED extends this into a one-line-per-match listing, and the Pinnacle API key page on pnclODDS covers sign-up and the first request step by step.

Endpoints

The feed has a small surface. Four REST routes, two streams, a health check and an optional raw WebSocket cover everything the documentation describes.

RoutePurposeNotes
GET /kit/v1/markets?sport_id=NThe live board for one sportReturns an object with events and a last cursor. Send last back as since to receive only what changed.
GET /kit/v1/prematch/fixtures?sport_id=NThe prematch board for one sportFixtures with their markets. A similarly named route below takes an event rather than a sport.
GET /kit/v1/prematch/markets?event_id=NOne prematch event's marketsConfusing this with the fixtures route makes a valid feed look empty or returns a parameter error.
GET /api/drops?mode=live|prematchRecent drops, on requestmin_drop_pct sets the size of move and max_age_sec the window. Keeps roughly the last three hours.
GET /odds-drop?min_drop=NLive drop alerts, streamedServer-sent events. One connection per key per stream; a second live connection closes the first.
GET /odds-drop-prematch?min_drop=NPrematch drop alerts, streamedAdd recheck=N to hold each drop N seconds and send it only if the price has not bounced back.
Health endpointHow long ago the live feed last wroteNamed in the documentation; poll it beside a quiet stream to tell a quiet market from a broken feed.
Raw WebSocketEvery frame, unprocessedA paid add-on on the three REST plans. You rebuild market state yourself.

Sport ids are the feed's own, 1 to 13, and each response publishes Pinnacle's internal id beside them. Soccer is 1, tennis 2, basketball 3, hockey 4, football 5, baseball 6, rugby 7, MMA 8, boxing 9, volleyball and handball 10, esports 11, golf 12 and cricket 13. Map them once at the edge of your code and never mix the two namespaces.

The response shape

A board response is an object, not a list. events holds one record per match and last is the cursor for the next call. Each event carries its teams, its start time, the feed's sport id and Pinnacle's, and its markets grouped under periods. periods.num_0 is the full match; other keys are halves, quarters, sets or maps depending on the sport.

{
  "last": 141,
  "events": [
    {
      "home": "Home FC",
      "away": "Away FC",
      "periods": {
        "num_0": {
          "money_line": { "home": 2.10, "draw": 3.40, "away": 3.60 },
          "spreads": { "-0.5": { "home": 2.05, "away": 1.85 } },
          "totals":  { "2.5":  { "over": 1.95, "under": 1.90 } },
          "team_total": { "home": { "points": 1.5, "over": 2.00, "under": 1.85 } }
        }
      }
    }
  ]
}

Prices are decimal odds. Money line, spreads, totals and team totals have different shapes, and a full-match total and a first-half total are different markets even when they share a line. Presence varies by event and period: a market can be absent, empty or suspended, and your model should keep those three states apart. Never substitute zero for a missing price.

Special markets are an opt-in expansion through include_specials. They arrive as extra rows linked by parent_id and separate special_markets structures, they grow the payload, and their price changes do not all follow the same alert behaviour as the main markets. Keep the full-market parser and the drop-alert parser separate. The normalization guide shows the event, period, market, line, selection and timestamp keys that make records from this feed comparable with any other.

Polling with the since cursor

The cursor is the whole trick to polling this feed cheaply. The first call for a sport returns the full board and a last value; every call after it sends that value as since and receives only the events that changed. Store the cursor with your snapshot so a restart resumes instead of refetching, and treat a full board as the way to resynchronise, not the way to poll.

import os, time, requests

BASE = os.environ["ODDS_BASE_URL"]
HEADERS = {"x-portal-apikey": os.environ["ODDS_API_KEY"]}

def poll(sport_id=1, interval=900):
    since = None
    while True:
        params = {"sport_id": sport_id}
        if since is not None:
            params["since"] = since
        r = requests.get(f"{BASE}/kit/v1/markets", headers=HEADERS, params=params, timeout=30)
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "5")))
            continue
        r.raise_for_status()
        body = r.json()
        since = body["last"]
        for event in body["events"]:
            line = event["periods"].get("num_0", {}).get("money_line")
            if line:
                print(event["home"], event["away"], line)
        time.sleep(interval)

poll()

Fifteen minutes between calls keeps one board inside the free key's 100 requests a day. On a paid tier the interval is a budget decision: the capacity planning page on pnclENGINE turns sports, phases and an interval into requests per second and per day, and the API cost calculator here turns the same schedule into a monthly figure. The Python quickstart and the Node.js quickstart on pnclFEED carry longer versions of this loop.

Drop alerts over SSE

The streams push price drops the moment the feed detects them. A GET to /odds-drop for live markets or /odds-drop-prematch for prematch ones, with the key and Accept: text/event-stream, returns a 200 that never finishes. Set min_drop yourself: the documented example uses 5 and the floor is 1, and the default is documented inconsistently, so relying on it is a mistake.

Every message is a data: line holding JSON. The first is a control object, {"type": "connected"}; after it, each batch of alerts is an array. An object later in the stream is another control message, such as plan_lacks_sse when the key's plan has no stream or rate_limited when a client reconnects in a tight loop, which also carries a Retry-After: 60 header.

data: {"type": "connected", "id": "d648beb8-..."}
data: [{"home": "Sunshine Coast Phoenix", "away": "Cairns Dolphins",
        "league": "Australia - NBL1 Women", "sport": "Basketball",
        "sect": "Moneyline", "outcome": "Home", "period": 4,
        "from_price": 2.86, "to_price": 2.7, "nvp": 3.04,
        "id": 1629729400, "alerted": 1777625196}]

Each alert names the line with sport, league, sect, outcome and period, carries the price before and after as from_price and to_price, adds nvp, the no-vig fair price after the move, and stamps the alert in Unix seconds as alerted. The percentage is yours to compute. Missed alerts are not replayed: fill a gap from /api/drops, whose records use different field names (from, to, drop_pct, market, side), so check which one a sample reads.

// Node.js 18+: read the live drop stream and reconnect with backoff.
const BASE = process.env.ODDS_BASE_URL;
const KEY = process.env.ODDS_API_KEY;

async function listen(minDrop = 5) {
  let delay = 1000;
  for (;;) {
    try {
      const res = await fetch(`${BASE}/odds-drop?min_drop=${minDrop}`, {
        headers: { "x-portal-apikey": KEY, accept: "text/event-stream" },
      });
      if (res.status === 429) {
        await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") || 60) * 1000));
        continue;
      }
      delay = 1000;
      let buffer = "";
      for await (const chunk of res.body) {
        buffer += Buffer.from(chunk).toString("utf8");
        let end;
        while ((end = buffer.indexOf("\n\n")) >= 0) {
          const frame = buffer.slice(0, end);
          buffer = buffer.slice(end + 2);
          const line = frame.split("\n").find((l) => l.startsWith("data:"));
          if (!line) continue;
          const payload = JSON.parse(line.slice(5));
          if (Array.isArray(payload)) payload.forEach(handle);
          else if (payload.type !== "connected") console.warn("control", payload);
        }
      }
    } catch (err) {
      console.error("stream closed", err.message);
    }
    await new Promise((r) => setTimeout(r, delay + Math.random() * delay));
    delay = Math.min(delay * 2, 60000);
  }
}

function handle(alert) {
  const pct = ((alert.from_price - alert.to_price) / alert.from_price) * 100;
  console.log(alert.sport, alert.home, alert.away, alert.sect, alert.outcome, pct.toFixed(1) + "%");
}

listen();

Thresholds, seen-alert sets and dedupe windows belong next to the reader process, not in a browser tab. The SSE setup and dedupe and cooldowns pages on pnclPULSE cover the rule side, and the Telegram alerts page on pnclDATA shows the same stream delivered to a phone.

Rate limits, errors and backoff

Every plan carries a per-second ceiling on REST, and the free key carries per-minute and per-day ones as well. Crossing a limit returns 429 Too Many Requests, normally with a Retry-After header in seconds. That header is authoritative: wait exactly that long, then continue. A rising 429 count is the early warning that steady-state load is drifting toward the ceiling.

StatusMeaningWhat to do
401The header is missing or the key is wrongFix the key; do not retry
403The key is valid but the plan does not include what you asked forCheck the plan, or the stream entitlement
429Over the per-second, per-minute or per-day allowanceSleep for Retry-After, then retry with jitter
5xxThe feed had a problemRetry a few times with growing delays, then fail the cycle

Keep steady-state usage under roughly 70 percent of the per-second quota so retries, admin tooling and a second environment fit in the rest. Share backoff state across workers; ten processes each backing off politely still add up to ten times the requests. The rate limits page and the error runbook on pnclENGINE go status by status.

Plans and prices

Published prices in USD, monthly billing, checked 26 September 2026. The checkout shows the figures that apply on the day you buy, and taxes, fees and proration are not published.

PlanMonthlyREST allowanceSSE alertsRaw WebSocket
Free key$020 a minute, 100 a dayNoNo
SSE drop alerts$9920 a minute, 100 an hour, 100 a dayIncludedNot eligible
REST snapshots$9910 requests a secondNo+$99 per 30 days
REST + SSE$14910 requests a secondIncluded+$99 per 30 days
High-volume REST$22930 requests a secondIncluded+$99 per 30 days

Prepaid terms exist: three months at $249 for the two $99 plans, $379 for REST + SSE and $599 for high-volume REST, and six months at $449 and $679 for the two REST plans only. The raw WebSocket add-on is quoted in 30, 90 and 180-day blocks at $99, $297 and $594. A monthly equivalent is a comparison number, not a monthly bill. The Pinnacle API pricing page on pnclODDS has the estimator and the plan-fit decisions, and the pricing quickstart on pnclFEED reads the same numbers from a developer's side.

Coverage

The documentation lists soccer, tennis, basketball, hockey, football, baseball, rugby, MMA, boxing, volleyball and handball, esports, golf and cricket. Live and prematch are both documented, and event availability varies by sport and by day. REST markets are money line, spreads, totals and team totals under periods. A listed sport does not guarantee every competition or market, so run a representative sample on the free key while the events you care about are on the board.

Refresh cadence is stated inconsistently: the homepage says five seconds for prematch and the reference describes thirty. Neither is a measured end-to-end latency. There is no historical archive; the feed is a current-data service, and recording history is your own operational task with its own storage rights. The coverage page on pnclODDS keeps the evidence table, and the esports odds API page on pnclHUB shows one documented sport in practice.

Measuring latency and freshness yourself

This guide publishes no latency figure because none has been measured under a stated protocol. The honest way to get one is to run it: record the feed's own timestamp beside your receipt time for every alert or update over at least a week, align the clocks first, match markets rather than events, and report the distribution, not an average.

The benchmark protocol defines the workloads, the clock alignment and the report format, and the research page lists the clocks each result needs. When a measurement is published on this network it will appear with that method and its date.

Field reference

The fields a parser meets most, by endpoint, as the documentation names them. Types are what the documented examples show; validate them rather than assuming.

FieldWhereMeaning
eventsBoard responsesArray of event records for the requested sport.
lastBoard responsesCursor to send back as since on the next call.
home, awayEvent, alert, dropTeam or player names as the feed prints them.
periodsEventObject keyed num_0, num_1 and so on; each period holds its own markets.
money_linePeriodhome, draw (where the sport has one) and away as decimal odds.
spreadsPeriodKeyed by handicap line; each line holds home and away prices.
totalsPeriodKeyed by total line; each line holds over and under.
team_totalPeriodPer-team totals with a points line and over and under prices.
parent_id, special_marketsEvent, with include_specialsLinks a special market row to the event it belongs to.
typeSSE control messageconnected on open; error names such as plan_lacks_sse or rate_limited later.
sport, league, sect, outcome, periodSSE alertThe line that moved: sport, competition, market section, side and period number.
from_price, to_price, nvpSSE alertPrice before, price after and the no-vig fair price after the move.
id, alertedSSE alertEvent id and the alert time in Unix seconds; together with the line fields they make a dedupe key.
market, side, from, to, drop_pct, nvpREST /api/dropsThe same move as a REST record, with the percentage computed for you.
Retry-After429 responses, rate_limited framesSeconds to wait before the next request or reconnect.

Reading a drop from REST and from SSE

The same detection feeds both the stream and the drops endpoint, but the two records are shaped differently. A parser written for one will silently misread the other, so map the fields explicitly.

What you wantSSE alertREST drop
The linesport, league, sect, outcome, periodsport_name, market, side
Price before and afterfrom_price, to_pricefrom, to
Size of the moveCompute: (from_price minus to_price) divided by from_pricedrop_pct
Fair price after the movenvpnvp
Whenalerted, Unix secondsA timestamp on each record
Filteringmin_drop on the connection, recheck on prematchmode, min_drop_pct, max_age_sec on the request
ReplayNone; missed alerts are goneRoughly the last three hours

Glossary

Board
Every event the feed currently lists for one sport in one phase, live or prematch, with its markets.
Cursor
The last value a board response carries; sent back as since, it limits the next response to what changed.
Drop
A price falling by at least the threshold you set, measured from the previous price the feed saw to the current one.
No-vig price (nvp)
The fair price implied by a market once the bookmaker's margin is removed, so the probabilities of all outcomes sum to one.
Period
The part of a match a market settles on: num_0 for the full match, then halves, quarters, sets or maps by sport.
Phase
Prematch, before the event starts, or live, while it runs. The feed serves both through different routes and streams.
Snapshot
One board response held in your own store, the state you reconcile against and update from the stream.

Choosing a transport

REST polling is the right default for snapshots and reconciliation: it is cheap with the cursor, it recovers by itself and it needs no long-lived connection. The SSE streams are right when your application reacts to drops and nothing else, because the feed has already done the detection. Raw WebSocket frames are right only when you need every price movement, including the ones too small to alert, and are prepared to rebuild market state and recover gaps yourself.

Most production setups use two: REST for the bootstrap and periodic reconciliation, a stream for the hot path. The transport guide weighs recovery, freshness, state and operating cost, and the SSE stream versus REST endpoint page on pnclPULSE compares the two ways of reading drops.

Frequently asked questions

Is this Pinnacle's official API?
No. Pinnacle closed public sign-up for its own API on 23 July 2025 and grants access by application. The feed documented here serves Pinnacle's prices to anyone with a key, and it reads prices only.
Is there a free Pinnacle odds API?
Yes. The free key allows 20 requests a minute and 100 a day on REST, with no card and no expiry. Streams and higher rates need a paid plan or the three-day demo.
Does the API have historical odds?
No. The documentation describes a current-data service with no archive. Record what you need from the day you start, and check the storage rights that apply to your use.
How often do prices refresh?
The vendor's pages disagree: five seconds on the homepage, thirty seconds in the reference, for prematch. Treat both as documented cadence, not measured latency, and measure your own path if it matters.
Can I place bets through it?
No. It is a data feed. Placing bets through an API means Pinnacle's own betting API, which is available to approved users only.
Which plan do I need for drop alerts?
The $99 SSE drop alerts plan, or REST + SSE at $149 and high-volume REST at $229, which include the streams. The $99 REST snapshots plan has no stream.
What happens if I exceed the rate limit?
The API answers 429 with a Retry-After header. Wait that many seconds and continue; a tight retry loop turns a short limit into a longer throttle, and on the streams it produces a rate_limited control message.

Where each subject goes deeper

This guide is the reference; seven sites by the same publisher each take one part of the feed further.

  • pnclFEED: quickstarts in curl, Python, Node.js, Google Sheets and raw WebSocket, every one runnable on the free key.
  • pnclENGINE: REST capacity planning, the since cursor, rate limits, error handling, history storage and pipeline monitoring.
  • pnclPULSE: drop alerts over SSE, from thresholds and windows to dedupe, delivery and reconnects.
  • pnclODDS: plans, pricing, access, coverage and the WebSocket add-on, compared in full.
  • pnclDATA: dropping odds alerts with fair prices, and EV, dropping odds, CLV and Kelly calculators.
  • pnclHUB: a live esports match desk for CS2, League of Legends and Dota 2 with market guides.
  • pnclAPI tools: the odds converter, the no-vig calculator and the API cost calculator, here on this site.

Corrections to any fact on this page follow the editorial policy: the page changes and so does its updated date.