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.
| Situation | Pick |
|---|---|
| Long-lived backend worker, full control of the network stack | WebSocket |
| Behind a corporate proxy that drops Upgrade | SSE |
| Browser front-end, want zero-dep, willing to give up client->server messages | SSE (uses native EventSource) |
| Serverless function (Lambda, Cloud Run) for a 30–90 s scan | SSE |
| Need to send subscribe/unsubscribe frames after connect | WebSocket (SSE is server-to-client only — filter via query param at connect) |
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.
| Form | Where |
|---|---|
| Query param | ?apiKey=YOUR_KEY on the URL (browser EventSource can't set headers) |
| X-API-Key header | Server-side clients (curl, Python httpx, Node eventsource) |
| Authorization Bearer | Authorization: Bearer YOUR_KEY — accepted alias |
| Param | Type | Description |
|---|---|---|
event_id | string | Filter to a single event for the life of the stream. SSE has no subscribe message, so the filter is fixed at connect time. |
bookmakers | csv | Only push rows from these books. pinnacle,novig,fanduel. |
markets | csv | Only push rows for these markets. h2h,player_points. |
kinds | csv | Filter by kind: game,props,live. |
heartbeat_s | int 1..30 | Heartbeat cadence. Default 5 s. Match this to your proxy's keep-alive minimum. |
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":[...]}
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);
// 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);
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 --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.
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.
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.
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.
event_id at connect time as a query param. To change the filter, close and reconnect with the new value.event: + data: prefix (~30 bytes); negligible for our payload sizes (~500–5000 bytes).