Server-Sent Events (SSE)

The same odds feed that flows over WebSocket, delivered as plain HTTP text/event-stream. Use SSE when a long-lived WebSocket isn't an option: corporate proxies that don't pass Upgrade, browser-only with built-in EventSource, or short-window serverless scans.

WS reference Quickstart Examples Player props Edge alerts SSE Troubleshooting

When to pick SSE over WebSocket

SituationPick
Long-lived backend worker, full control of the network stackWebSocket
Behind a corporate proxy that drops UpgradeSSE
Browser front-end, want zero-dep, willing to give up client->server messagesSSE (uses native EventSource)
Serverless function (Lambda, Cloud Run) for a 30–90 s scanSSE
Need to send subscribe/unsubscribe frames after connectWebSocket (SSE is server-to-client only — filter via query param at connect)

Endpoints

GET /v1/sse/odds/{sport_key}?apiKey=YOUR_KEY
GET /v1/sse/hot/{sport_key}?apiKey=YOUR_KEY     # same feed, hot-path branding for marketing
GET /v1/odds-drop/{sport_key}?apiKey=YOUR_KEY   # alt URL for compatibility
Accept: text/event-stream

Same tier gate as WebSocket: Business / Enterprise / Scale only. Same concurrent connection cap counts both WS and SSE together.

Auth (same three forms as WebSocket)

FormWhere
Query param?apiKey=YOUR_KEY on the URL (browser EventSource can't set headers)
X-API-Key headerServer-side clients (curl, Python httpx, Node eventsource)
Authorization BearerAuthorization: Bearer YOUR_KEY — accepted alias

Optional query params

ParamTypeDescription
event_idstringFilter to a single event for the life of the stream. SSE has no subscribe message, so the filter is fixed at connect time.
bookmakerscsvOnly push rows from these books. pinnacle,novig,fanduel.
marketscsvOnly push rows for these markets. h2h,player_points.
kindscsvFilter by kind: game,props,live.
heartbeat_sint 1..30Heartbeat cadence. Default 5 s. Match this to your proxy's keep-alive minimum.

Frame format

Each frame is a single JSON object on a single line, sent as the data: field of an SSE event. The contents match the WebSocket protocol verbatim — same type discriminator, same row shape.

: heartbeat at 1715587200          ← SSE comment line, ignored by parsers

event: connected
data: {"type":"connected","sport_key":"basketball_nba","tier":"scale",
       "min_push_interval_s":0.0,"push_mode":"raw","transport":"sse",
       "timestamp":1715587200}

event: initial_state
data: {"type":"initial_state","sport_key":"basketball_nba","count":487,
       "timestamp":1715587200,"data":[...]}

event: odds_update
data: {"type":"odds_update","sport_key":"basketball_nba","count":4,
       "timestamp":1715587235,"data":[...]}

Browser EventSource

const KEY = "pk_live_xxxx";     // ⚠️ proxy through your backend, don't ship the key
const es  = new EventSource(`/v1/sse/odds/basketball_nba?apiKey=${KEY}`);

es.addEventListener("connected", (ev) => {
  const f = JSON.parse(ev.data);
  console.log("connected:", f.tier, f.push_mode);
});
es.addEventListener("initial_state", (ev) => {
  const f = JSON.parse(ev.data);
  console.log("snapshot:", f.count, "rows");
});
es.addEventListener("odds_update", (ev) => {
  const f = JSON.parse(ev.data);
  console.log("update:", f.count, "rows");
});
es.onerror = (e) => console.error("sse error", e);

Node — eventsource library

// npm install eventsource
import EventSource from "eventsource";

const KEY = process.env.PARLAY_API_KEY;
const URL = `https://parlay-api.com/v1/sse/odds/basketball_nba`;
const es  = new EventSource(URL, { headers: { "X-API-Key": KEY } });

es.addEventListener("odds_update", (ev) => {
  const f = JSON.parse(ev.data);
  for (const row of f.data) {
    console.log(row.bookmaker, row.outcome, row.price_american);
  }
});
es.onerror = (e) => console.error("error", e);

Python — httpx streaming

import httpx, json, os

KEY = os.environ["PARLAY_API_KEY"]
URL = "https://parlay-api.com/v1/sse/odds/basketball_nba"

with httpx.stream("GET", URL, headers={"X-API-Key": KEY, "Accept": "text/event-stream"}, timeout=None) as r:
    event_type = None
    for line in r.iter_lines():
        if line.startswith("event: "):
            event_type = line[7:].strip()
        elif line.startswith("data: "):
            frame = json.loads(line[6:])
            if event_type == "odds_update":
                print(f"{frame['count']} updates")
        elif line == "":
            event_type = None

curl smoke test

curl --no-buffer \
  --header "X-API-Key: $PARLAY_API_KEY" \
  --header "Accept: text/event-stream" \
  "https://parlay-api.com/v1/sse/odds/basketball_nba"

You should see the heartbeat comment, then the connected event, then initial_state, then odds_update as prices change. Ctrl-C to stop.

Heartbeats

SSE intermediaries (proxies, CDNs, load balancers) tend to drop "silent" connections more aggressively than WebSocket. We send an SSE-comment heartbeat every heartbeat_s seconds (default 5) to keep the path alive:

: heartbeat 1715587205

Comments start with : and are ignored by all spec-compliant SSE parsers — you don't have to handle them in code. They're purely there so your proxy / CDN sees activity on the socket.

Reconnect

Browser EventSource reconnects automatically with a 3 s delay by default. Server-side clients should implement the same: on disconnect, wait 1–3 s, reopen the GET.

The Last-Event-ID header is part of the SSE spec but we don't currently use it for replay — on reconnect you'll get a fresh initial_state with the current snapshot. Treat that as authoritative and discard any local cache from the previous session.

Tier coalescing applies to SSE too

Same as WebSocket: Business 1 s coalesce, Enterprise 0.5 s, Scale 0 s (raw). The push_mode field on the connected envelope tells you which one your key gets. See the WebSocket reference for the full coalesce behavior — SSE inherits it.

Limitations vs WebSocket