On this page
Thank you for requesting the kit. Everything below is the document we wished had existed in August 2025: a map from the old Pinnacle API to what works now, the differences that break things, a checklist, and code you can paste. The longer reference behind the mapping is on the documentation page; the reasoning behind our choice of pinnodds is on the alternatives page.
Endpoint mapping, old to new
Every data endpoint of the old Pinnacle API has a direct pinnodds equivalent; the account and betting endpoints have none. Paths below are relative to https://pinnodds.com and authenticate with the x-api-key header.
Scroll for more
| Old Pinnacle endpoint | pinnodds equivalent | Note |
|---|---|---|
/v1/sports | Fixed ids: 1 Soccer, 2 Tennis, 3 Basketball, 4 Hockey, 5 Am. Football, 6 Baseball, 7 Rugby, 8 MMA, 9 Boxing, 10 Other, 11 Esports, 12 Golf, 13 Cricket | Ids differ from Pinnacle's; SDKs export SPORTS |
/v2/leagues | League fields inside fixtures and markets responses | No separate league endpoint |
/v1/fixtures | /kit/v1/prematch/fixtures?sport_id=… | since, include_specials=1 supported |
/v1/odds, /v3/odds | /kit/v1/markets?sport_id=…&event_type=live or prematch | since, include_specials=1 supported |
/v1/line | /kit/v1/prematch/lines?event_id=…&market_type=totals | Line ladder per market type |
| Per-event odds (client-side filter) | /kit/v1/details?event_id=…, /kit/v1/prematch/markets?event_id=… | New, dedicated calls |
/v1/fixtures/special, /v1/odds/special | include_specials=1 on fixtures and markets | Folded into main responses |
/v1/periods | Period id on each market row; 0 = full match | No lookup endpoint |
/v1/inrunning | event_type=live on markets | Live is an event property |
| Own polling for line moves | /api/drops, SSE stream_drops, wss://pinnodds.com/ws/feed?key=KEY | New capability |
/v1/fixtures/settled | Not listed in public docs; check current docs | Keep your results source |
/v2/bets, /v1/client/balance, /v1/currencies | None | Data-only feed |
Behavioral differences
Seven differences account for nearly every production surprise during a migration. Read them before you change any code.
- Auth header instead of Basic auth. Send
x-api-key: KEY. No username, no password, no base64. Kit endpoints also accept?key=, but keep that out of logs. - No funded account, no IP allow-list. The key is the whole identity. Cloud egress IPs can rotate freely. Remove any allow-list management code.
- Rate limits by plan, not fair use. Trial and Stream allow 20 requests a minute, 100 an hour and 100 a day. Pro and Pro + SSE allow 10 a second, Scale 30 a second. Exceeding them returns a rate-limit error with a retry-after value; the SDKs raise
RateLimitErrorcarrying it. - One stream per key. A second SSE or WebSocket connection on the same key disconnects the first. One consumer per key, or a relay you run.
sincecursors still work. Markets and prematch fixtures acceptsinceand return alastvalue to feed into the next call. Keep your delta loop; change the paths.- Two drop shapes. REST drop rows carry
from,to,nvpand a computeddrop_pct, plus event fields such ashome,away,market,period,designation,points. SSE alerts carrysect,outcome,from_price,to_price,id,limit,nvpand no percentage; derive it as (1 − to ÷ from) × 100. - Flatter responses and new sport ids. The old nested league, event, period document becomes a list of events with markets attached. Sport ids are pinnodds' own. Conventions that do not change: decimal prices, period 0 for the full match, handicaps from the home side, quarter lines as separate rows.
Cut-over checklist
Work through these ten items in order. Most teams finish the first six in an afternoon and spend the rest of the time on the parser and on measuring their real request rate.
- Sign up and obtain a key. pinnodds issues one in seconds, without a card or Pinnacle account.
- Make one request by hand with curl and save a real response for each endpoint you will use.
- Translate your sport id constants to pinnodds ids.
- Replace Basic auth with the
x-api-keyheader; delete IP allow-list logic. - Swap endpoint paths using the mapping table; drop the separate leagues and periods calls.
- Rewrite the parser for the flatter event list; keep period, handicap and decimal conventions.
- Keep the
sinceloop, pointing it at the new paths and storinglastper sport and event type. - Add
RateLimitErrorhandling that sleeps forretry_after; run the 3-day demo and log your actual requests per minute. - If you polled to detect drops, replace that loop with the drop buffer or the SSE stream, and pick a plan with SSE if you keep the stream.
- Choose the plan whose limit sits comfortably above your measured rate, using the pricing page, then switch DNS, cron or systemd from the old job to the new one.
Python snippets
Install with pip install pinnodds. Python 3.8 or later, one dependency. The three snippets cover a board fetch, a delta loop with rate-limit handling, and the SSE drop stream.
Live board
# pip install pinnodds
from pinnodds import Client, SPORTS
api = Client("YOUR_KEY")
board = api.markets(sport_id=SPORTS["soccer"], event_type="live")
print(len(board["events"]), "live events") Delta loop with since
import time
from pinnodds import Client, SPORTS, RateLimitError
api = Client("YOUR_KEY")
last = None
while True:
try:
resp = api.markets(sport_id=SPORTS["soccer"], event_type="prematch", since=last)
except RateLimitError as e:
time.sleep(e.retry_after or 5)
continue
last = resp.get("last") or last
for ev in resp["events"]:
pass # apply changes to your store
time.sleep(60) Drop stream over SSE
from pinnodds import Client
api = Client("YOUR_KEY")
for d in api.stream_drops(min_drop=5):
pct = (1 - d["to_price"] / d["from_price"]) * 100
print(f'{d["home"]} v {d["away"]} {d["sect"]} {d["outcome"]} '
f'{d["from_price"]} -> {d["to_price"]} ({pct:.1f}%) nvp={d.get("nvp")}') Node.js snippets
Install with npm install pinnodds. Zero dependencies, TypeScript types included. Same three patterns as the Python versions.
Live board
// npm install pinnodds
import { Client, SPORTS } from "pinnodds";
const api = new Client(process.env.PINNODDS_KEY);
const board = await api.markets({ sportId: SPORTS.soccer, eventType: "live" });
console.log(board.events.length, "live events"); Delta loop with since
import { Client, SPORTS, RateLimitError } from "pinnodds";
const api = new Client(process.env.PINNODDS_KEY);
let last;
for (;;) {
try {
const resp = await api.markets({ sportId: SPORTS.soccer, eventType: "prematch", since: last });
last = resp.last ?? last;
for (const ev of resp.events) { /* apply changes */ }
} catch (e) {
if (e instanceof RateLimitError) { await new Promise(r => setTimeout(r, (e.retryAfter ?? 5) * 1000)); continue; }
throw e;
}
await new Promise(r => setTimeout(r, 60_000));
} Drop stream over SSE
import { Client } from "pinnodds";
const api = new Client(process.env.PINNODDS_KEY);
for await (const d of api.streamDrops({ minDrop: 5 })) {
const pct = (1 - d.to_price / d.from_price) * 100;
console.log(d.home, "v", d.away, d.sect, d.outcome, d.from_price, "->", d.to_price, pct.toFixed(1) + "%");
} REST drop buffer
If you do not want a long-lived connection, poll the drop buffer instead. This returns prematch drops of at least 2 percent from the last hour; add mode=live or sport_id to narrow it.
curl -H "x-api-key: YOUR_KEY" \
"https://pinnodds.com/api/drops?mode=prematch&min_drop_pct=2&max_age_sec=3600" Verifying the cut-over
A migration is done when the new feed produces the same decisions as the old one would have. Five checks catch nearly all regressions before they reach production.
- Event coverage. For one sport, list the prematch events from the new feed and compare against the fixtures your old store holds for the same window. Expect the sets to match once you account for events that started or settled in between.
- Sign of the handicap. Pick three spread markets and confirm that a negative value means the home side gives points in your parsed output. A flipped sign is the most common silent bug in a migration.
- Quarter lines. Confirm that a 2.25 total or a −0.75 handicap appears as its own row with its own price and is not merged into the nearest half line.
- Cursor continuity. Run the delta loop, stop it for five minutes, restart it with the stored
lastvalue, and confirm that changes made during the gap arrive on the first request. - Rate limit behavior. Deliberately exceed the limit on the Trial plan and confirm your code sleeps for the retry-after value instead of retrying in a tight loop.
Keep the old parser's output for a day of historical data if you have it, and diff the two parsers' results on the same events. Where they differ, the new feed's flatter structure or a renamed field is usually the cause, and the pinnodds documentation is the reference to settle it.
More code and reference
Longer, runnable versions of everything here live on pinnacleapi.dev, and the provider's reference is the authority on field names.
- Python quickstart
- Node.js quickstart
- SSE consumer
- Raw WebSocket client with reconnect
- Recipes, including a Telegram drop-alert bot
- pinnodds documentation for the current field reference
- Archived Pinnacle API documentation on this site, for the old side of the mapping
- How to read Pinnacle lines, if you are new to quarter lines and no-vig prices