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

MLB Odds API: Pinnacle Moneyline, Run Line, Totals

How to pull MLB moneyline, run line and totals from a Pinnacle MLB odds API — real endpoints, field names, an SSE worked example, and where the feed falls short.

If you want Pinnacle's MLB moneyline, run line and totals in JSON, you need a feed that pushes — polling a REST endpoint on a timer will hand you the aftermath of every pitcher scratch instead of the move itself. This is the walkthrough: the endpoints that exist, the field names you'll actually parse, a worked SSE example, and the four places where this whole approach is the wrong tool.

Pinnacle shut its own public API in July 2025. pinnodds is an independent service carrying the same market data, so everything below runs against endpoints you can hit today with a trial key.

Why baseball is a push problem, not a polling problem

Three things separate MLB from, say, an NBA game.

Starting pitchers. Prematch baseball is priced off projected starters. Scratch one and the market re-prices hard — often twice, once on the news and again when the book reopens after a suspension. A 30-second poller sees the new number, never the transition. That transition is the information.

In-play is a step function. Live baseball is long stretches of nothing punctuated by a leadoff double that moves the game total instantly. There is no gradual drift to sample. You either catch the tick or you reconstruct it badly.

The run line hides its signal in the price. Almost every MLB game is -1.5/+1.5. The handicap value carries almost no information; the juice attached to it carries all of it, plus whatever alternate rungs Pinnacle chooses to hang. Any feed that flattens "run line" to one row per game has thrown away the market.

That's the case for an mlb odds api pinnacle consumer built on a stream rather than a cron job.

The three core markets, and the F5 split people forget

For a given fixture you're pulling:

  • Moneyline — two-way, home/away. No draw, so no-vig math is simpler than soccer.
  • Run line — main line ±1.5, plus alternates at ±0.5, ±2.5 and beyond wherever Pinnacle prices them.
  • Totals — game total, team totals, and first-five-innings totals where offered.

The thing worth more room: Pinnacle splits many baseball markets into full game and first five innings. F5 is not a variant of the game line. No bullpen risk, no pinch-hitting, pure starter-versus-starter — it reacts to pitcher news with a completely different elasticity than the full-game number. If your model treats F5 as "game line times 0.55," it will be wrong in exactly the situations you care about. Key them separately in your store.

Prematch in three calls

The prematch flow is staged deliberately so you don't drag the whole board to price one game.

# 1. What's on the board
curl -H "x-api-key: $PINNODDS_KEY" \
  "https://api.pinnodds.com/kit/v1/prematch/fixtures?sport=baseball"

# 2. Markets for the fixtures you care about
curl -H "x-api-key: $PINNODDS_KEY" \
  "https://api.pinnodds.com/kit/v1/prematch/markets?sport=baseball"

# 3. Every line and price, including alternates
curl -H "x-api-key: $PINNODDS_KEY" \
  "https://api.pinnodds.com/kit/v1/prematch/lines?event_id=1610234567"

/kit/v1/prematch/lines is the call that matters for run lines and totals — that's where the full ladder lives, not just the headline number. Live has a single REST equivalent:

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

Same endpoint serves prematch with event_type=prematch; /kit/v1/details fills in event metadata. Exact response shapes are in the docs.

The Node SDK (npm install pinnodds) collapses the URL assembly:

import { Pinnodds } from "pinnodds";

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

const fixtures = await client.prematch.fixtures({ sport: "baseball" });
const game = fixtures.find(f => f.home.includes("Dodgers"));
const lines = await client.prematch.lines({ event_id: game.id });

pip install pinnodds gives you the same surface in Python. Both are low-dependency wrappers over REST + SSE. Neither is a framework and neither wants to own your architecture.

Stop polling: take the WebSocket

REST is for snapshots and backfill. For anything time-sensitive, subscribe to the raw passthrough at /ws/feed:

const ws = new WebSocket(
  "wss://api.pinnodds.com/ws/feed?key=" + process.env.PINNODDS_KEY
);

ws.onmessage = (evt) => {
  const msg = JSON.parse(evt.data);
  book.apply(msg); // route by event id, mutate your in-memory book
};

Updates arrive sub-second and you maintain state yourself. Yes, it is more work than a cron job hitting REST. Do it anyway. A five-second poller is, by construction, up to five seconds stale on a market that repriced on one pitch — and on MLB that window is where the entire edge lives.

Auth is one key everywhere: x-api-key header on REST, ?key= on the streams. That same key is your login.

Drops: two shapes, and they are not interchangeable

If what you care about is "this price fell against its own recent history," don't rebuild a diffing engine from the raw feed. There are two forms, and confusing them is the most common integration mistake I see.

