{"asyncapi":"3.0.0","info":{"title":"ParlayAPI Real-time Odds Stream","version":"1.0.0","description":"Machine-readable description of ParlayAPI's WebSocket and Server-Sent Events surface for real-time odds, props, and source-freshness updates. Generated 2026-05-14. Tier-gated to Business and above for connect; query params unchanged from REST.\n\nClients can generate type-safe SDKs from this spec via `asyncapi generate fromTemplate ./asyncapi.json @asyncapi/python-paho-template` or any of the 30+ AsyncAPI generators.","license":{"name":"Commercial; see https://parlay-api.com/terms"},"contact":{"name":"ParlayAPI support","email":"support@parlay-api.com","url":"https://parlay-api.com/docs"},"tags":[{"name":"WebSocket","description":"Bidirectional WebSocket stream with subscribe/unsubscribe commands. Authenticated via X-API-Key. Pinned to sports via path param."},{"name":"SSE Streaming","description":"Server-Sent Events one-way streams. Same odds data as the WebSocket plus specialty feeds (hot path, odds-drops, live in-play). Simpler reconnect semantics; works over plain HTTP/2."},{"name":"Live & In-Play","description":"Subset of streaming channels focused on currently-live games (in-play state changes, real-time score updates, period transitions)."}]},"servers":{"production-ws":{"host":"parlay-api.com","protocol":"wss","pathname":"/ws","description":"Production WebSocket endpoint. Connect with a Business+ API key. See `channels` for path templates."},"production-sse":{"host":"parlay-api.com","protocol":"https","pathname":"/v1/sse","description":"Production Server-Sent Events endpoint for /v1/sse/odds/{} and /v1/sse/hot/{}. Same data feed as WebSocket, streamed as text/event-stream. Use this when WebSocket upgrade is blocked (corporate proxies, browser EventSource API, HTTP-only environments)."},"production-v1":{"host":"parlay-api.com","protocol":"https","pathname":"/v1","description":"Production base for SSE endpoints that live outside the /v1/sse/ prefix: /v1/odds-drop/{} (price-drop alerts) and /v1/sports/{}/live/sse (in-play stream)."}},"channels":{"oddsStream":{"address":"/odds/{sportKey}","title":"Programmatic odds stream","description":"API-key-authenticated WebSocket. Pushes one envelope per detected change in `odds_snapshots` or `prop_snapshots` for the subscribed sport. Push latency from DB insert to client frame is ~5 ms via Postgres LISTEN/NOTIFY.\n\nThree forms of API-key delivery accepted on the upgrade request: `?apiKey=<key>` query param, `X-API-Key: <key>` header, or `Sec-WebSocket-Protocol: apikey.<key>`.","parameters":{"sportKey":{"description":"Sport key from /v1/sports (e.g. baseball_mlb, basketball_nba, soccer_epl)."}},"messages":{"connectedFrame":{"$ref":"#/components/messages/Connected"},"initialStateFrame":{"$ref":"#/components/messages/InitialState"},"oddsUpdateFrame":{"$ref":"#/components/messages/OddsUpdate"},"heartbeatFrame":{"$ref":"#/components/messages/Heartbeat"},"subscribedFrame":{"$ref":"#/components/messages/Subscribed"},"unsubscribedFrame":{"$ref":"#/components/messages/Unsubscribed"},"subscribeMessage":{"$ref":"#/components/messages/SubscribeCommand"},"unsubscribeMessage":{"$ref":"#/components/messages/UnsubscribeCommand"}},"tags":[{"name":"WebSocket"}]},"sseOddsStream":{"address":"/odds/{sportKey}","title":"Server-Sent Events odds stream","description":"API-key-authenticated SSE. Same data feed as the WebSocket channel above. One-way (server to client). Each message is a single `data:` line carrying the JSON envelope. Heartbeat every 5 seconds by default to prove the stream is live through corporate proxies.\n\nQuery params: apiKey, event_id, bookmakers, markets, kinds, heartbeat_s, limit, since, diff.","servers":[{"$ref":"#/servers/production-sse"}],"parameters":{"sportKey":{"description":"Sport key from /v1/sports."}},"messages":{"connectedFrame":{"$ref":"#/components/messages/Connected"},"hotFeedStatusFrame":{"$ref":"#/components/messages/HotFeedStatus"},"initialStateFrame":{"$ref":"#/components/messages/InitialState"},"oddsUpdateFrame":{"$ref":"#/components/messages/OddsUpdate"},"heartbeatFrame":{"$ref":"#/components/messages/Heartbeat"}},"tags":[{"name":"SSE Streaming"}]},"sseHotStream":{"address":"/hot/{sportKey}","title":"Hot-tier SSE stream","description":"Same data feed as /v1/sse/odds/{sportKey}, sourced from the hot-path collector for the small set of leagues currently in heavy live-betting use (NFL Sunday, NBA prime time, MLB late innings). Cadence target is tighter than the standard /sse/odds stream: 0.5-1.5 s vs 1-3 s. API-key-authenticated, Business+ tier required.\n\nQuery params identical to /sse/odds: apiKey, event_id, bookmakers, markets, kinds, heartbeat_s, limit, since, diff.","servers":[{"$ref":"#/servers/production-sse"}],"parameters":{"sportKey":{"description":"Sport key from /v1/sports. Hot-tier coverage is league-gated; sports outside the hot rotation fall through to the standard /sse/odds cadence."}},"messages":{"connectedFrame":{"$ref":"#/components/messages/Connected"},"hotFeedStatusFrame":{"$ref":"#/components/messages/HotFeedStatus"},"initialStateFrame":{"$ref":"#/components/messages/InitialState"},"oddsUpdateFrame":{"$ref":"#/components/messages/OddsUpdate"},"heartbeatFrame":{"$ref":"#/components/messages/Heartbeat"}},"tags":[{"name":"SSE Streaming"}]},"sseOddsDropStream":{"address":"/odds-drop/{sportKey}","title":"Price-drop alerts SSE stream","description":"Filtered SSE stream that emits only when a tracked price moves by at least `threshold` American-odds cents (default 5). Designed for steam-detection bots and CLV trackers that do not want a full odds feed. Each emitted event carries the before and after prices plus the wall-clock delta.\n\nQuery params: apiKey, threshold (int, default 5), bookmakers (CSV), markets (CSV), heartbeat_s (default 10), since (resume cursor in unix ms).","servers":[{"$ref":"#/servers/production-v1"}],"parameters":{"sportKey":{"description":"Sport key from /v1/sports."}},"messages":{"connectedFrame":{"$ref":"#/components/messages/Connected"},"oddsDropFrame":{"$ref":"#/components/messages/OddsDrop"},"heartbeatFrame":{"$ref":"#/components/messages/Heartbeat"}},"tags":[{"name":"SSE Streaming"}]},"sseSportsLiveStream":{"address":"/sports/{sportKey}/live/sse","title":"In-play live SSE stream","description":"SSE stream of in-play state changes for currently-live games of the subscribed sport: score, period, clock, last play. Emits one frame per state change observed in pbp_events; quiet windows fall through to heartbeats. API-key-authenticated.\n\nQuery params: apiKey, event_id (filter to one game), heartbeat_s.\n\nNote: the `state` field of each frame is a JSON object carrying period / clock / status / raw_periods. Older spec versions encoded this field as a JSON string; current revision emits it as a nested object.","servers":[{"$ref":"#/servers/production-v1"}],"parameters":{"sportKey":{"description":"Sport key from /v1/sports."}},"messages":{"connectedFrame":{"$ref":"#/components/messages/Connected"},"initialStateFrame":{"$ref":"#/components/messages/InitialState"},"liveStateFrame":{"$ref":"#/components/messages/LiveState"},"heartbeatFrame":{"$ref":"#/components/messages/Heartbeat"}},"tags":[{"name":"SSE Streaming"},{"name":"Live & In-Play"}]}},"operations":{"receiveOddsStream":{"action":"receive","channel":{"$ref":"#/channels/oddsStream"},"title":"Subscribe to programmatic odds stream","description":"Receive odds updates after sending the upgrade. Frames arrive in this order: one `connected`, one `initial_state`, then zero or more `odds_update` / `heartbeat` / `subscribed` / `unsubscribed` frames.","messages":[{"$ref":"#/channels/oddsStream/messages/connectedFrame"},{"$ref":"#/channels/oddsStream/messages/initialStateFrame"},{"$ref":"#/channels/oddsStream/messages/oddsUpdateFrame"},{"$ref":"#/channels/oddsStream/messages/heartbeatFrame"},{"$ref":"#/channels/oddsStream/messages/subscribedFrame"},{"$ref":"#/channels/oddsStream/messages/unsubscribedFrame"}],"tags":[{"name":"WebSocket"}]},"sendOddsCommands":{"action":"send","channel":{"$ref":"#/channels/oddsStream"},"title":"Send client commands","description":"Send commands from client to server over the same WebSocket. Currently `subscribe` (filter to one event_id) and `unsubscribe` (drop the filter).","messages":[{"$ref":"#/channels/oddsStream/messages/subscribeMessage"},{"$ref":"#/channels/oddsStream/messages/unsubscribeMessage"}],"tags":[{"name":"WebSocket"}]},"receiveSseStream":{"action":"receive","channel":{"$ref":"#/channels/sseOddsStream"},"title":"Subscribe to SSE odds stream","description":"Receive odds updates as Server-Sent Events. Same data, different transport.","tags":[{"name":"SSE Streaming"}]},"receiveSseHotStream":{"action":"receive","channel":{"$ref":"#/channels/sseHotStream"},"title":"Subscribe to hot-tier SSE stream","description":"Receive odds updates as SSE from the hot-path collector. Used by latency-sensitive consumers during live-betting windows on the small set of leagues in the hot rotation.","tags":[{"name":"SSE Streaming"}]},"receiveSseOddsDropStream":{"action":"receive","channel":{"$ref":"#/channels/sseOddsDropStream"},"title":"Subscribe to price-drop alerts","description":"Receive only `odds_drop` frames where the change exceeds the configured threshold. Heavy filter on the server side; bandwidth is a tiny fraction of the full /sse/odds stream.","tags":[{"name":"SSE Streaming"}]},"receiveSseSportsLiveStream":{"action":"receive","channel":{"$ref":"#/channels/sseSportsLiveStream"},"title":"Subscribe to in-play live state stream","description":"Receive per-game state changes (score, period, clock, last play) as SSE. Pairs with /v1/sports/{key}/live for current snapshot.","tags":[{"name":"SSE Streaming"},{"name":"Live & In-Play"}]}},"components":{"messages":{"Connected":{"name":"connected","title":"Initial handshake frame","summary":"Sent once immediately after upgrade. Confirms auth, tier, push mode, and any active query-param filters.","payload":{"$ref":"#/components/schemas/ConnectedPayload"}},"HotFeedStatus":{"name":"hot_feed_status","title":"Source freshness snapshot (SSE only)","summary":"Sent once after `connected`, before `initial_state`. Carries the per-source freshness map at connect time so clients can decide whether to trust the immediate snapshot.","payload":{"$ref":"#/components/schemas/HotFeedStatusPayload"}},"InitialState":{"name":"initial_state","title":"Snapshot of current odds at connect time","summary":"Sent once after `connected`. Carries every active row for the subscribed sport, filtered by any active query params. Honors the `since` resume cursor when within the 60-minute lookback window; falls back to full snapshot with `truncated: true` otherwise.","payload":{"$ref":"#/components/schemas/InitialStatePayload"}},"OddsUpdate":{"name":"odds_update","title":"Change broadcast","summary":"Sent every time a row in `odds_snapshots` or `prop_snapshots` for this sport changes. Payload shape varies by `diff_mode`: when off, every field of the affected row is included; when on, only the changed fields plus identifier fields are included.","payload":{"$ref":"#/components/schemas/OddsUpdatePayload"}},"Heartbeat":{"name":"heartbeat","title":"Liveness ping","summary":"Sent on idle channel timeout (default 30 s for WS, configurable 1-30 s for SSE via `heartbeat_s`). Carries server timestamp; clients should treat absence of any frame for 2x heartbeat as a disconnect signal.","payload":{"$ref":"#/components/schemas/HeartbeatPayload"}},"Subscribed":{"name":"subscribed","title":"Filter applied","summary":"Server acknowledgement of a successful `subscribe` command. Subsequent `odds_update` frames are filtered to the requested event_id only.","payload":{"$ref":"#/components/schemas/SubscribedPayload"}},"Unsubscribed":{"name":"unsubscribed","title":"Filter cleared","summary":"Server acknowledgement of a successful `unsubscribe` command. Subsequent `odds_update` frames are unfiltered again.","payload":{"$ref":"#/components/schemas/UnsubscribedPayload"}},"SubscribeCommand":{"name":"subscribe","title":"Filter to one game","summary":"Client-to-server command. Restricts `odds_update` frames to a single event_id.","payload":{"$ref":"#/components/schemas/SubscribeCommandPayload"}},"UnsubscribeCommand":{"name":"unsubscribe","title":"Clear filter","summary":"Client-to-server command. Reverts to all-events broadcast.","payload":{"$ref":"#/components/schemas/UnsubscribeCommandPayload"}},"OddsDrop":{"name":"odds_drop","title":"Tracked price-drop event","summary":"Emitted only on the /v1/odds-drop/{sport_key} channel. Fires when a tracked price moves by at least the configured threshold (default 5 American-odds cents). Payload carries both the prior and the new price.","payload":{"$ref":"#/components/schemas/OddsDropPayload"}},"LiveState":{"name":"live_state","title":"In-play state change","summary":"Emitted on the /v1/sports/{sport_key}/live/sse channel. Fires once per observed state change in pbp_events. Carries score, period, clock, and the last play.","payload":{"$ref":"#/components/schemas/LiveStatePayload"}}},"schemas":{"ConnectedPayload":{"type":"object","required":["type","sport_key","timestamp"],"properties":{"type":{"type":"string","const":"connected"},"sport_key":{"type":"string"},"tier":{"type":"string","enum":["business","enterprise","scale"]},"min_push_interval_s":{"type":"number","description":"Per-connection coalesce window. 0.0 = no coalesce (Scale tier). 0.5 = Enterprise. 1.0 = Business and other paying."},"push_mode":{"type":"string","enum":["raw","coalesced"]},"diff_mode":{"type":"boolean","description":"True when ?diff=true was set; subsequent odds_update frames carry only changed fields."},"filters":{"$ref":"#/components/schemas/ConnectFilters"},"timestamp":{"type":"integer","description":"Server unix timestamp at connect time."},"note":{"type":"string"}}},"ConnectFilters":{"type":"object","properties":{"event_id":{"type":["string","null"]},"bookmakers":{"type":["array","null"],"items":{"type":"string"}},"markets":{"type":["array","null"],"items":{"type":"string"}},"kinds":{"type":["array","null"],"items":{"type":"string","enum":["game","prop"]}},"since":{"type":["integer","null"],"description":"Resume cursor in unix ms. Initial state will only include rows with timestamp_ms > since."}}},"HotFeedStatusPayload":{"type":"object","required":["type","sport_key","sources","timestamp"],"properties":{"type":{"type":"string","const":"hot_feed_status"},"sport_key":{"type":"string"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/SourceFreshness"}},"timestamp":{"type":"integer"}}},"SourceFreshness":{"type":"object","required":["bookmaker"],"properties":{"bookmaker":{"type":"string"},"latest_ms":{"type":["integer","null"]},"age_s":{"type":["number","null"]},"rows_60s":{"type":"integer"}}},"InitialStatePayload":{"type":"object","required":["type","sport_key","timestamp","count","data"],"properties":{"type":{"type":"string","const":"initial_state"},"sport_key":{"type":"string"},"timestamp":{"type":"integer"},"count":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/OddsRow"}},"since":{"type":"integer","description":"Effective resume cursor used by the server."},"truncated":{"type":"boolean","description":"True when the requested `since` cursor was older than the 60-minute lookback window; client should reconcile from this full snapshot."}}},"OddsUpdatePayload":{"type":"object","required":["type","sport_key","timestamp","count","data"],"properties":{"type":{"type":"string","const":"odds_update"},"sport_key":{"type":"string"},"timestamp":{"type":"integer"},"count":{"type":"integer"},"data":{"type":"array","items":{"$ref":"#/components/schemas/OddsRow"}},"coalesced":{"type":"boolean","description":"True when this envelope contains multiple ticks merged by the connection's min_push_interval throttle window."}}},"OddsRow":{"type":"object","description":"Normalized row shape carrying either a game-level moneyline/spread/total OR a per-player prop. The `kind` discriminator switches between the two shapes. In diff mode, only changed fields plus the identifier fields are present.","properties":{"kind":{"type":"string","enum":["game","prop"]},"bookmaker":{"type":"string"},"event_id":{"type":"string"},"home_team":{"type":"string"},"away_team":{"type":"string"},"commence_time":{"type":"string","description":"ISO-8601 UTC."},"last_update":{"type":"integer","description":"Unix ms at which this row was written to the database."},"home_ml":{"type":["integer","null"],"description":"American odds when kind=game."},"away_ml":{"type":["integer","null"]},"home_spread":{"type":["number","null"]},"away_spread":{"type":["number","null"]},"total":{"type":["number","null"]},"player":{"type":["string","null"],"description":"Player name when kind=prop."},"market_key":{"type":["string","null"],"description":"Prop market key when kind=prop, e.g. player_points, player_rebounds."},"market":{"type":["string","null"],"description":"Human-readable market label when kind=prop."},"line":{"type":["number","null"],"description":"Prop line value when kind=prop."},"over_price":{"type":["integer","null"]},"under_price":{"type":["integer","null"]},"_diff":{"type":"boolean","description":"Set to true when this row is a diff (only changed fields present)."}}},"HeartbeatPayload":{"type":"object","required":["type","timestamp"],"properties":{"type":{"type":"string","const":"heartbeat"},"sport_key":{"type":"string"},"timestamp":{"type":"integer"},"connections":{"type":"integer","description":"Total active connections across all sports on this worker. Operational signal, not customer-facing."}}},"SubscribedPayload":{"type":"object","required":["type","event_id","timestamp"],"properties":{"type":{"type":"string","const":"subscribed"},"event_id":{"type":"string"},"timestamp":{"type":"integer"}}},"UnsubscribedPayload":{"type":"object","required":["type","timestamp"],"properties":{"type":{"type":"string","const":"unsubscribed"},"timestamp":{"type":"integer"}}},"SubscribeCommandPayload":{"type":"object","required":["type","event_id"],"properties":{"type":{"type":"string","const":"subscribe"},"event_id":{"type":"string"}}},"UnsubscribeCommandPayload":{"type":"object","required":["type"],"properties":{"type":{"type":"string","const":"unsubscribe"}}},"OddsDropPayload":{"type":"object","required":["type","sport_key","timestamp","bookmaker","event_id"],"properties":{"type":{"type":"string","const":"odds_drop"},"sport_key":{"type":"string"},"timestamp":{"type":"integer","description":"Server unix ms when this drop was observed."},"bookmaker":{"type":"string"},"event_id":{"type":"string"},"home_team":{"type":"string"},"away_team":{"type":"string"},"market_key":{"type":["string","null"]},"market":{"type":["string","null"]},"player":{"type":["string","null"]},"line":{"type":["number","null"]},"side":{"type":"string","enum":["over","under","home","away"],"description":"Which side of the market moved."},"prior_price":{"type":"integer","description":"American odds before the move."},"new_price":{"type":"integer","description":"American odds after the move."},"delta_cents":{"type":"integer","description":"Absolute change in American-odds cents. Always >= threshold."},"elapsed_ms":{"type":"integer","description":"Wall-clock ms between prior_price observation and new_price observation."}}},"LiveStatePayload":{"type":"object","required":["type","sport_key","timestamp","match_id"],"properties":{"type":{"type":"string","const":"live_state"},"sport_key":{"type":"string"},"timestamp":{"type":"integer","description":"Server unix ms at emit."},"match_id":{"type":"string"},"home_team":{"type":"string"},"away_team":{"type":"string"},"score_a":{"type":["integer","null"],"description":"Home score (or first-listed-team for sports without a home/away convention)."},"score_b":{"type":["integer","null"]},"state":{"type":"object","description":"Nested object carrying period, clock, status, and any raw_periods provenance. Older spec versions encoded this as a JSON string; current revision emits it as a real object.","properties":{"period":{"type":["string","integer","null"]},"clock":{"type":["string","null"]},"status":{"type":["string","null"],"description":"e.g. pending, in_progress, halftime, final."},"league_id":{"type":["integer","null"]},"raw_periods":{"type":["array","null"],"items":{"type":"object"}}}},"last_play":{"type":["string","null"],"description":"Human-readable last play description, when available."}}}},"securitySchemes":{"apiKeyQuery":{"type":"apiKey","in":"query","name":"apiKey","description":"?apiKey=<key> on the WebSocket upgrade or SSE request URL."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-API-Key","description":"X-API-Key: <key> request header."},"apiKeySubprotocol":{"type":"userPassword","description":"Sec-WebSocket-Protocol: apikey.<key>. Useful for browsers and EventSource-style clients that cannot set arbitrary headers on the upgrade."}}},"x-parlay-close-codes":{"description":"Server-initiated close codes specific to ParlayAPI.","codes":{"1008":"Invalid API key.","4001":"Authentication required (session not present or expired).","4002":"Concurrent connection cap reached for this tier. Close idle connections or upgrade.","4003":"Missing API key. Pass via ?apiKey=, X-API-Key header, or Sec-WebSocket-Protocol: apikey.<key>."}},"x-parlay-tier-gates":{"description":"WebSocket and SSE require Business tier or above. Free, Starter, and Pro tiers connect but immediately close with 4001.","minimum_tier":"business"}}