Official Python client for PinnFeed: real-time Pinnacle odds over REST and WebSocket, with normalized markets, no-vig fair prices, price drops and closing-line history.
- Typed REST client (sync
PinnFeedand asyncAsyncPinnFeed) for every/v1endpoint - Async WebSocket client (
PinnFeedStream) with automatic reconnect, exponential backoff with jitter, heartbeat (answers server pings, detects dead connections) and re-subscribe OddsBook: an in-memory book kept up to date fromsnapshot+deltaframes- Typed
TypedDictmodels (py.typed), Python 3.9+
The package is not on PyPI yet. Install it from GitHub:
pip install git+https://github.com/pinnfeed/pinnfeed-pythonCreate a key at pinnfeed.io/signup and export it:
export PINNFEED_API_KEY=ps_live_...The Free plan is REST only with a 30-second delay; the WebSocket starts at Builder. Plans and limits: pinnfeed.io/pricing. As of 9 Nov 2026, PinnFeed Builder (€99/mo or €990/yr, VAT incl.) was the lowest published live + prematch Pinnacle WebSocket on monthly or annual billing (details and sources).
from pinnfeed import PinnFeed
pf = PinnFeed() # reads PINNFEED_API_KEY
print(pf.account()["plan"])
for s in pf.sports()["sports"]:
print(s["slug"], s["live"], s["prematch"], "in plan" if s["inPlan"] else "")
page = pf.odds("soccer", market="moneyline", main=True, limit=5)
for ev in page["events"]:
for m in ev.get("markets", []):
print(ev["home"], "v", ev["away"], [(o["name"], o.get("decimal"), o.get("fair")) for o in m["outcomes"]])| Method | Endpoint | Notes |
|---|---|---|
account() |
GET /v1/account |
plan, limits, today's usage |
sports() |
GET /v1/sports |
live/prematch counts, inPlan |
events(**filters) |
GET /v1/events |
events without markets |
odds(sport, with_markets=None, **filters) |
GET /v1/odds |
events with markets; sport required |
event(id, **filters) |
GET /v1/events/{id} |
one event with markets |
drops(minutes=, min_pct=, limit=, **filters) |
GET /v1/drops |
recent price drops (window 1–60 min) |
closing(event_id) |
GET /v1/closing/{eventId} |
closing lines, Builder+ |
closing_day(date, **filters) |
GET /v1/closing |
closing lines for a UTC day, Builder+ |
Filters (all optional, lists or comma-separated strings): sport, league, event, market (moneyline, spread, total, team_total), live (True/False/"live"/"prematch"), main, specials, q, limit, offset. The response headers of the last call are on pf.last_headers (e.g. X-Plan, X-Data-Delay-Seconds).
AsyncPinnFeed has the same methods as coroutines.
import asyncio
from pinnfeed import PinnFeedStream
async def main():
async with PinnFeedStream(sports=["soccer", "tennis"], markets=["moneyline"], main=True) as stream:
async for msg in stream:
if msg["type"] == "snapshot" and msg["final"]:
print("book ready:", len(stream.book.events), "events")
elif msg["type"] == "delta":
for ch in msg["changes"]:
if ch["kind"] == "market" and ch["op"] == "upsert":
m = ch["market"]
print(m["eventId"], m["type"], [(o["name"], o.get("decimal")) for o in m["outcomes"]])
elif msg["type"] == "error":
print("server error:", msg["code"], msg["message"])
asyncio.run(main())What the client handles for you (protocol: pinnfeed.io/docs/websocket):
- Sends your
subscriberight after connecting (the server closes with4002if none arrives within 30 s). - Replies
pongto every serverping(sent every 20 s; the server drops clients silent for 60 s) and reconnects if nothing arrives foridle_timeoutseconds (default 60). - Reconnects with exponential backoff + jitter (
initial_backoff,max_backoff,max_retries) on network errors,4008 slow_consumerand429 connection_limit, then re-subscribes and receives a fresh snapshot. - Raises
AuthenticationErroron401or close code4001 key_revoked, andPlanErroron403 plan_lacks_ws(Free/Hobby keys), withupgrade_urlset.
Change filters on the fly with await stream.subscribe(sports=["basketball"]). Pass format="raw" (Pro and Enterprise) for type: "raw" frames with Pinnacle's own records, un-normalized (see pinnfeed.types.RawFrame and the format).
All errors subclass PinnFeedError and carry status, code, message, retry_after_ms and upgrade_url from the API's {"error": {...}} body: AuthenticationError (401), PlanError (403), NotFoundError (404), RateLimitError (429). Codes are listed at pinnfeed.io/docs/errors.
examples/live_stream.py: stream moneyline moves for a sportexamples/dropping_odds_alert.py: alert on drops via REST/v1/drops(any plan) or detect them yourself on the WebSocketexamples/value_vs_fair.py: compare another bookmaker's price with Pinnacle's no-vig fair priceexamples/closing_line_value.py: closing-line value of a bet you placed (Builder+)
PinnFeed carries one bookmaker (Pinnacle), so there is no cross-bookmaker arbitrage example; value_vs_fair.py shows the supported use: comparing your own book's prices with Pinnacle's no-vig line.
pip install -e ".[dev]"
mypy && pytest -qTests run against a local fake server; no API key needed.
Docs · OpenAPI · Pricing · Node/TypeScript SDK · support@pinnfeed.io
MIT licensed. PinnFeed is not affiliated with Pinnacle.