pnclFEED Free key

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 key

Raw, 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.

ws.py
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())

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.

JobTransport
Occasional checks, cron, sheetsREST polling with a since cursor
Drop alerts, processed for youSSE stream
Full market state, every priceRaw 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.