Feed liveChannel BlogSports 12
Transport MQTT / SSE / RESTPush 100–500 msV1
Guides

Pinnacle API Shut Down: The Migration Path

Pinnacle closed its public API in July 2025. Here is the real pinnacle api shut down alternative: push feeds, the code you delete, and the field gotchas.

Pinnacle closed its public API in July 2025. If your service still runs a since-token loop against the old fixtures and odds endpoints, that module is dead code — and replacing it is not a base-URL swap, because the thing that changes is the direction data travels.

The short answer

A pinnacle api shut down alternative is a third-party service that carries Pinnacle's market data — the same events, the same lines, the same stake limits — under its own auth and its own transport. pinnodds is one: an independent API serving real-time live and prematch Pinnacle odds, pushed over WebSocket and Server-Sent Events, with a REST surface for snapshots and drop history.

The migration has three moving parts and that's genuinely all:

  • Transport. The old API was pull. You held a since cursor, hit an endpoint on a timer, diffed the response, and hoped your interval was tight enough. pinnodds pushes. Open /ws/feed and updates arrive when the price moves.
  • Auth. One API key. x-api-key: YOUR_KEY on REST, ?key= on the stream URLs. The same key logs you into the dashboard.
  • IDs. Event identifiers are not the old public-API identifiers. Any mapping table you keep needs rebuilding.

Everything else in this post is detail on those three.

What this is not: a Pinnacle account

Worth being blunt, because it derails sprint planning. A pinnacle api replacement gives you data, not action. You can read every price Pinnacle publishes. You cannot place a bet through it, check a balance, or settle a wager.

If your old integration both pulled odds and fired bets, only the first half has a path here. The second half needs an account and whatever placement route that account offers. Decide which half you were actually depending on before you scope anything.

Why the transport change is the whole story

Polling has a floor on reaction time, and the floor is your interval. Set it to five seconds and you learn about a move a few seconds late, on average, forever. Tighten it and you spend rate limit on responses that are byte-identical to the last one.

Push removes the floor. But the part I'd emphasise to anyone planning this work is not what you build — it's what you get to delete:

  • The since cursor persisted to Redis so a restart doesn't replay the world.
  • The adaptive poll interval that speeds up near kickoff and slows down overnight.
  • The dedupe layer that strips the enormous majority of poll responses that changed nothing.
  • The "did I miss a tick" reconciliation job nobody trusts.

Then there's a capability the old API never had at all: odds-drop alerts as a first-class stream. /odds-drop (live) and /odds-drop-prematch are SSE endpoints that fire when a market's price falls against its own recent history. That was a computation you used to bolt onto your poll loop, usually with a window length someone picked once and never revisited.

The honest caveats

These are the things that will annoy you. Better now than in week three.

There is no historical odds archive. This is a real-time feed. You can start building your own history the moment you connect, but you cannot ask for a closing line from last March. If your model trains on years of closing prices, treat this as a live-execution layer bolted onto a history you source elsewhere. It is not a backfill.

It is Pinnacle only, by design. For a sharp operation, Pinnacle-as-reference-price is usually exactly the shape you want. But if your edge is "compare forty books and find the outlier," a single-book feed will not do that job. Use an aggregator for the wide sweep and pinnodds for the anchor. Running both is a perfectly sane architecture and I'd recommend it over pretending either tool covers the other's ground.

Specials will flatten your parser if you flip the flag casually. Pass include_specials=1 and soccer prematch goes from roughly 1,500 events to roughly 12,400. Player and team props, exact scores, futures and outrights all arrive as their own event rows carrying special, special_category, special_markets and a parent_id back to the parent fixture. They're off by default for exactly this reason.

IDs are not the old IDs. Say it twice because it's the single most common day-one surprise. Rebuild the mapping before your alerting goes live, not after.

Rate limits and push access differ by plan. Line depth does not — every plan gets every line Pinnacle prices. But if your design assumes the WebSocket and SSE streams, confirm which tier includes them on the pricing page before you architect around push.

Step 1 — snapshot on boot, not on a timer

You still want a full picture at startup and after a reconnect. That's REST, called deliberately, not in a loop.

# live markets snapshot
curl -H "x-api-key: $PINNODDS_KEY" \
  "https://pinnodds.com/kit/v1/markets?event_type=live"

# prematch, with props / futures / outrights folded in
curl -H "x-api-key: $PINNODDS_KEY" \
  "https://pinnodds.com/kit/v1/prematch/markets?include_specials=1"

/kit/v1/markets covers live or prematch via event_type. When you want the prematch surface broken out, /kit/v1/prematch/fixtures, /kit/v1/prematch/markets and /kit/v1/prematch/lines do that, and /kit/v1/details fills in a single event. Point your readiness probe at /health or /ping rather than hammering a data endpoint to prove the service is up.

Step 2 — open the socket, delete the loop

/ws/feed is a raw passthrough. Whatever Pinnacle prices comes through, quarter lines included: soccer totals at 1.75 / 2.25 / 2.75 / 3.25 and quarter-ball handicaps at -0.75 / -1.25 / -1.75 / -2.25.

That matters more than it sounds. If your old parser snapped quarter lines to the nearest half because the poll payload felt noisy, that hack is now actively wrong — you'd be collapsing distinct markets that Pinnacle prices separately.

