Feed liveChannel BlogSports 12
Transport MQTT / SSE / RESTPush 30–150 msV1
API & Data

Pinnacle API Shut Down: The Alternative & Migration Path

Pinnacle shut down its public API in July 2025. Here's the pinnacle api shut down alternative, the endpoint mapping, the field-name gotchas and a migration order.

Pinnacle closed its public API in July 2025, and if your model was wired into it, this is not a patch — you're rewriting the part of your system that decides when it learns about a price. The market data itself is still available: pinnodds is an independent service carrying the same Pinnacle market data, pushed over WebSocket and Server-Sent Events instead of pulled on a timer.

What follows is the migration map I'd hand to an engineer on day one. What broke, which endpoint replaces what, where the field names diverge (they do, and it's the thing that costs people an afternoon), and where this feed is flatly the wrong choice.

What actually broke in July 2025

The old integration pattern was simple and slightly ugly. Authenticate, hit the odds endpoint on a timer, diff the response against your last snapshot, act on the delta. Everyone who built on Pinnacle wrote some version of that loop — and then wrote the retry logic, the "were we rate limited" guard, and the cron job that kicked the worker when it silently wedged.

Three things died at the same moment:

  • The authenticated REST surface your client was calling.
  • The polling cadence your entire detection logic was tuned around.
  • Your line depth, if you replaced it carelessly — most substitutes carry the headline number and stop there.

That last one is the underestimated one. If your edge lives at 2.25 goals or a -1.25 handicap, a feed that gives you the main line and nothing else isn't a replacement. It's a different product that happens to mention the same sport.

The endpoints that replace what you lost

pinnodds splits the surface deliberately: REST for state, push for change. One API key does both — x-api-key: YOUR_KEY on REST, ?key= on the streams. That key is also your login, so there's no second credential to rotate.

REST, for snapshots and for backfill after a reconnect:

  • GET /kit/v1/markets — live or prematch, switched by event_type
  • GET /kit/v1/details — the detail view for one event
  • GET /kit/v1/prematch/fixtures, /kit/v1/prematch/markets, /kit/v1/prematch/lines
  • GET /api/drops with mode=live or mode=prematch
  • GET /health and GET /ping, which belong in your own monitoring, not just in a smoke test

Push, for anything time-sensitive:

  • /ws/feed — a raw WebSocket passthrough of the odds feed
  • /odds-drop and /odds-drop-prematch — SSE alerts fired when a market's price falls against its own recent history

Line depth is the first thing to verify in any pinnacle api shut down alternative, before you look at latency, pricing or SDK ergonomics. Every line Pinnacle prices comes through, quarter lines included: soccer totals at 1.75 / 2.25 / 2.75 / 3.25, quarter-ball handicaps at -0.75 / -1.25 / -1.75 / -2.25. Depth is identical on every plan — plans differ in rate limit and push access, not in what you're allowed to see. Field reference lives in the docs.

Specials are off by default, on purpose

Pass include_specials=1 and player props, team props, exact scores, futures and outrights arrive as their own event rows, each carrying special, special_category, special_markets and a parent_id pointing back at the parent fixture.

They're opt-in because they dominate the payload. Soccer prematch is roughly 1,500 events without the flag and roughly 12,400 with it. If you flip it on globally and then wonder why the ingest worker is chewing memory, that's your answer. Scope it per sport, to the sports you actually price.

Where push earns its keep

Polling has a floor you cannot code your way under: the interval. Poll every five seconds and your worst-case detection latency is five seconds plus request time, forever. Push removes the interval — the update lands when the price moves, sub-second.

Two places where that changes outcomes rather than just feeling nicer.

Live markets. In-play prices move on events, and the interesting window after a red card or a goal is short. A polling loop tuned to stay polite with a rate limit is structurally too slow for that window, no matter how tight the code is. This was the quiet, permanent tax on the old integration.

