pinnacle odds api / websocket / raw frames
Pinnacle WebSocket API: every frame, your own state
WSS/ws/feed
every tickframe
The Pinnacle odds API's SSE stream sends drop alerts the provider already processed. The raw WebSocket sends every frame, and your code reconstructs market state from them. You see each price as it moves, and you decide what counts as a drop.
Get an API keyRaw, not processed
SSE answers one question: which prices fell. The socket hands you the whole feed, so anything beyond that question, full books, custom signals, your own drop thresholds, is built on your side from the frames.
The trade is work. State, gaps and reconnects belong to you. The sections below show each step.
What a frame carries
The first frame on each stream is a snapshot, a full baseline of the events you subscribed to. Every frame after it updates that baseline, so one open connection replaces a polling loop.
Subscribe by sport, or by up to 200 event IDs per stream.
WSS/ws/feed?key=<your key>
subscribewithin 5 s
snapshot firstthen updates
Connect and authenticate
The socket sits next to the REST API, on the host listed in the docs. Pass your key as ?key= on the socket URL, as the sample does; the x-portal-apikey header also works on the upgrade request. The sample keeps the address and the key in environment variables, so nothing secret sits in the file.
Send a subscribe message within five seconds of connecting or the socket closes. After that, answer each ping with a pong and read frames as they arrive.
Needs the WebSocket add-on on poll, poll + push or bulk.
import asyncio
import json
import os
import websockets
WS = os.environ["ODDS_WS_URL"]
KEY = os.environ["ODDS_API_KEY"]
PONG = json.dumps({"type": "pong"})
SUB = json.dumps({
"type": "subscribe",
"streams": ["live"],
"sport_ids": [1, 2],
})
async def handle(ws, f):
kind = f.get("type")
if kind == "ping":
await ws.send(PONG)
elif kind == "snapshot":
ev = f.get("events", [])
print("baseline", len(ev))
else:
print(kind, f)
async def main():
url = f"{WS}?key={KEY}"
ws = await websockets.connect(url)
await ws.send(SUB) # within 5 s
async for raw in ws:
f = json.loads(raw)
await handle(ws, f)
asyncio.run(main())
Select the code to copy it.
frames stopconnection lost
WSSconnect again, subscribe again
new snapshotfull resync
Recovery and reconnects
Missed frames are not replayed. Everything between the last frame you saw and the next snapshot is a gap in your state, and no catch-up request will fill it.
Treat every new snapshot as a full resync: drop the old state, rebuild from the snapshot, then apply the frames that follow. That single rule keeps reconnects boring, whether the socket dropped or you restarted the process.
Keep the socket up
The feed pings; your client pongs. The sample answers each ping with a pong, which keeps an idle connection alive and tells the server you are still reading.
If pings stop arriving at all, assume the connection is stale: close it, connect again and resync from the new snapshot.
socket, stream or polling
When the socket wins
REST polling fits jobs that wake up, ask what changed since the cursor and stop. SSE fits alerting: the provider watches every price and pushes the drops it found. The raw WebSocket fits anything that needs the whole market all the time, because every frame lands with you and your own state answers immediately.
All three can live in one stack. The Python page has a polling loop and an SSE listener next to this reader.
What the add-on costs per 30 days and how it behaves after a disconnect is set out on the WebSocket page on pnclODDS.
Which transport fits which job.
| Job | Transport |
|---|---|
| Occasional checks, cron, sheets | REST polling with a since cursor |
| Drop alerts, processed for you | SSE stream |
| Full market state, every price | Raw WebSocket |
GET/pinnacle-odds-api-pricing/
What the socket costs
The raw WebSocket is an add-on at $99 per 30 days, on top of poll ($99 a month), poll + push ($149 a month) or bulk ($229 a month). It is not available on push, and the free key is REST only.
The pricing page lays out all five plans and the add-on side by side.
NEXTpython ws.py
Keep every frame
Pick a plan that carries the add-on, export the address and the key, and the script above runs as it is. The pricing page shows which plans carry it.