Step 3 — subscribe to drops

The SDKs handle SSE reconnection so you don't hand-roll it:

npm install pinnodds
# or
pip install pinnodds
import os
from pinnodds import Client

client = Client(api_key=os.environ["PINNODDS_KEY"])

for alert in client.odds_drops(mode="live"):
    # sect = market, outcome = the selection that moved
    print(alert["sect"], alert["outcome"],
          alert["from_price"], "->", alert["to_price"],
          "limit", alert["limit"], "nvp", alert["nvp"])

Both Client and the raw endpoints take the same key. Full payload shapes are in the docs.

The field gotcha worth memorising

SSE alerts and REST drop rows describe the same event in different vocabulary. Writing one parser and pointing it at both is the bug I'd bet on you hitting.

An SSE alert carries sect (the market), outcome (the moving selection), from_price / to_price, id (the event), plus limit — Pinnacle's max stake, which is your single best read on how much conviction sits behind that price — and nvp, the no-vig fair price. What it does not carry is a drop percentage.

A REST /api/drops row is the enriched form: market, designation, from / to, event_id, and a precomputed drop_pct. Query it with mode=live or mode=prematch.

SSE  /odds-drop   →  sect, outcome, from_price, to_price, id, limit, nvp
REST /api/drops   →  market, designation, from, to, event_id, drop_pct

Worked example. Say an alert lands with sect: "Total", outcome: "Over", from_price: 1.95, to_price: 1.83, limit: 4200, nvp: 1.88. Your consumer wants a drop percentage to threshold on, and the SSE payload has none — so you compute it at the edge from from_price and to_price, and you stamp it into the same internal field that the drop_pct from /api/drops writes to. Downstream code compares one number and never learns which transport delivered it.

def normalise_sse(a):
    return {
        "event_id": a["id"],
        "market": a["sect"],
        "designation": a["outcome"],
        "from": a["from_price"],
        "to": a["to_price"],
        "drop_pct": (a["from_price"] - a["to_price"]) / a["from_price"] * 100,
        "limit": a["limit"],
        "nvp": a["nvp"],
    }

def normalise_rest(r):
    return {
        "event_id": r["event_id"],
        "market": r["market"],
        "designation": r["designation"],
        "from": r["from"],
        "to": r["to"],
        "drop_pct": r["drop_pct"],
        "limit": None,
        "nvp": None,
    }

Two adapters, one domain object. It costs an hour and it kills the entire class of bug where your reconciliation job and your live consumer disagree about what "the price moved" means.

A rollout order that doesn't burn a weekend

Normally you'd run old and new in parallel. You can't — the old API is gone. So:

  1. Point a staging consumer at /kit/v1/markets and rebuild the event ID mapping. Nothing else proceeds until this is right.
  2. Log /ws/feed to disk for a day. Diff it against your last known-good snapshot. Specifically confirm quarter lines land where you expect them, both totals and handicaps.
  3. Move alerting to the SSE stream, and keep /api/drops as the reconciliation source on a slow cadence — that catches anything a consumer missed during a reconnect.
  4. Flip production. Delete the since-token module.

A trial key takes seconds and no card, so step 1 can start before anyone has a procurement conversation. Plan details sit on the main site.

Takeaway

The migration off the discontinued Pinnacle API is small in lines changed and large in architecture. You are swapping a pull loop for a push socket, which means the code you remove matters more than the code you add. Do two things first: rebuild your event ID mapping against the new feed, and write the adapter that normalises SSE alerts (sect / outcome / from_price) and REST drop rows (market / designation / from / drop_pct) into one internal type — including computing drop_pct yourself on the SSE side. Everything downstream then stops caring which transport delivered the move.

Frequently asked questions

Is there still a public Pinnacle API?

No. Pinnacle closed its public API in July 2025. Market data is still available through independent third-party services such as pinnodds, which carry the same events, lines and stake limits under their own auth and transport.

Can I place bets through a Pinnacle API alternative?

No. pinnodds is a data feed only — it serves live and prematch odds, not account actions. You cannot place a wager, check a balance or settle a bet through it; that requires a betting account and its own placement route.

How do I replace the Pinnacle since-token polling loop?

Take one REST snapshot from /kit/v1/markets on boot and after each reconnect, then open the WebSocket at /ws/feed and let updates arrive when prices move. The since cursor, adaptive poll interval and dedupe layer all get deleted.

Does pinnodds include Pinnacle quarter lines?

Yes. Every line Pinnacle prices comes through, including soccer totals at 1.75 / 2.25 / 2.75 / 3.25 and quarter-ball handicaps at -0.75 / -1.25 / -1.75 / -2.25. Line depth is identical on every plan; plans differ in rate limit and push access.

Is there historical Pinnacle odds data available?

No, there is no historical odds archive. The feed is real-time, so you can build your own history from the moment you connect, but you cannot request closing lines from past seasons.

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

SSE alerts carry the raw move — sect, outcome, from_price, to_price, id, limit and nvp — without a computed percentage. The enriched REST form at /api/drops includes a precomputed drop_pct, or you can calculate it from from_price and to_price at the edge.

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