Drop detection. A price falling against its own recent history is worth knowing about while it's happening. On the next tick, you're reading history. That's the whole reason the SSE endpoints exist.

A minimal live drop consumer with the official Node SDK (npm install pinnodds; pip install pinnodds for Python, both zero/low dependency):

import { PinnoddsClient } from "pinnodds";

const client = new PinnoddsClient({ apiKey: process.env.PINNODDS_KEY });

// Live odds-drop alerts over SSE
client.onOddsDrop((alert) => {
  // alert: { id, sect, outcome, from_price, to_price, limit, nvp }
  const move = alert.from_price - alert.to_price;

  // Filter on stake limit first: a move on a market Pinnacle will
  // barely take action on is usually noise.
  if (alert.limit > 1000 && move > 0.05) {
    console.log(
      `${alert.sect} / ${alert.outcome}: ${alert.from_price} -> ${alert.to_price}`,
      `fair ${alert.nvp}, max stake ${alert.limit}`
    );
  }
});

Note what I filtered on. limit is Pinnacle's maximum stake on that selection, and it is the cheapest signal-quality filter you have — it tells you how much conviction sits behind the number. nvp is the no-vig fair price, which is what you actually want to compare against your own model output rather than the offered price.

The data shapes differ — write the normaliser first

This is the migration detail that bites, so here it is in plain terms.

An SSE alert is lean because it's built for speed. The market is in sect, the moving selection in outcome, prices in from_price / to_price, the event in id, plus limit and nvp. What it does not include is a drop percentage. You compute that yourself.

A REST /api/drops row is the enriched form of the same phenomenon: market, designation, from / to, event_id, and a precomputed drop_pct.

Same price move, two dialects. Point one handler at both sources without translating and you'll spend an evening chasing undefined. Do it at the edge instead:

def normalise(row, source):
    if source == "sse":
        frm, to = row["from_price"], row["to_price"]
        return {
            "event_id": row["id"],
            "market": row["sect"],
            "selection": row["outcome"],
            "from": frm,
            "to": to,
            "drop_pct": (frm - to) / frm * 100,
            "limit": row.get("limit"),
            "nvp": row.get("nvp"),
        }
    return {
        "event_id": row["event_id"],
        "market": row["market"],
        "selection": row["designation"],
        "from": row["from"],
        "to": row["to"],
        "drop_pct": row["drop_pct"],
        "limit": None,
        "nvp": None,
    }

Twenty lines, written before the business logic, and your alerting rules never learn that two vocabularies exist.

A worked example: rebuilding one alert

Say your old system polled every 5s, diffed soccer totals, and paged you when a total's price dropped more than 3% on a market with a decent stake limit. Here's the same rule after migration.

The old detection step becomes an SSE subscription, and the 3% threshold — which the SSE payload doesn't hand you — comes out of the normaliser above:

for raw in sse_stream("/odds-drop"):          # live drops
    ev = normalise(raw, "sse")
    if ev["market"].lower().startswith("total") \
       and ev["drop_pct"] >= 3.0 \
       and (ev["limit"] or 0) >= 1000:
        page_me(ev)

Then, and this is the part people skip, you reconcile. When the stream reconnects you have a gap, so pull GET /api/drops?mode=live, normalise those rows with source="rest", and dedupe on (event_id, market, selection, to). REST is your source of truth for state; the stream is your source of truth for change. Confusing those two is the single most common architectural mistake in a push migration.

A migration order that works

The sequence matters more than the code.

  1. Get a trial key and hit /ping and /health from the environment that will run in production. Confirm auth before you touch application code.
  2. Swap the snapshot first. Point your existing snapshot job at /kit/v1/markets (or the prematch trio for pre-game) and keep your diffing logic exactly as it is. Your system works again — same architecture, new source. This is an afternoon.
  3. Verify depth against something you know. Pull a soccer fixture and check that 2.25 and -1.25 are genuinely present, with prices, not just listed. This is your data-quality gate, and it's non-negotiable.
  4. Add push in parallel. Open /ws/feed and let it run alongside the poller for a day, logging both. You'll see, in your own data, exactly what the poll interval was costing you.
  5. Move detection to SSE, keep REST for reconciliation on reconnect.
  6. Delete the poller — only after step 5 has run clean through a busy weekend.