SSE/odds-drop for live, /odds-drop-prematch for prematch — is the lean, fast form. Fields: sect (the market), outcome (the moving selection), from_price / to_price, id (the event), limit (Pinnacle's max stake), and nvp (the no-vig fair price). There is no drop percentage. Compute it yourself.

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

Note the naming actually differs between the two — sect versus market, outcome versus designation, id versus event_id. Normalise once at the edge or you will spend a Sunday debugging a KeyError.

limit is the underrated field for baseball. Pinnacle's max stake tells you how much conviction sits behind a number. A run line drifting while the limit is being raised is a different animal from one drifting while the limit is being cut — the first is a book that wants the action, the second is a book protecting itself.

Worked example: total-only alerts, gated on limit

import json, requests, sseclient

KEY = "YOUR_KEY"
resp = requests.get(
    "https://api.pinnodds.com/odds-drop?key=" + KEY,
    stream=True,
    headers={"Accept": "text/event-stream"},
)

MIN_LIMIT = 2000  # your own threshold, in the feed's stake units

for event in sseclient.SSEClient(resp).events():
    d = json.loads(event.data)
    if not d["sect"].lower().startswith("total"):
        continue
    if d["limit"] < MIN_LIMIT:
        continue  # thin market, ignore the move

    drop = (d["from_price"] - d["to_price"]) / d["from_price"]
    edge = (d["nvp"] - d["to_price"]) / d["to_price"]
    print(
        f"{d['id']} {d['outcome']} {d['from_price']}→{d['to_price']} "
        f"drop={drop:.2%} nvp={d['nvp']} edge_vs_nvp={edge:+.2%} limit={d['limit']}"
    )

Two lines of gating turn a firehose into a shortlist. The nvp comparison is the part most people skip: a big raw drop into a still-vigged price is less interesting than a small drop that takes the offer past fair value.

Alternate ladders come through in full

Every line Pinnacle prices arrives — the whole alternate run line ladder, the whole totals ladder, not just the main number. The same depth guarantee holds across sports, which matters if MLB is one leg of a multi-sport model: 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, never in which lines you can see. That's deliberate — a model fed partial ladders is broken regardless of what you paid for it.

Props, futures and outrights are opt-in

Strikeout props, hits allowed, total bases, team props, exact scores, division futures, World Series outrights — all available, all off by default:

curl -H "x-api-key: $PINNODDS_KEY" \
  "https://api.pinnodds.com/kit/v1/prematch/markets?sport=baseball&include_specials=1"

Each special arrives as its own event row carrying special, special_category, special_markets and a parent_id back to the parent fixture. Join on parent_id to hang props off the game.

The flag is off by default because specials dominate the payload. For scale, soccer prematch is roughly 1,500 events without it and roughly 12,400 with it. Baseball prop boards are similarly top-heavy. If you only need moneyline, run line and totals, leaving it off cuts parse time and memory substantially — and if you do need props, run them on a separate, slower job rather than bloating your hot path.

Where this feed is the wrong tool

Read this before you build on it.

There is no historical odds archive. This is a real-time feed. Nothing here returns "the closing run line for every MLB game in 2023." If you're backtesting closing-line value, you capture and store the stream yourself starting today, or you buy history from someone else. Build the capture layer on day one — retrofitting one after a month of live traffic is genuinely miserable, and you'll have a month-shaped hole in your dataset forever.

Pinnacle only. One book. If your strategy is arbitrage across a dozen sportsbooks, or you need US-market books for line shopping, this is not an aggregator and won't pretend to be. Its job is to be the sharp reference price you measure other books against. Running pinnodds alongside a broad aggregator is a perfectly sane architecture, and for arb teams it's the right one.

No scores, no settlement, no stats. You get odds. Inning-by-inning box scores, pitch-level data, injury wires — other vendors. Don't make this feed your source of truth for game state.

You own state on the WebSocket. /ws/feed is a passthrough. Reconnect logic, gap handling, snapshot-then-delta reconciliation: your code. The SDKs and REST endpoints smooth a lot of edges, but the raw stream assumes competence.

Rate limits bite naive fan-out. Hitting /kit/v1/prematch/lines once per fixture per minute across a full slate will find the ceiling. Batch at the markets level, drill into lines only for games you're actively pricing. Tiers are on the pricing page — Pro at $99/mo, Pro + SSE at $149/mo, Scale at $229/mo, with a free trial key that takes seconds and no card.

Takeaway

Build MLB ingestion in two layers. Layer one is a /ws/feed consumer maintaining an in-memory book keyed by event id, with /kit/v1/markets?event_type=live as the cold-start snapshot and the reconnect resync. Layer two is /odds-drop SSE for alerting, computing your own percentage from from_price and to_price and gating every alert on limit — because in baseball a two-cent move on a rising limit beats a ten-cent move on a limit that's being pulled. Keep include_specials=1 on a separate, slower job, and normalise sect/outcome to market/designation at the edge so the SSE and REST drop shapes stop fighting each other. Response shapes are in the docs.

Frequently asked questions

Is there still a public Pinnacle API for MLB odds?

No. Pinnacle closed its own public API in July 2025. pinnodds is an independent service that carries the same market data, including MLB moneyline, run line and totals, over REST, WebSocket and SSE.

How do I get Pinnacle alternate run lines and totals, not just the main line?

Call GET /kit/v1/prematch/lines with an event_id. That endpoint returns the full ladder — every handicap and every total Pinnacle is pricing — and the depth is identical on every plan.

What is the difference between the SSE odds-drop alert and the REST /api/drops response?

The SSE alert is the lean form with sect, outcome, from_price, to_price, id, limit and nvp, and no drop percentage. The REST /api/drops row is the enriched form with market, designation, from, to, event_id and a precomputed drop_pct.

Can I get MLB player props like strikeouts and total bases?

Yes, pass include_specials=1. Props, exact scores, futures and outrights come back as their own event rows carrying special, special_category, special_markets and a parent_id linking them to the parent fixture.

Does pinnodds have historical MLB closing lines for backtesting?

No. It is a real-time feed with no historical archive, so you need to capture and store the stream yourself from day one or source history from another vendor.

How much does an MLB odds API from Pinnacle cost?

Paid plans start at $99/mo for Pro, $149/mo for Pro + SSE and $229/mo for Scale, with quarterly and semi-annual options. A free trial key takes seconds and needs no card.

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