{"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>`.\n\nQuery params: apiKey, event_id, bookmakers, markets, kinds, limit, since, diff, max_age_s. `limit` is the initial_state row budget: 1 to 1000, default 500, the same name, bounds and default the SSE channel takes. The socket validates it itself: any value that is not an integer in range, `limit=abc` included, is accepted and then closed 4005 carrying the same sentence the SSE endpoint's 422 gives (for example `limit: Input should be less than or equal to 1000`). A WebSocket has no status code to answer with, and letting the framework reject it would close 1008, which on this channel means an invalid API key.\n\nThe initial_state budget is a fair allocation among the books the fetch returned rather than a raw-recency fill. It is an allocation and not a coverage guarantee: a book can still be thin or absent from the frame. The limit carries into the re-attach, so a connection served a `partial` frame and re-attached later is filled at the same budget. See `books` and `truncated_books` on InitialStatePayload.","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"},"streamStatusFrame":{"$ref":"#/components/messages/StreamStatus"}},"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, max_age_s. `limit` is the initial_state row budget: 1 to 1000, default 500, identical to the WebSocket channel's, and the initial_state fill is the same fair allocation among the books the fetch returned.","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"},"streamStatusFrame":{"$ref":"#/components/messages/StreamStatus"}},"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"},"streamStatusFrame":{"$ref":"#/components/messages/StreamStatus"}},"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"},"streamStatusFrame":{"$ref":"#/components/messages/StreamStatus"}},"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. A stream_status frame can disclose incomplete Pinnacle coverage separately from odds.","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"},{"$ref":"#/channels/oddsStream/messages/streamStatusFrame"}],"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. Coverage diagnostics arrive separately as stream_status frames.","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"}},"StreamStatus":{"name":"stream_status","title":"Stream coverage diagnostic","summary":"Reports unresolved Pinnacle fixture families once per subscription generation. It contains no odds. Valid families continue; a fresh current snapshot is required to clear the incomplete-coverage warning.","payload":{"$ref":"#/components/schemas/StreamStatusPayload"}}},"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","description":"Best-effort source freshness telemetry sent after the SSE initial snapshot. An unavailable status does not close the odds stream and an empty sources array in that case is not a coverage statement.","required":["type","sport_key","sources","timestamp"],"properties":{"status":{"type":"string","enum":["ok","unavailable"],"description":"Whether freshness telemetry was obtained within its independent time budget."},"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 limit, omitted records, or source-time resume fallback prevents a complete snapshot. Pinnacle uses a fresh current snapshot for source-time resume."},"snapshot_complete":{"type":"boolean","description":"True only for a complete supported current snapshot within the requested scope."},"partial":{"type":"boolean","description":"True when a book the subscription asked for could not be included and the stream was served WITHOUT it rather than refused. The socket stays open, `missing_books` names what is absent, and a later `initial_state` without `partial` is sent when the missing book is attached. Before 2026-09-08 this condition closed the stream (WebSocket 1013, SSE `stream_closed reason=current_state_unavailable`) instead."},"partial_reason":{"type":"string","enum":["pinnacle_current_unavailable"],"description":"Why the snapshot is partial. `pinnacle_current_unavailable` means Pinnacle's current-snapshot read did not complete inside the server's budget; every other requested book is present and unaffected."},"missing_books":{"type":"array","items":{"type":"string"},"description":"Requested bookmaker keys omitted from this frame. Rows for these books are NOT served from a fallback source: a stale price presented as a live one is worse than an absent one."},"partial_note":{"type":"string","description":"Human-readable explanation of the partial snapshot."},"pinnacle_resume":{"type":"string","enum":["current_snapshot","retrying"],"description":"`current_snapshot` when Pinnacle rows in this frame are the committed current snapshot. `retrying` when Pinnacle is absent and the server is still trying to attach it."},"snapshot_omitted_unresolved_families":{"type":"integer","minimum":1,"description":"Unresolved fixture family references encountered in the selected snapshot pages. Those families are not emitted."},"coverage_complete":{"type":"boolean"},"coverage_reason":{"type":"string","enum":["fixture_unresolved","pinnacle_current_unavailable","fixture_schedule_ambiguous"]},"fresh_snapshot_required_to_clear":{"type":"boolean"},"books":{"type":"array","items":{"type":"string"},"description":"Every bookmaker key represented in `data`, sorted. This is the universe the frame carries and the list a client should build its book universe from; a book this build served no rows for is absent from it. Before 2026-09-08 the snapshot was filled by raw recency, so one fast book could hold all `count` rows and this list would have been one book long while nine other requested books were streaming."},"snapshot_fill":{"type":"string","enum":["per_book"],"description":"How the row budget was allocated. `per_book` means books drew rows in turn until `snapshot_limit` was spent, game lines before props within each book and fixture recency within each kind. It is a fair allocation among the books the fetch returned, not a coverage guarantee: a book can still be thin or absent from the frame. The merged selector, the native current page and stream lifecycle behaviour are unchanged by it."},"truncated_books":{"type":"object","additionalProperties":{"type":"object","properties":{"served":{"type":"integer","description":"Rows for this book in `data`."},"omitted":{"type":"integer","description":"Rows for this book the budget did not reach."}}},"description":"Present only when the budget cut somebody. Keyed by bookmaker, with the counts served and omitted for that book, measured against what this snapshot held after every filter the frame applies; the counts describe this build only. `served` can be 0, which means this snapshot held rows for that book and the budget reached none of them, and such a book is absent from `books`. Raise `limit` (up to 1000) or narrow `bookmakers` / `markets` / `kinds` to see more. There is no guarantee of eventual delivery of the omitted rows: an `odds_update` frame is sent when a row changes, so a line that does not move may never be pushed on this connection."}}},"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)."},"available":{"type":"boolean"},"action":{"type":"string","enum":["upsert","remove"]},"removal_reason":{"type":"string","description":"fixture_unresolved is a local retirement of an earlier delivered selection. Clear its price; the original source clock and provider status are retained. Other local/provider removal reasons may also occur."}}},"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."}}},"StreamStatusPayload":{"type":"object","required":["type","sport_key","bookmaker","status","reason","dataset","affected_families_in_page","coverage_complete","fresh_snapshot_required_to_clear"],"properties":{"type":{"const":"stream_status"},"sport_key":{"type":"string"},"bookmaker":{"const":"pinnacle"},"status":{"const":"degraded"},"reason":{"const":"fixture_unresolved"},"dataset":{"enum":["games","periods"]},"affected_families_in_page":{"type":"integer","minimum":1,"description":"Affected families in the reported committed page, not a total board count."},"coverage_complete":{"const":false},"fresh_snapshot_required_to_clear":{"const":true}}}},"securitySchemes":{"apiKeyQuery":{"type":"apiKey","in":"query","name":"apiKey","description":"?apiKey=<key> on the WebSocket upgrade or SSE request URL."},"apiKeyQuerySnakeCase":{"type":"apiKey","in":"query","name":"api_key","description":"?api_key=<key>. The same credential as ?apiKey=; both spellings are accepted on every WebSocket and SSE route."},"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":"Tier gate (key below Business), or an expired dashboard session.","4002":"Concurrent connection cap reached for this tier. Close idle connections or upgrade.","4003":"Missing API key. Pass via ?apiKey=, ?api_key=, X-API-Key header, or Sec-WebSocket-Protocol: apikey.<key>. Identical on every WebSocket route, live and odds alike.","4004":"Monthly credit allowance spent.","4005":"Either a query parameter that cannot be served (`bookmakers` naming only suppressed books, or a `limit` that is not an integer in 1 to 1000; the reason carries the parameter name and the same sentence the SSE endpoint's 422 gives for the same value), OR a past-due subscription: suspended, or over the soft cap. The two are told apart by the reason text, and only the billing one is fixed by paying an invoice."}},"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"}}