The reason for that order: step 2 restores service immediately, and everything after it is an optimisation you can roll back without going dark.

Where this feed is the wrong tool

Read this before you sign up, not after.

There is no historical odds archive. This is a real-time feed. If the project is a backtest across three seasons of closing lines, you have to record forward from today — the past does not ship with the key. People discover this after the model is built. Start the recorder now, even if you won't query it for months.

It's Pinnacle only. That is the design: Pinnacle is the sharp reference price and the value is depth and fidelity on that one book. But if your product is a price-comparison grid across 40 sportsbooks, or you need book-specific promos and boosts, you want a multi-book aggregator. Running both is a perfectly sound architecture — aggregator for breadth, pinnodds for the reference line — and I'd take that over forcing either tool to do the other's job.

Push access is a paid tier. The trial key takes seconds and no card, and REST runs on Pro at $99/mo, but SSE drop alerts start at Pro + SSE ($149/mo), with Scale at $229/mo for higher rate limits; quarterly and semi-annual terms exist. If the budget is genuinely zero, this isn't your tool. Pricing.

You cannot place a bet through it. It serves odds. Execution was always your problem and still is.

Specials will hurt a careless integration. 12,400 events versus 1,500 on soccer prematch is not a rounding error, and it will show up as memory pressure and slow parses long before it shows up as a clear error.

Takeaway

The migration is mostly a mental shift: stop asking for prices, start receiving them. Restore service with a REST snapshot swap, run /ws/feed beside the old poller until you trust it, then move detection to SSE with /api/drops for reconciliation after every reconnect. And write the normaliser before the business logic — sect/outcome/from_price and market/designation/from describe the identical price move in two dialects, and only one of them hands you drop_pct. Grab a trial key, curl /ping, and read the field reference in the docs before you write a single handler.

Frequently asked questions

Is there still a public Pinnacle API?

No. Pinnacle closed its public API in July 2025. Independent services such as pinnodds carry the same Pinnacle market data, but the direct public endpoints are gone.

What is the best alternative to the Pinnacle API after the shutdown?

You need a feed that keeps full line depth, not just the main line. pinnodds pushes live and prematch Pinnacle odds over a WebSocket at /ws/feed plus SSE drop alerts, and every line Pinnacle prices — including quarter lines like 2.25 and -1.25 — comes through on every plan.

How do I migrate from a Pinnacle polling loop to a pushed odds feed?

Swap your snapshot job to GET /kit/v1/markets first so service is restored with your existing diffing logic intact, then run /ws/feed alongside the poller for a day, then move detection to the SSE endpoints with REST reconciliation on reconnect. Delete the poller last.

Does pinnodds include historical Pinnacle odds?

No. It is a real-time feed with no historical archive, so if you need closing lines for backtesting you have to record them forward from the day you start.

Why does my SSE odds-drop alert have no drop percentage?

SSE alerts are the lean form: they carry id, sect, outcome, from_price, to_price, limit and nvp, but no drop percentage. Compute it yourself, or read the enriched REST rows from /api/drops, which include a precomputed drop_pct.

How much does a Pinnacle odds API replacement cost?

A free trial key takes seconds with no card. Paid plans start at $99/mo for Pro (REST), $149/mo for Pro + SSE if you need pushed drop alerts, and $229/mo for Scale, with quarterly and semi-annual options.

Get real-time Pinnacle odds in your code

Live & prematch markets with instant odds-drop alerts over SSE and WebSocket. Free trial key in seconds — no card.

Start free trial