On the wire · 14:55:41 UTC Live 79 · Prematch 8,497 · Markets 34,205 Vol. I · No. 1 · Monday 9 November 2026 wss://pinnfeed.io/v1/stream
PinnFeed
Answer · Python · WebSocket

Pinnacle odds WebSocket Python example

Updated · PinnFeed · 2 min read

Answer: install websockets, set PINNFEED_KEY, connect to wss://pinnfeed.io/v1/stream with header x-api-key, send a subscribe frame, apply every snapshot/delta, reply to ping with {"type":"pong"}, and reconnect with exponential backoff. You need the Builder plan (€99/mo) — Free and Hobby keys get 403 plan_lacks_ws on /v1/stream.

This is the same client the Python and Node guide ships; the snippet below is the minimal runnable shape.

Minimal client#

pip install websockets
export PINNFEED_KEY="ps_live_..."
# Example — truncated for the page. Full file: see the Python/Node guide.
import asyncio, json, os, random
import websockets

WS = os.environ.get("PINNFEED_WS", "wss://pinnfeed.io/v1/stream")
KEY = os.environ["PINNFEED_KEY"]

async def main():
    delay = 1
    while True:
        try:
            async with websockets.connect(WS, additional_headers={"x-api-key": KEY}, max_size=None) as ws:
                await ws.send(json.dumps({
                    "type": "subscribe",
                    "sports": ["soccer", "tennis"],
                    "markets": ["moneyline"],
                    "main": True,
                    "specials": False,
                }))
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg["type"] == "ping":
                        await ws.send('{"type":"pong"}')
                    elif msg["type"] == "snapshot" and msg.get("final"):
                        print("book ready", len(msg.get("markets", [])), "markets in last part")
                        delay = 1
                    elif msg["type"] == "delta":
                        for c in msg["changes"]:
                            if c["kind"] == "market" and c["op"] == "upsert":
                                m = c["market"]
                                print(m["id"], [(o["name"], o["decimal"], o["fair"]) for o in m["outcomes"]])
        except (OSError, websockets.ConnectionClosed) as e:
            print("disconnected", e, "retry in", delay, "s")
        await asyncio.sleep(delay + random.random())
        delay = min(delay * 2, 30)

asyncio.run(main())

Fields on each outcome: decimal, american, fair (no-vig), fairProb. Market id is eventId:key. Full frame list: WebSocket docs. Schema: schema.

Trial CTA#

Start the 3-day Builder trial from pricing (card required; charged after day 3 unless you cancel), or grab a free REST key first. Related: Node reconnect answer, steam detection.

Questions people ask#

Why does my Free key get 403 on the WebSocket?#

Free and Hobby are REST-only. Opening /v1/stream returns 403 plan_lacks_ws. Upgrade to Builder for the push feed, or poll /v1/odds on Free (30 s delay) / Hobby (real time).

Do I have to answer pings?#

Yes. The server sends ping about every 20 seconds. Reply with {"type":"pong"} or the socket closes after ~60 s of silence.

Is this educational content for adults?#

Yes (18+). It is tooling documentation, not tips or a promise of profit.

The WebSocket plan: Builder, €99/mo.

Real-time Pinnacle odds pushed over one WebSocket, all 13 sports, every market, no-vig fair odds and closing lines. 3 days free, cancel in one click.

Form K-1 · Free API key

Get your free API key.

No password. We email you a link; click it and your key is on screen.

One free key per email. Link valid for 20 minutes.