Streaming (Pro)
Instead of polling /v2/odds, open one WebSocket and
receive every price change as it happens. Streaming is available on the Pro plan.
- Endpoint:
wss://api.pingodds.com/v2/stream - Each
subscribecounts as one request against your monthly allowance; the price updates themselves are free. - At most 5 open connections per API key.
1. Connect and authenticate
No header is needed to open the socket, so browsers can connect directly. Within 5 seconds, send your API key as the first message:
{"action":"auth","key":"po_live_…"} The server answers:
{"type":"ready","plan":"pro","connection_id":"c_5152b34cb825b586"} On a Free or Starter key it sends {"type":"error","code":"plan_required","message":"…","upgrade_url":"…"} and closes with 4403.
2. Subscribe
{"action":"subscribe","id":"s1","snapshot":true,
"filters":[{"league":"americanfootball_nfl","books":["pinnacle","fanduel"],"markets":["spreads"]}]} Each filter needs a league; books and markets are optional (omitted means all). Keys are
the ones /v2/leagues, /v2/books and /v2/markets list; an unknown key gets an invalid_filter error and no subscription. With "snapshot": true you first get the current board, in
the same shape as /v2/odds returns in events, then the acknowledgement:
{"type":"snapshot","id":"s1","events":[
{"event":{"id":10,"league":"americanfootball_nfl","commence_time":"2026-09-27T17:00:00Z",
"home":{"id":16,"name":"Cleveland Browns"},"away":{"id":17,"name":"Carolina Panthers"},
"status":"scheduled"},
"markets":[{"key":"spreads","books":[{"key":"fanduel","last_update":"2026-09-24T16:22:29.867Z",
"last_checked":"2026-09-25T16:15:41.742Z","outcomes":[
{"name":"home","line":2.5,"price_dec":1.9524,"price_am":-105,"implied":0.51219,"no_vig":0.489168,"suspended":false},
{"name":"away","line":-2.5,"price_dec":1.8696,"price_am":-115,"implied":0.534874,"no_vig":0.510832,"suspended":false}]}]}]}
]}
{"type":"subscribed","id":"s1"} Re-using an id replaces that subscription. {"action":"unsubscribe","id":"s1"} answers {"type":"unsubscribed","id":"s1"}, and {"action":"ping"} answers {"type":"pong"}.
3. Price updates
One message per outcome whose price or line changed:
{"type":"odds","event_id":88213,"league":"americanfootball_nfl","book":"pinnacle",
"market":"spreads","outcome":"home","line":-3.5,"price_dec":1.909,"price_am":-110,
"suspended":false,"ts":"2026-09-25T17:00:01Z"} outcome is home/away for moneyline and spread, home/draw/away for a three-way moneyline (soccer), over/under for totals. Snapshots list outcomes in that order, home first. line is null for moneylines. ts is when PingOdds saw the change.
4. Errors and close codes
Errors that keep the connection open arrive as {"type":"error","id":"s1","code":"…","message":"…"} (id echoes the subscription when there is one): invalid_filter, unknown_subscription, bad_request, rate_limited, monthly_limit_reached, internal_error. The server closes the socket with:
| Code | Reason | What to do |
|---|---|---|
| 4401 | unauthenticated | No valid auth within 5 s, or the key is invalid or revoked. Fix the key. |
| 4403 | plan_required | The account is not on Pro. Don't reconnect; upgrade. |
| 4429 | too_many_connections | This key already has 5 open streams. Close one, or reconnect later. |
| 4008 | slow_consumer | You fell 1,000 messages behind. Read faster or subscribe to less, then reconnect. |
| 1011 | internal_error | Server-side failure. Reconnect with backoff. |
For anything but 4401 and 4403, reconnect with exponential backoff (1 s, 2 s, 4 s … capped at
30 s) and subscribe again with "snapshot": true so
you don't miss changes made while you were away.
5. Example client
// Node 22+ has a global WebSocket; in the browser the same code works as is.
const ws = new WebSocket("wss://api.pingodds.com/v2/stream");
ws.onopen = () => ws.send(JSON.stringify({ action: "auth", key: process.env.PINGODDS_API_KEY }));
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
switch (msg.type) {
case "ready":
ws.send(JSON.stringify({
action: "subscribe", id: "nfl", snapshot: true,
filters: [{ league: "americanfootball_nfl", markets: ["spreads", "totals"] }],
}));
break;
case "snapshot": console.log("snapshot:", msg.events.length, "events"); break;
case "odds": console.log(msg.book, msg.event_id, msg.outcome, msg.line, msg.price_am); break;
case "error": console.error(msg.code, msg.message); break;
}
};
ws.onclose = (e) => console.log("closed", e.code, e.reason); // reconnect with backoff unless 4403
setInterval(() => ws.readyState === 1 && ws.send('{"action":"ping"}'), 25_000);Try it
Connects from this page straight to wss://api.pingodds.com/v2/stream.
Your key stays in your browser.
- Messages appear here, newest first.