Python guide

How to use the Pinnacle odds API with Python

PinnWire is an independent, real-time Pinnacle odds API for Python developers. It provides current live and prematch prices for 13 sports, a recent line-movement buffer, paid SSE drop alerts and an optional raw WebSocket—without making you reverse-engineer a sportsbook.

Fastest route: run pip install pinnwire, create Client("demo"), then call markets(1) for live soccer or prematch_fixtures(1) for upcoming soccer. Prices are decimal and full-game markets live under periods["num_0"].

Is PinnWire the right Python sports betting API?

Good fit

Use PinnWire for

Pinnacle reference prices, live-odds dashboards, line-movement tracking, model inputs, fair-price calculations, player props, esports and real-time drop alerts.

Choose another tool

Not the right fit for

Multi-book comparison from one API, bet placement, account access, guaranteed picks or a ready-made historical archive. PinnWire is a data service, not a bookmaker.

PinnWire is especially useful when your Python project needs Pinnacle as a sharp market reference. For arbitrage, pair it with prices from other books. For backtests, save snapshots yourself. See the broader Pinnacle API comparison before choosing a feed.

Install the Python client

The official package supports Python 3.8+ and uses requests. Install it from PyPI:

python -m pip install pinnwire

Start with the public demo key. It needs no signup and works on REST endpoints:

from pinnwire import Client

api = Client("demo")
health = api.health()

print(health["status"])
print(health["last_odds_update_seconds_ago"])

Demo limit: 10 requests/minute and 50/day in one shared bucket for everyone. It is for a quick test, not sustained development. If it returns HTTP 429, wait for the shared limit to reset or get a free personal trial key (20/minute, 100/day). Demo and trial keys are REST-only.

Fetch live Pinnacle odds

markets(sport_id) returns the current live board for one sport. This runnable example prints available full-game moneylines for live soccer:

from pinnwire import Client, SPORTS

api = Client("demo")
data = api.markets(SPORTS["SOCCER"])

print("Response generated:", data["generated_at"])

for event in data.get("events", []):
    full_game = event.get("periods", {}).get("num_0", {})
    moneyline = full_game.get("money_line")
    if not moneyline:
        continue

    prices = " | ".join(
        f"{side}: {price}"
        for side, price in moneyline.items()
        if price is not None
    )
    print(f'{event["home"]} vs {event["away"]} — {prices}')

Sport IDs are stable: soccer 1, tennis 2, basketball 3, hockey 4, football 5, baseball 6, rugby 7, MMA 8, boxing 9, volleyball/handball 10, esports 11, golf 12 and cricket 13.

Not every event exposes every market or period, so the example uses .get(). A two-way sport may omit draw. Always use generated_at to confirm the response is fresh rather than treating cached output as live.

Fetch prematch fixtures and markets

Use prematch_fixtures() for upcoming events. It returns the same event and period structure as the live endpoint:

from pinnwire import Client, SPORTS

api = Client("demo")
data = api.prematch_fixtures(SPORTS["BASKETBALL"])

for event in data.get("events", [])[:10]:
    game = event.get("periods", {}).get("num_0", {})
    spread_lines = game.get("spreads", {})
    total_lines = game.get("totals", {})

    print(event["home"], "vs", event["away"], event["starts"])
    print("  spreads:", spread_lines)
    print("  totals: ", total_lines)

The main shapes are money_line, spreads, totals, team_total and team_totals. Full-game prices are normally num_0; later period keys vary by sport and only appear when published. Add include_specials=1 to return player props and other specials as separate event rows.

data = api.prematch_fixtures(
    SPORTS["SOCCER"],
    include_specials=1,
)

Call the REST API with requests

You do not have to use the SDK. This version reads a personal key from the environment, falls back to the demo key, applies a timeout and honors rate-limit responses:

import os
import time
import requests

API_KEY = os.getenv("PINNWIRE_KEY", "demo")
URL = "https://pinnwire.com/kit/v1/markets"

response = requests.get(
    URL,
    params={"sport_id": 1, "fresh": str(time.time_ns())},
    headers={"x-api-key": API_KEY},
    timeout=15,
)

if response.status_code == 429:
    wait = response.headers.get("Retry-After", "a short while")
    raise SystemExit(f"Rate limited; retry after {wait} seconds")

