pinnacle odds api / node.js / fetch / websockets
Pinnacle odds API in Node.js
GET/kit/v1/markets?sport_id=2&since=141
changed events only200 OK
One short Node.js script for the Pinnacle odds API with nothing to install: a polling loop on the global fetch that asks only for what changed since the last call and prints each moved money line. Export your key and the base URL from the docs, then run it unchanged.
Create a free keyPoll with a cursor
The loop keeps last from each response and sends it back as since, so after the first call only changed events arrive. A 429 carries Retry-After, and the loop waits exactly that long instead of crashing.
Node 18 and later ship fetch globally, so the script has no dependencies. At one call every three seconds the free key's 100 daily calls last five minutes. Poll every 15 minutes for an all-day test, or move to poll for 10 calls a second.
// Poll the REST feed with a
// since cursor. Node 18+.
const HOST = process.env.ODDS_BASE_URL;
const PATH = "/kit/v1/markets";
const KEY = process.env.ODDS_API_KEY;
const HDRS = {
"x-portal-apikey": KEY,
};
const sleep = (ms) =>
new Promise((r) =>
setTimeout(r, ms));
function moneyLine(ev) {
const p = ev.periods || {};
return (p.num_0 || {}).money_line;
}
async function poll(sportId, ms) {
let since = null;
for (;;) {
const q = new URLSearchParams({
sport_id: String(sportId),
});
if (since !== null)
q.set("since", String(since));
const r = await fetch(
HOST + PATH + "?" + q,
{ headers: HDRS });
if (r.status === 429) {
const w =
r.headers.get("Retry-After");
await sleep(Number(w) * 1000);
continue;
}
if (!r.ok)
throw Error("HTTP " + r.status);
const data = await r.json();
since = data.last;
for (const ev of data.events) {
const ml = moneyLine(ev);
if (ml)
console.log(ev.home, "v",
ev.away, ml);
}
await sleep(ms);
}
}
poll(2, 3000); // 2 = tennis
Select the code to copy it.
GET/odds-drop?min_drop=5
as prices falldata: [alerts]
Listen for drops from Node
EventSource is browser-only, but an SSE stream is one HTTP request that stays open. Send an Accept: text/event-stream header with fetch, then read res.body line by line: each data: line is a JSON batch of alerts. A bare object instead of a list is a control message: the connected greeting, or plan_lacks_sse when the key's plan has no stream.
The stream has no sport filter, so check sport as alerts arrive if you only want some sports. min_drop goes down to 1.
Needs push, poll + push or bulk.
WSS/ws/feed?key=<your key>
subscribewithin 5 s
then updatessnapshot
WebSocket notes
The socket expects a subscribe message within five seconds of connecting. The first frame for each stream is a snapshot; every frame after it updates that baseline. Answer each ping with a pong.
Node 22 and later expose a global WebSocket; on 18, the ws package speaks the same protocol. Missed frames are not replayed, so after a disconnect, connect again and start from the new snapshot. You can subscribe by sport or by up to 200 event IDs per stream.
Needs the WebSocket add-on on poll, poll + push or bulk.
NEXTnode node.mjs
Paste, export, run
The free key runs the polling script today. When the drops or the socket earn their place in your code, the pricing page shows which plan adds them.