Pinnacle Odds in Python: A Working Guide

Get Pinnacle odds in Python with pip install pinnodds: REST markets, SSE drop streaming, quarter lines, specials and a full working script you can run today.
The shortest path to Pinnacle odds in Python is pip install pinnodds, one API key in a header, and a Client that wraps both the REST snapshots and the push streams. There is no public Pinnacle API to call directly anymore — Pinnacle closed theirs in July 2025 — so what you're actually doing is pointing a Python client at a service that carries the same market data.
This guide is the version I'd hand a colleague on day one: what to install, which endpoint answers which question, the one field-naming gotcha that will bite you, and a complete script at the end that streams live odds drops and enriches them with a REST lookup.
The short definition
Getting Pinnacle odds in Python means one of two things, and you should know which one you want before you write a line of code:
- Snapshots — you ask, you get the current state of a market. That's REST:
GET /kit/v1/marketsfor live, the same endpoint withevent_type=prematch(or the dedicated/kit/v1/prematch/*family) for future fixtures. - Pushed updates — you connect once and the server tells you when something moves. That's the raw WebSocket passthrough at
/ws/feed, or Server-Sent Events at/odds-dropand/odds-drop-prematchif you only care about prices falling.
Install and authenticate:
pip install pinnodds
export PINNODDS_KEY="your_key_here"
Auth is a single API key. Header x-api-key: YOUR_KEY on REST, or ?key= on the stream URLs because EventSource clients can't set headers. Same key for both, and it doubles as your login — full reference in the docs.
Why "pinnacle api python" no longer means what it used to
If you search pinnacle api python you'll still find blog posts and dead GitHub repos referencing Pinnacle's own v1 endpoints and old scraping tricks. Those are historical artifacts. The public API is gone.
The practical consequence: your Python code no longer manages a Pinnacle session, since data reaches you as a normalised feed. That's a simplification — but it also means you should stop thinking in terms of "poll every N seconds." A polling loop against a pushed feed is wasted requests and a worse view of the market. Which brings us to the thing most people get wrong.
Polling versus pushing (the confusion worth clearing up)
Most odds tooling in Python is written as a while True: requests.get(...); time.sleep(5) loop. It works. It's also the reason people think they have real-time data when they have five-second-old data.
pinnodds pushes. The odds arrive when they change, sub-second. Two consequences for your code:
- Your process becomes long-lived and event-driven, not a cron job. Design for that: a handler function, a reconnect policy, and somewhere to put state.
- You still need REST. Streams tell you what moved; REST tells you what the board looks like right now, which is what you need on startup and after a disconnect gap. Use both.
My recommendation: REST once at boot to build your picture, SSE for drops, and a periodic REST refresh (a minute or two is plenty) as a reconciliation safety net rather than a data source.
The field-name difference nobody documents in their own code
This is the single most common source of KeyError in Python integrations, and it's not a bug — the two surfaces are genuinely different shapes.
An SSE alert from /odds-drop is a lean movement notification:
sect— the marketoutcome— the selection that movedfrom_price/to_priceid— the eventlimit— Pinnacle's max stake on that linenvp— the no-vig fair price- No drop percentage. You compute it yourself if you want one.
A REST /api/drops row is the enriched form of the same idea:
market,designationfrom,toevent_iddrop_pct— precomputed
So sect ↔ market, outcome ↔ designation, from_price ↔ from, id ↔ event_id. If you plan to consume both, normalise at the boundary:
def normalise(row, source):
if source == "sse":
return {
"market": row["sect"],
"sel": row["outcome"],
"from": row["from_price"],
"to": row["to_price"],
"event_id": row["id"],
"limit": row.get("limit"),
"nvp": row.get("nvp"),
"drop_pct": None, # not sent on SSE — derive it
}
return {
"market": row["market"],
"sel": row["designation"],
"from": row["from"],
"to": row["to"],
"event_id": row["event_id"],
"limit": row.get("limit"),
"nvp": row.get("nvp"),
"drop_pct": row.get("drop_pct"),
}
Write that once, never think about it again.
Quarter lines and specials: the two flags that change your payload
Quarter lines come through by default. Every line Pinnacle prices is in the feed — soccer totals at 1.75, 2.25, 2.75, 3.25, and quarter-ball handicaps at -0.75, -1.25, -1.75, -2.25. If your Python model treats totals as integers or halves, you will silently drop half the board. Parse the line value as a float and key your market map on (market, line, selection).
Line depth is identical on every plan. Plans differ on rate limit and push access, not on what you can see — see /#pricing.
Specials are opt-in. Pass include_specials=1 and player/team props, exact scores, futures and outrights appear as their own event rows, each carrying special, special_category, special_markets, and a parent_id pointing back to the parent fixture. They're off by default for a blunt reason: they dominate the payload. Soccer prematch is roughly 1,500 events without the flag and roughly 12,400 with it.
So don't turn specials on globally in a script that also does live market scanning. Fetch them in a separate pass, group by parent_id, and cache.
The honest caveats
Things I'd want to know before committing a Python service to this feed:
- It's Pinnacle only. There is no cross-book aggregation here. If your strategy needs the same market from forty bookmakers to find a positive-EV outlier, this is the wrong single dependency — pair it with an aggregator and use Pinnacle as your sharp reference price.
- No historical odds archive. The API serves current state and live movement. If you want a backtest dataset, you build it: subscribe, write every message to Postgres or Parquet, wait. Starting that logger on day one is the cheapest thing you'll ever do.
- SSE is a paid tier. REST is on Pro at $99/mo; push access via SSE starts at Pro + SSE ($149/mo), with Scale at $229/mo. If you're on REST-only, your
/api/dropspolling loop is the correct architecture, anddrop_pctbeing precomputed there is a real convenience. - Reconnects lose the gap. A dropped SSE connection means movements that happened during the gap are simply not replayed. That's why the REST reconciliation call after reconnect isn't optional in a serious system.
- Specials
parent_idis your join key, and you have to do the join. Nothing arrives pre-nested. - Free trial keys exist to prove the shape of the data, not to run production. Get one in seconds without a card, print a payload, then decide.
A complete working script
This connects to the live drops stream, reconnects with backoff, normalises fields, computes the drop percentage SSE doesn't send, and pulls a REST snapshot at boot. Python 3.9+.
import os, time, logging
from pinnodds import Client
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
log = logging.getLogger("odds")
KEY = os.environ["PINNODDS_KEY"]
client = Client(api_key=KEY)
MIN_DROP_PCT = 3.0
MIN_LIMIT = 500 # ignore thin lines
def pct(from_price, to_price):
"""Implied-probability move. Positive = price fell (prob rose)."""
try:
f, t = float(from_price), float(to_price)
return ((1 / t) - (1 / f)) / (1 / f) * 100
except (TypeError, ValueError, ZeroDivisionError):
return 0.0
def boot_snapshot():
"""Current live board, quarter lines included, specials excluded."""
markets = client.markets(event_type="live")
log.info("snapshot: %d live events", len(markets))
for ev in markets[:3]:
log.info(" %s vs %s", ev.get("home"), ev.get("away"))
return {ev.get("event_id") or ev.get("id"): ev for ev in markets}
def handle(msg, board):
row = {
"market": msg["sect"],
"sel": msg["outcome"],
"from": msg["from_price"],
"to": msg["to_price"],
"event_id": msg["id"],
"limit": msg.get("limit"),
"nvp": msg.get("nvp"),
}
row["drop_pct"] = pct(row["from"], row["to"])
if row["drop_pct"] < MIN_DROP_PCT:
return
if row["limit"] is not None and float(row["limit"]) < MIN_LIMIT:
return
ev = board.get(row["event_id"], {})
log.info(
"DROP %.1f%% | %s %s | %s -> %s | nvp=%s limit=%s | %s v %s",
row["drop_pct"], row["market"], row["sel"],
row["from"], row["to"], row["nvp"], row["limit"],
ev.get("home", "?"), ev.get("away", "?"),
)
def main():
board = boot_snapshot()
backoff = 1
while True:
try:
for msg in client.stream_drops(mode="live"):
backoff = 1
handle(msg, board)
except KeyboardInterrupt:
return
except Exception as e:
log.warning("stream dropped (%s); resync in %ds", e, backoff)
time.sleep(backoff)
backoff = min(backoff * 2, 60)
board = boot_snapshot() # close the gap the stream lost
if __name__ == "__main__":
main()
Three details in there are load-bearing. The backoff resets to 1 only after a message actually arrives, so a server that accepts and immediately closes doesn't get hammered. The snapshot re-runs on every reconnect, which is how you close the replay gap. And the limit filter is doing more work than the percentage filter — a 6% move on a line with a tiny max stake is noise; the same move on a high-limit line is information.
For prematch, swap mode="live" for mode="prematch" and use /kit/v1/prematch/fixtures plus /kit/v1/prematch/lines for the boot snapshot. Endpoint-by-endpoint parameters are in the API reference.
Where the Node SDK is the better choice
If your stack is already TypeScript, npm install pinnodds gives you the same surface with the same field names, and native EventSource support makes the SSE side marginally less fiddly. Python wins when the consumer is a model — pandas, numpy, a notebook, anything where you want the odds two lines from the analysis. Don't run both languages in one service just to feel modern.
Takeaway
Build your Python odds feed as two halves that agree with each other: REST for truth, SSE for speed. Normalise sect/outcome/from_price into the same dict shape as market/designation/from at the ingest boundary, parse line values as floats so quarter lines survive, keep include_specials=1 in its own pass, and start logging every message to disk today — the historical dataset you'll want in six months only exists if you begin collecting it now.
Frequently asked questions
Is there still a public Pinnacle API for Python?
No. Pinnacle closed its public API in July 2025, so there is no official endpoint to authenticate against. pinnodds is an independent service that carries the same market data and offers an official Python SDK via pip install pinnodds.
How do I install the Pinnacle odds Python SDK?
Run pip install pinnodds, then create a client with your API key: Client(api_key="YOUR_KEY"). The SDK is low-dependency and wraps both the REST endpoints and the SSE drop streams.
Why does my SSE odds alert have no drop percentage?
SSE alerts are deliberately lean — they carry sect, outcome, from_price, to_price, id, limit and nvp, but no drop_pct. The REST /api/drops rows are the enriched form and include a precomputed drop_pct, so either compute the percentage yourself from from_price/to_price or read it from REST.
Do quarter lines like 2.25 totals come through the API?
Yes. Every line Pinnacle prices is in the feed, including soccer totals at 1.75/2.25/2.75/3.25 and quarter-ball handicaps at -0.75/-1.25/-1.75/-2.25. Parse line values as floats, because integer-only parsing will silently drop half the board.
How do I get player props and outrights in Python?
Pass include_specials=1 on your markets request. Specials come back as their own event rows with special, special_category, special_markets and a parent_id linking to the parent fixture — but they balloon the payload (soccer prematch goes from roughly 1,500 events to roughly 12,400), so fetch them in a separate pass.
Do I need a paid plan to stream odds in Python?
REST access starts at $99/mo (Pro), and pushed SSE streaming starts at Pro + SSE ($149/mo), with Scale at $229/mo. A free trial key takes seconds and needs no card if you just want to inspect the payload shape first.
Get real-time Pinnacle odds in your code
The Pinnacle APIby pinnodds: live & prematch markets with instant odds-drop alerts over SSE and WebSocket. See plans and pricing, or get a free key in seconds — no card, no Pinnacle account.
Get your free API key