response.raise_for_status()
data = response.json()
print(data["generated_at"], len(data.get("events", [])))

The optional fresh parameter is an ignored cache-buster. Authentication also works as ?key=YOUR_KEY, but the x-api-key header keeps personal keys out of URLs and logs. Full response shapes and errors are in the API documentation.

Calculate no-vig fair odds in Python

Raw bookmaker prices include margin. The proportional method below normalizes a complete moneyline to 100% and returns a simple fair-price baseline:

def remove_vig(moneyline):
    prices = {
        side: float(price)
        for side, price in moneyline.items()
        if side in {"home", "draw", "away"} and price
    }
    if len(prices) < 2:
        raise ValueError("Need a complete two-way or three-way market")

    implied = {side: 1 / price for side, price in prices.items()}
    overround = sum(implied.values())
    return {
        side: 1 / (probability / overround)
        for side, probability in implied.items()
    }


# moneyline comes from periods["num_0"]["money_line"]
fair_odds = remove_vig(moneyline)
for side, price in fair_odds.items():
    print(side, round(price, 3), f"({1 / price:.1%} fair probability)")

This is an analytical estimate, not a prediction or guarantee. PinnWire also supplies nvp, its no-vig decimal price, on detected drop records. Read the no-vig fair odds guide for the exact interpretation.

Find recent odds movement

The REST drops endpoint keeps roughly three hours of detected movement. The demo key can query it:

from pinnwire import Client

api = Client("demo")
result = api.drops(
    mode="prematch",
    min_drop_pct=5,
    max_age_sec=600,
)

for drop in result.get("drops", []):
    print(
        drop["home"], "vs", drop["away"],
        drop["market"], drop["side"],
        f'{drop["from"]} -> {drop["to"]}',
        f'drop={drop["drop_pct"]:.2f}%',
        f'fair={drop.get("nvp")}',
    )

A shorter price is evidence of market movement, not proof of who placed a bet or why. Store the records yourself if you need longer history. See every filter in the dropping-odds API guide.

Stream drop alerts instead of polling

Paid Stream, Pro + Drops and Scale keys can consume live or prematch drop alerts over server-sent events (SSE). The Python client reconnects automatically:

import os
from pinnwire import Client

api = Client(os.environ["PINNWIRE_KEY"])

for drop in api.stream_drops(min_drop=5):
    print(
        drop["home"], "vs", drop["away"],
        drop["from_price"], "->", drop["to_price"],
        "fair:", drop.get("nvp"),
    )

The public demo and free trial do not include SSE. For every live and prematch market update—not only detected drops—the optional raw Pinnacle WebSocket API is the better interface and needs the WebSocket add-on.

What to build next

  • Odds dashboard: poll current snapshots by sport and display num_0 moneyline, spread and total markets.
  • Line-movement database: save last and use the since parameter to request changed events, or store drop records as they arrive.
  • Value-betting model: treat Pinnacle’s de-vigged price as one market input, then compare it with independently collected offered prices.
  • Alert bot: route paid SSE drops to Telegram, Discord or email after applying sport, market, age and drop-size filters.

For production, keep the key in an environment variable, honor Retry-After, set request timeouts, check generated_at, and expect optional fields. PinnWire returns current data; your application owns storage, comparison logic and betting decisions.

Python and Pinnacle API questions

Does PinnWire work with Python?

Yes. Install the official requests-based client with pip install pinnwire, or call the REST endpoints with Python requests. Both return JSON with current live and prematch Pinnacle odds in decimal format.

Can I test the Pinnacle odds API in Python without signing up?

Yes. Client("demo") works for REST requests without signup. The public demo allowance is shared across all users and is limited to 10 requests per minute and 50 per day, so use a free personal trial key for reliable development.

Can PinnWire find arbitrage opportunities by itself?

No. PinnWire supplies Pinnacle data, not a multi-book odds comparison. Use it as the sharp reference side and add prices from other sources to build a complete arbitrage or value-betting scanner.

Does PinnWire provide historical odds for Python research?

PinnWire provides current live and prematch snapshots plus an approximately three-hour buffer of detected price drops. It is not a historical archive; store snapshots or drops in your own database for backtesting and long-term analysis.