# Get event deltas (V1 format) Source: https://docs.therundown.io/api-reference/generated/v1-delta/get-event-deltas-v1-format get /api/v1/delta # Get event deltas (V2 format via V1 path) Source: https://docs.therundown.io/api-reference/generated/v1-delta/get-event-deltas-v2-format-via-v1-path get /api/v1/deltaV2 # Get a single event with lines Source: https://docs.therundown.io/api-reference/generated/v1-events/get-a-single-event-with-lines get /api/v1/events/{eventId} # Get closing lines for a sport and date Source: https://docs.therundown.io/api-reference/generated/v1-events/get-closing-lines-for-a-sport-and-date get /api/v1/sports/{sportID}/closing/{date} # Get event by line ID Source: https://docs.therundown.io/api-reference/generated/v1-events/get-event-by-line-id get /api/v1/events/{eventId}/lines/{lineId} # Get events with lines for a sport and date Source: https://docs.therundown.io/api-reference/generated/v1-events/get-events-with-lines-for-a-sport-and-date get /api/v1/sports/{sportID}/events/{date} # Get opening lines for a sport and date Source: https://docs.therundown.io/api-reference/generated/v1-events/get-opening-lines-for-a-sport-and-date get /api/v1/sports/{sportID}/openers/{date} # Get best lines for multiple events Source: https://docs.therundown.io/api-reference/generated/v1-lines/get-best-lines-for-multiple-events get /api/v1/lines/best # Get moneylines for an event Source: https://docs.therundown.io/api-reference/generated/v1-lines/get-moneylines-for-an-event get /api/v1/lines/{id}/moneyline # Get spreads for an event Source: https://docs.therundown.io/api-reference/generated/v1-lines/get-spreads-for-an-event get /api/v1/lines/{id}/spread # Get team totals for an event Source: https://docs.therundown.io/api-reference/generated/v1-lines/get-team-totals-for-an-event get /api/v1/lines/{id}/team_total # Get totals (over/under) for an event Source: https://docs.therundown.io/api-reference/generated/v1-lines/get-totals-overunder-for-an-event get /api/v1/lines/{id}/total # List all sportsbooks (alias) Source: https://docs.therundown.io/api-reference/generated/v1-reference/list-all-sportsbooks-alias get /api/v1/sportsbooks # List all sportsbooks/affiliates Source: https://docs.therundown.io/api-reference/generated/v1-reference/list-all-sportsbooksaffiliates get /api/v1/affiliates # List season types per sport Source: https://docs.therundown.io/api-reference/generated/v1-reference/list-season-types-per-sport get /api/v1/season_types # Get available dates for a sport Source: https://docs.therundown.io/api-reference/generated/v1-sports/get-available-dates-for-a-sport get /api/v1/sports/{sportID}/dates # Get conferences for a sport Source: https://docs.therundown.io/api-reference/generated/v1-sports/get-conferences-for-a-sport get /api/v1/sports/{sportID}/conferences # Get divisions for a sport Source: https://docs.therundown.io/api-reference/generated/v1-sports/get-divisions-for-a-sport get /api/v1/sports/{sportID}/divisions # Get event schedule for a sport Source: https://docs.therundown.io/api-reference/generated/v1-sports/get-event-schedule-for-a-sport get /api/v1/sports/{sportID}/schedule # Get teams for a sport Source: https://docs.therundown.io/api-reference/generated/v1-sports/get-teams-for-a-sport get /api/v1/sports/{sportID}/teams # List all sports Source: https://docs.therundown.io/api-reference/generated/v1-sports/list-all-sports get /api/v1/sports # V1 WebSocket — stream real-time event and market updates Source: https://docs.therundown.io/api-reference/generated/v1-websocket/v1-websocket-—-stream-real-time-event-and-market-updates get /api/v1/ws Establishes a WebSocket connection for streaming V1 event updates and V2 market price deltas. **Connection**: `wss://therundown.io/api/v1/ws?key=YOUR_API_KEY` **Filter parameters** (query string): - `affiliate_ids` — comma-separated affiliate IDs - `sport_ids` — comma-separated sport IDs (V1 messages only) - `event_ids` — comma-separated event IDs - `date` — date filter in YYYY-MM-DD format (V1 messages only) **Heartbeat**: Every 15 seconds. **Message queue**: 1024-message buffer. Messages dropped if client falls behind. # Get a single event with markets Source: https://docs.therundown.io/api-reference/generated/v2-events/get-a-single-event-with-markets get /api/v2/events/{eventID} # Get best available line for an event Source: https://docs.therundown.io/api-reference/generated/v2-events/get-best-available-line-for-an-event get /api/v2/events/{eventID}/best-line Returns the best moneyline, spread, and total across all sportsbooks. # Get event changes since a delta ID Source: https://docs.therundown.io/api-reference/generated/v2-events/get-event-changes-since-a-delta-id get /api/v2/delta Returns raw JSON delta payloads for events that changed since `last_id`. Each delta entry is a raw JSON string containing the full event update — the `data` field is a serialized JSON string, not a parsed object. The response is wrapped in a `meta` object containing the `delta_last_id` cursor for subsequent polling. # Get events with markets for a sport and date Source: https://docs.therundown.io/api-reference/generated/v2-events/get-events-with-markets-for-a-sport-and-date get /api/v2/sports/{sportID}/events/{date} Returns events with full market/odds data for the specified sport and date. This is the primary endpoint for building odds screens. The response includes `meta.delta_last_id` which serves as the bootstrap cursor for the `/api/v2/markets/delta` polling endpoint. # Get opening lines for a sport and date Source: https://docs.therundown.io/api-reference/generated/v2-events/get-opening-lines-for-a-sport-and-date get /api/v2/sports/{sportID}/openers/{date} Returns events with the earliest recorded market prices (opening lines). # Get opening prices for an event Source: https://docs.therundown.io/api-reference/generated/v2-events/get-opening-prices-for-an-event get /api/v2/events/{eventID}/openers # Get play-by-play for an event Source: https://docs.therundown.io/api-reference/generated/v2-events/get-play-by-play-for-an-event get /api/v2/events/{eventID}/plays Returns the play-by-play timeline for a live or completed event — one entry per play with description, period, game clock, running score, and (as attribution rolls out) the players involved. Live game state and play-by-play require an **Ultra plan or higher**; keys on lower tiers receive a `403`. The companion `live_game_state` snapshot (current inning/quarter, count, base runners, possession, and more) is embedded on live event payloads for entitled keys. Also available for streaming via the `plays` channel on the V2 WebSocket. # Get futures for a sport Source: https://docs.therundown.io/api-reference/generated/v2-futures/get-futures-for-a-sport get /api/v2/sports/{sportID}/futures Returns futures/outright **competition events** (e.g. tournament winner) for a sport, with future-class markets in the standard V2 market → participants → lines → prices shape and per-market settlement state. Competition events are excluded from the dated game endpoints and served only here. **Early access — requires an Ultra plan or higher.** A competition is an **interval**, not an instant: `event_date` is when it starts and `settle_by` is when it must have graded. Futures events carry no `teams` array and no score — the human-readable identity is `schedule.event_name`. **Date filtering uses interval overlap.** An event is returned when its `[event_date, settle_by]` span intersects the requested window — `settle_by >= from` and `event_date <= to`. So the default `from=now` means "in flight right now" and matches a season future that started months ago. **Paging is keyset-based** (no skip parameter). When more results exist the response carries `meta.next_cursor`; pass it back verbatim as `cursor`. Ordering is soonest-to-resolve first: `settle_by`, then `event_date`, then `event_id`. **Default visibility:** settled markets are removed per market, an event is hidden once every requested future-class market on it has settled, and terminal (final/canceled/abandoned) events are excluded. `include_settled=true` lifts all of those **and** the default `from=now`, so settled history is reachable. **Delta bootstrap:** `meta.delta_last_id` seeds `GET /api/v2/markets/delta` exactly like the game-event snapshots. Futures market IDs are **not** in the delta feed's default set — pass them explicitly (e.g. `market_ids=1141`) when polling. See the [Futures guide](/guides/futures) for the full integration recipe. # Get one futures competition, with progress and leaders Source: https://docs.therundown.io/api-reference/generated/v2-futures/get-one-futures-competition get /api/v2/sports/{sportID}/futures/{eventID} Returns **one** futures/outright competition event — the same envelope and event object shape as `GET /api/v2/sports/{sportID}/futures` (`meta.delta_last_id`, `events` with a single event carrying `event_id`, `sport_id`, `event_date`, `settle_by`, `event_status`, `settlement`, `schedule`, `markets`) — plus up to three optional detail-only keys not served on the listing. **Early access — requires an Ultra plan or higher.** **`progress`** — the competition's live non-odds state (a golf leaderboard, a team-sport standings table), refreshed on its own cycle independent of the odds board. `entries[].participant_id` is the same participant ID space `markets[].participants[].id` uses, so a progress row joins directly to a price. Omitted entirely (never `null`) when no progress document exists yet for the competition — "priced, no odds-board progress" is the normal state for a fresh team-championship board, not a degraded response. See [Competition Stats (Progress)](/guides/futures#competition-stats-progress) for the full field-by-field breakdown of both kinds live today. **`leaders`** / **`player_leaders`** — optional curated season-stats leaderboards, team-grain and player-grain respectively, for a hand-verified set of sports. Both reuse the same `{stat, stat_id, season_year, entries[]}` shape and are omitted entirely (never an empty array) when the sport is not curated or no stat rows exist yet — see [Competition Stats (Progress)](/guides/futures#competition-stats-progress) for the curated sport list and the two distinct entry ID spaces. **Settled competitions are always served here.** Unlike the listing (which hides settled markets and terminal events unless `include_settled=true`), this is a deep link to a specific competition you already hold the ID for, and it keeps working after settlement, with the graded results visible in `settlement`. There is no `include_settled` parameter on this endpoint. # Get available markets for a sport and date Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-available-markets-for-a-sport-and-date get /api/v2/sports/{sportID}/markets/{date} # Get available markets for an event Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-available-markets-for-an-event get /api/v2/events/{eventID}/markets Returns the list of market definitions available for this event (not prices — use the event endpoint with market_ids for prices). # Get line price chart data for a specific market Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-line-price-chart-data-for-a-specific-market get /api/v2/events/{eventID}/markets/{marketID}/history Returns time-series price data suitable for charting line movement. # Get market participants Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-market-participants get /api/v2/markets/participants Returns participants (teams, players, or results) for specified markets and events. # Get market price changes since a given ID Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-market-price-changes-since-a-given-id get /api/v2/markets/delta Returns market line price changes (new, updated, closed) since the specified `last_id`. Use for efficient polling. **Bootstrap flow:** To obtain an initial `last_id` cursor, call `GET /api/v2/sports/{sportID}/events/{date}` — the response `meta.delta_last_id` provides the current cursor. Then pass that value as `last_id` to begin polling this endpoint. **Staleness guard:** Cursors older than 30 minutes are rejected with HTTP 400. If your cursor becomes stale, re-bootstrap from the events endpoint. # Get market price history for an event Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-market-price-history-for-an-event get /api/v2/events/{eventID}/markets/history # Get opening prices for an event's markets Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-opening-prices-for-an-events-markets get /api/v2/events/{eventID}/markets/opening # Get price history for specific market line prices Source: https://docs.therundown.io/api-reference/generated/v2-markets/get-price-history-for-specific-market-line-prices get /api/v2/markets/history # List all market definitions Source: https://docs.therundown.io/api-reference/generated/v2-markets/list-all-market-definitions get /api/v2/markets Returns all canonical market definitions with IDs, names, period info, and whether the line value represents a participant. # Get a player by ID Source: https://docs.therundown.io/api-reference/generated/v2-players/get-a-player-by-id get /api/v2/players/{playerID} # List all sportsbooks (alias for /affiliates) Source: https://docs.therundown.io/api-reference/generated/v2-reference/list-all-sportsbooks-alias-for-affiliates get /api/v2/sportsbooks # List all sportsbooks/affiliates Source: https://docs.therundown.io/api-reference/generated/v2-reference/list-all-sportsbooksaffiliates get /api/v2/affiliates Returns all enabled sportsbooks with IDs, names, URLs, and an integration `status` (`healthy`, `unhealthy`, or `beta`). No authentication required. # List season types per sport Source: https://docs.therundown.io/api-reference/generated/v2-reference/list-season-types-per-sport get /api/v2/season_types # Get available dates for a sport Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-available-dates-for-a-sport get /api/v2/sports/{sportID}/dates # Get available dates for multiple sports Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-available-dates-for-multiple-sports get /api/v2/sports/dates Returns dates with events for the specified sport IDs. # Get available markets by date Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-available-markets-by-date get /api/v2/sports/markets/{date} Returns available market definitions for each sport on the given date. # Get conferences for a sport Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-conferences-for-a-sport get /api/v2/sports/{sportID}/conferences # Get divisions for a sport Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-divisions-for-a-sport get /api/v2/sports/{sportID}/divisions # Get events across sports for a date Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-events-across-sports-for-a-date get /api/v2/sports/events/{date} Returns event IDs grouped by sport for the given date. # Get teams for a sport Source: https://docs.therundown.io/api-reference/generated/v2-sports/get-teams-for-a-sport get /api/v2/sports/{sportID}/teams # List all sports Source: https://docs.therundown.io/api-reference/generated/v2-sports/list-all-sports get /api/v2/sports Returns all available sports with their IDs. No authentication required. # Get player game stats for an event Source: https://docs.therundown.io/api-reference/generated/v2-stats/get-player-game-stats-for-an-event get /api/v2/events/{eventID}/players/stats # Get team game stats for an event Source: https://docs.therundown.io/api-reference/generated/v2-stats/get-team-game-stats-for-an-event get /api/v2/events/{eventID}/stats # List all stat definitions Source: https://docs.therundown.io/api-reference/generated/v2-stats/list-all-stat-definitions get /api/v2/stats # Get a team by ID Source: https://docs.therundown.io/api-reference/generated/v2-teams/get-a-team-by-id get /api/v2/teams/{teamID}/ # Get player season stats for a team Source: https://docs.therundown.io/api-reference/generated/v2-teams/get-player-season-stats-for-a-team get /api/v2/teams/{teamID}/players/stats # Get players for a team Source: https://docs.therundown.io/api-reference/generated/v2-teams/get-players-for-a-team get /api/v2/teams/{teamID}/players # Get season stats for a team Source: https://docs.therundown.io/api-reference/generated/v2-teams/get-season-stats-for-a-team get /api/v2/teams/{teamID}/stats # Markets WebSocket — stream real-time market price updates Source: https://docs.therundown.io/api-reference/generated/v2-websocket/markets-websocket-—-stream-real-time-market-price-updates get /api/v2/ws/markets Establishes a WebSocket connection for streaming V2 market price changes in real-time. **Connection**: `wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY` **Filter parameters** (query string): - `affiliate_ids` — comma-separated affiliate IDs - `sport_ids` — comma-separated sport IDs - `event_ids` — comma-separated event IDs - `market_ids` — comma-separated market IDs All filters are optional. If none are specified, all messages are received. **Heartbeat**: Server sends `{"meta":{"type":"heartbeat"},"data":{"now":""}}` every 15 seconds. **Message format**: ```json { "meta": {...}, "data": { "affiliate_id": 19, "sport_id": 4, "event_id": "abc123", "market_id": 1, ... } } ``` **Queue**: 1024-message buffer per client. If the client falls behind, messages are dropped. # Multiplexed WebSocket — subscribe to markets, scores, plays, stats, and live channels Source: https://docs.therundown.io/api-reference/generated/v2-websocket/multiplexed-websocket-—-subscribe-to-markets-scores-plays-and-live-channels get /api/v2/ws Establishes a single WebSocket connection carrying multiple logical channels. Instead of query-parameter filters, send JSON subscribe messages after connecting. **Connection**: `wss://therundown.io/api/v2/ws?key=YOUR_API_KEY` **Channels**: - `markets` — market price updates (same payloads as `/api/v2/ws/markets`) - `scores` — score and event-status updates (no live game state fields) - `plays` — play-by-play deltas for live games. **Requires an Ultra plan or higher** (the live game state entitlement); a `plays`, `stats`, or `live` subscribe from a non-entitled key is rejected with error code `forbidden`. - `stats` — changed team and player box-score rows, delivered inline with `meta.type=game_stats`. **Requires an Ultra plan or higher**. This is a delta-only channel with no snapshot, replay, or resume; bootstrap and recover from both event game-stat REST endpoints. Each changed stat row is one stats data point. - `live` — score/status deltas including `live_game_state`, play-by-play deltas, and game-stat deltas, on one subscription. **Requires an Ultra plan or higher**; `live_game_state` and `game_state` are accepted aliases. **Subscribe** (client → server): ```json { "action": "subscribe", "id": "p1", "channel": "plays", "params": { "sport_ids": [3], "event_ids": [""] } } ``` The server acknowledges with `{"type":"subscribed","id":"p1","sequence":N,"message":"subscribed to plays"}`. Supported `params` filters: `sport_ids`, `event_ids` (plus `market_ids` and `affiliate_ids` on the `markets` channel). `stats` has no stat, team, or player filter. Unsubscribe with `{"action":"unsubscribe","id":"p1"}`. **Delta messages** (server → client) are wrapped in an envelope tagged with your subscription `id`: ```json { "type": "delta", "id": "p1", "sequence": 42, "sub_sequence": 7, "delta_last_id": "...", "data": { "meta": { "type": "play" }, "data": { ... } } } ``` A stats delta uses the same outer envelope. Its inner payload has `meta.type=game_stats`; `data.team_stats[]` and `data.player_stats[]` contain only changed rows, and each row's `value` remains a JSON string. Upsert those rows into the REST-bootstrapped box score rather than replacing a whole group. If both row arrays are absent and `complete=true`, the frame is the terminal completion marker; mark the cached box complete and treat repeats as idempotent. If both arrays are absent without `complete=true`, the frame is an invalidation fallback: refetch both `GET /api/v2/events/{eventID}/stats` and `GET /api/v2/events/{eventID}/players/stats`. Row deltas and completion markers use an RFC 3339 `updated_at`; the fallback uses numeric Unix seconds. Do not use `delta_last_id` as a game-stat replay cursor. Each changed nested row is one stats data point; either zero-row variant costs one stats data point. **Snapshots**: add `"snapshot": true` to `params` to receive current state (`snapshot` frames, then `snapshot_complete`) before deltas begin; an active subscription can request a fresh snapshot at any time with `{"action":"snapshot","id":"p1"}`. Snapshot requests need a bounded scope and are metered as data points like the equivalent REST reads. Snapshots are not supported on `stats`; a request returns `snapshot_error` with code `unsupported_snapshot`. A `live` snapshot does not include a game-stat baseline. **Errors**: `{"type":"error","id":"...","code":"...","message":"..."}` with codes `forbidden` (plays/stats/live without Ultra+), `invalid_channel`, `missing_id`, `duplicate_id`, and `subscription_limit` (plan's concurrent subscription cap reached). A slow multiplexed client may receive the connection-level error code `buffer_overflow` immediately before the connection closes with reason `buffer_overflow:reconnect_and_catchup`. **Queue and recovery**: each non-market subscription has its own 1024-message outbound queue; market subscriptions are sized separately. If a live frame cannot be queued, the server closes the connection rather than continue with a silent gap. Reconnect, re-send subscriptions, and recover current state before applying new deltas. For `stats`, refetch both event game-stat REST resources. Concurrent connection and subscription limits vary by tier. See the [WebSocket reference](/api-reference/v2/websocket) for full protocol details. # Stats Source: https://docs.therundown.io/api-reference/v2/stats Stat definitions, REST box scores, and live team/player game-stat deltas ## Overview The stats endpoints provide statistical data at multiple levels: a reference catalog of all stat definitions, team-level game stats per event, and player-level game stats per event. Season-level stats are available through the [Teams endpoints](/api-reference/v2/teams). For supported live games, changed box-score rows also stream over the V2 WebSocket. ## Live Game Stats over WebSocket On Ultra plans and higher, subscribe to the `stats` channel on the multiplexed endpoint: ```json theme={null} { "action": "subscribe", "id": "game-stats", "channel": "stats", "params": { "event_ids": ["EVENT_ID"] } } ``` For supported live games, team and player changes stream at play latency — typically within a few seconds of the corresponding play-by-play update. Like all live game data (plays included), stats trail the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay. The frames are row-level deltas, not full boxes: each `team_stats[].stats` or `player_stats[].stats` entry contains one changed value and its stat dictionary. Keep `value` as a string and upsert by owner ID plus `stat_id`. Because `stats` has no snapshot or replay source, use this sequence on first connect, reconnect (including a `buffer_overflow:reconnect_and_catchup` close), or a detected gap: 1. Subscribe and buffer incoming `game_stats` frames. 2. Fetch the current team box from `GET /api/v2/events/{eventID}/stats` and player box from `GET /api/v2/events/{eventID}/players/stats`. 3. Replace your local baseline, then merge the buffered deltas in order. 4. Continue merging live deltas. A zero-row frame with `complete: true` is the terminal completion marker; mark the cached box complete and treat repeats as idempotent. A zero-row frame without `complete: true` is an invalidation fallback; refetch both REST resources. The combined `live` channel includes the same `game_stats` frames alongside scores and plays. Each changed nested stat row costs one stats data point; a zero-row completion marker or invalidation fallback costs one. See the [WebSocket reference](/api-reference/v2/websocket#game-stats-messages) for the full payload, filters, completion and fallback shapes, and recovery rules. *** ## Endpoints Returns the catalog of all available stat definitions, including IDs, names, and associated sports. Use this to discover which `stats_ids` are valid for filtering. ### Parameters | Parameter | Type | Required | Description | | ---------- | ----- | -------- | ----------------------------------------------------------- | | `id` | query | No | Filter by a specific stat ID | | `sport_id` | query | No | Filter by sport ID to see only stats relevant to that sport | ```bash All stat definitions theme={null} curl "https://therundown.io/api/v2/stats?key=YOUR_API_KEY" ``` ```bash NBA stats only theme={null} curl "https://therundown.io/api/v2/stats?key=YOUR_API_KEY&sport_id=4" ``` ```bash Lookup a specific stat theme={null} curl "https://therundown.io/api/v2/stats?key=YOUR_API_KEY&id=1" ``` ```python Python theme={null} import requests resp = requests.get( "https://therundown.io/api/v2/stats", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, params={"sport_id": 4} ) stats = resp.json() for s in stats: print(f"{s['id']}: {s['display_name']}") ``` ```javascript JavaScript theme={null} const resp = await fetch( "https://therundown.io/api/v2/stats?key=YOUR_API_KEY&sport_id=4" ); const stats = await resp.json(); stats.forEach(s => console.log(`${s.id}: ${s.display_name}`)); ``` ### Example Response ```json theme={null} [ { "id": 1, "name": "points", "display_name": "Points", "abbreviation": "PTS", "sport_id": 4, "category": "scoring" }, { "id": 2, "name": "rebounds", "display_name": "Rebounds", "abbreviation": "REB", "sport_id": 4, "category": "rebounding" }, { "id": 3, "name": "assists", "display_name": "Assists", "abbreviation": "AST", "sport_id": 4, "category": "playmaking" }, { "id": 4, "name": "steals", "display_name": "Steals", "abbreviation": "STL", "sport_id": 4, "category": "defense" }, { "id": 5, "name": "blocks", "display_name": "Blocks", "abbreviation": "BLK", "sport_id": 4, "category": "defense" } ] ``` Returns team-level statistics for a specific event (game). Includes box score data like points, rebounds, assists, etc. ### Parameters | Parameter | Type | Required | Description | | ----------- | ----- | -------- | -------------------------------------------------------------------------------------- | | `eventID` | path | Yes | Canonical V2 event ID from the `event_id` field | | `stats_ids` | query | No | Comma-separated stat IDs to include (max 12). If omitted, returns all available stats. | | `team_id` | query | No | Filter to a specific team | ```bash All team stats for a game theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/stats?key=YOUR_API_KEY" ``` ```bash Specific stats for one team theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/stats?key=YOUR_API_KEY&stats_ids=1,2,3&team_id=42" ``` ```python Python theme={null} import requests resp = requests.get( "https://therundown.io/api/v2/events/EVENT_ID/stats", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, params={"stats_ids": "1,2,3"} ) for team_stat in resp.json(): print(f"{team_stat['team']['name']}") for stat in team_stat["stats"]: print(f" {stat['stat']['display_name']}: {stat['value']}") ``` ```javascript JavaScript theme={null} const resp = await fetch( "https://therundown.io/api/v2/events/EVENT_ID/stats?key=YOUR_API_KEY&stats_ids=1,2,3" ); const teamStats = await resp.json(); teamStats.forEach(t => { console.log(t.team.name); t.stats.forEach(s => console.log(` ${s.stat.display_name}: ${s.value}`)); }); ``` ### Example Response ```json theme={null} [ { "team": { "team_id": 42, "name": "Boston Celtics", "abbreviation": "BOS", "is_away": true, "is_home": false }, "meta": { "complete": true, "event_id": "EVENT_ID" }, "stats": [ { "team_id": 42, "stat_id": 1, "stat": { "id": 1, "name": "points", "display_name": "Points", "abbreviation": "PTS", "sport_id": 4 }, "event_id": "EVENT_ID", "value": "112" }, { "team_id": 42, "stat_id": 2, "stat": { "id": 2, "name": "rebounds", "display_name": "Rebounds", "abbreviation": "REB", "sport_id": 4 }, "event_id": "EVENT_ID", "value": "48" } ] }, { "team": { "team_id": 45, "name": "Los Angeles Lakers", "abbreviation": "LAL", "is_away": false, "is_home": true }, "meta": { "complete": true, "event_id": "EVENT_ID" }, "stats": [ { "team_id": 45, "stat_id": 1, "stat": { "id": 1, "name": "points", "display_name": "Points", "abbreviation": "PTS", "sport_id": 4 }, "event_id": "EVENT_ID", "value": "105" }, { "team_id": 45, "stat_id": 2, "stat": { "id": 2, "name": "rebounds", "display_name": "Rebounds", "abbreviation": "REB", "sport_id": 4 }, "event_id": "EVENT_ID", "value": "42" } ] } ] ``` Stats are only available for events that have started or completed. Requesting stats for a future event returns an empty array. The nested team identity includes canonical `is_away` and `is_home` flags. Returns individual player statistics for a specific event. Includes per-player box score data. ### Parameters | Parameter | Type | Required | Description | | ------------ | ----- | -------- | ----------------------------------------------- | | `eventID` | path | Yes | Canonical V2 event ID from the `event_id` field | | `stats_ids` | query | No | Comma-separated stat IDs to include (max 12) | | `player_ids` | query | No | Comma-separated player IDs to filter (max 6) | ```bash All player stats for a game theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/players/stats?key=YOUR_API_KEY" ``` ```bash Specific players theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/players/stats?key=YOUR_API_KEY&player_ids=100,101,102" ``` ```bash Specific stat categories theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/players/stats?key=YOUR_API_KEY&stats_ids=1,2,3" ``` ```python Python theme={null} import requests resp = requests.get( "https://therundown.io/api/v2/events/EVENT_ID/players/stats", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, params={"stats_ids": "1,2,3"} ) for p in resp.json(): print(f"{p['player']['display_name']}") for s in p["stats"]: print(f" {s['stat']['display_name']}: {s['value']}") ``` ```javascript JavaScript theme={null} const resp = await fetch( "https://therundown.io/api/v2/events/EVENT_ID/players/stats?key=YOUR_API_KEY&stats_ids=1,2,3" ); const playerStats = await resp.json(); playerStats.forEach(p => { console.log(p.player.display_name); p.stats.forEach(s => console.log(` ${s.stat.display_name}: ${s.value}`)); }); ``` ### Example Response ```json theme={null} [ { "player": { "id": 1002, "display_name": "Jayson Tatum", "position": "SF", "team_id": 42 }, "meta": { "complete": true, "event_id": "EVENT_ID" }, "stats": [ { "stat": { "id": 1, "name": "points", "display_name": "Points", "abbreviation": "PTS", "sport_id": 4 }, "value": "32" }, { "stat": { "id": 2, "name": "rebounds", "display_name": "Rebounds", "abbreviation": "REB", "sport_id": 4 }, "value": "9" } ] }, { "player": { "id": 1001, "display_name": "LeBron James", "position": "SF", "team_id": 45 }, "meta": { "complete": true, "event_id": "EVENT_ID" }, "stats": [ { "stat": { "id": 1, "name": "points", "display_name": "Points", "abbreviation": "PTS", "sport_id": 4 }, "value": "28" }, { "stat": { "id": 3, "name": "assists", "display_name": "Assists", "abbreviation": "AST", "sport_id": 4 }, "value": "10" } ] } ] ``` Player REST rows omit IDs already represented by the group: use `player.id` for the owner, `meta.event_id` for the event, and `stat.id` for the stat key. Live WebSocket rows additionally carry explicit `player_id` and `stat_id` fields. MLB game-stat definitions include `startingPitcher` and the sparse `positionPlayerPitching` flag. Query `GET /api/v2/stats?sport_id=3` to discover their current IDs; do not hard-code IDs. *** ## Stats at Different Levels | Level | Endpoint | Description | | ----------------- | -------------------------------------------- | ---------------------------------- | | **Definitions** | `GET /api/v2/stats` | What stats exist and their IDs | | **Team game** | `GET /api/v2/events/{eventID}/stats` | Team box score for one game | | **Player game** | `GET /api/v2/events/{eventID}/players/stats` | Player box score for one game | | **Team season** | `GET /api/v2/teams/{teamID}/stats` | Aggregate team stats for a season | | **Player season** | `GET /api/v2/teams/{teamID}/players/stats` | Per-player season stats for a team | # WebSocket Source: https://docs.therundown.io/api-reference/v2/websocket Real-time streaming of odds, scores, plays, and game stats via WebSocket ## Overview TheRundown provides a V2 WebSocket endpoint for streaming real-time data without polling: | Endpoint | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | | `/api/v2/ws/markets` | Streams market price changes (odds updates) as they happen | | `/api/v2/ws` | Multiplexed endpoint — subscribe to `markets`, `scores`, `plays`, `stats`, and `live` channels over one connection | WebSocket connections do **not** count against your REST API rate limit — but each plan has a hard cap on **concurrent connections** (and on subscriptions per connection on the multiplexed endpoint). Opening a connection beyond the cap is rejected at upgrade time with a `429` (`WebSocket connection limit reached`). | Plan | Concurrent connections | Subscriptions per connection | | ---------- | ---------------------- | ---------------------------- | | Ultra | 1 | 3 | | Super | 3 | 5 | | Mega | 5 | 10 | | Max | 10 | 25 | | Enterprise | 50 | 50 | WebSocket access requires a real-time API tier (**Ultra and above**). The `plays`, `stats`, and `live` channels additionally require the live game state entitlement, which is also included from Ultra up — see [Rate Limits](/rate-limits#current-api-tier-defaults). *** ## Connection Connect using a standard WebSocket client. Authentication is via the `key` query parameter: ``` wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY ``` Use `wss://` (secure WebSocket), not `ws://`. Connections on `ws://` will be rejected. *** ## Markets WebSocket ### `GET /api/v2/ws/markets` Streams V2 market price changes in real time. Every time a sportsbook updates a price on any tracked market, you receive a message. ### Filter Parameters All filters are optional. If none are specified, you receive all messages across all sports and markets. | Parameter | Description | | --------------- | ---------------------------------------------- | | `affiliate_ids` | Comma-separated sportsbook IDs (e.g., `19,23`) | | `sport_ids` | Comma-separated sport IDs (e.g., `4,6`) | | `event_ids` | Comma-separated event IDs | | `market_ids` | Comma-separated market IDs (e.g., `1,2,3`) | ### Example Connection URLs ```bash theme={null} # All NBA market updates wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&sport_ids=4 # DraftKings + FanDuel moneyline updates only wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&affiliate_ids=19,23&market_ids=1 # Updates for a specific event wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&event_ids=EVENT_ID ``` ### Message Format Each message is a JSON object with two top-level fields: `meta` (message metadata) and `data` (the update payload). Unlike the REST API's nested structure, WebSocket messages deliver **one price update per message** in a flat format — each message represents a single price change for one participant, one market, and one sportsbook. #### Example: Market Price Update ```json theme={null} { "meta": { "type": "market_price", "version": "v2", "timestamp": 1772495104 }, "data": { "id": 193600383, "event_id": "9b9d0cf6007fdaeb15c3a1888dcfd5df", "affiliate_id": 26, "market_participant_id": 19402291, "market_id": 3, "line": "1.5", "price": "-117", "previous_price": "-122.0000", "price_delta": 5, "is_main_line": true, "normalized_market_participant_id": 10, "normalized_market_participant_type": 3, "sport_id": 7, "updated_at": "2026-03-02T23:44:44Z" } } ``` #### Meta Fields | Field | Type | Description | | ----------- | ------ | ---------------------------------------------------------------- | | `type` | string | `"market_price"` for price updates, `"heartbeat"` for keep-alive | | `version` | string | API version (`"v2"`) | | `timestamp` | number | Unix epoch timestamp when the message was sent | #### Data Fields | Field | Type | Description | | ------------------------------------ | ------- | -------------------------------------------------------------------------------- | | `id` | number | Unique price record ID | | `event_id` | string | Canonical V2 event ID | | `affiliate_id` | number | Sportsbook ID (see [Sportsbook IDs](/reference/sportsbooks)) | | `market_id` | number | Market type (see [Market IDs](/reference/markets)) | | `market_participant_id` | number | Sportsbook-specific participant ID for this market entry | | `normalized_market_participant_id` | number | Canonical participant ID — maps to `participant.id` in REST API responses | | `normalized_market_participant_type` | number | Participant type identifier | | `line` | string | Line value (e.g., `"-4.5"` for spread, `"224.5"` for total, `"0"` for moneyline) | | `price` | string | Current American odds (e.g., `"-117"`, `"+150"`) | | `previous_price` | string | Price before this update | | `price_delta` | number | Numeric change from previous price | | `is_main_line` | boolean | Whether this is the primary line | | `sport_id` | number | Sport ID | | `updated_at` | string | ISO 8601 timestamp of the price change | *** ## Multiplexed WebSocket Channels The multiplexed endpoint at `/api/v2/ws` carries multiple logical channels over a single connection. Instead of query-parameter filters, you send JSON subscribe messages after connecting: ``` wss://therundown.io/api/v2/ws?key=YOUR_API_KEY ``` For a step-by-step client walkthrough of this endpoint — connecting, subscribing, routing messages, and reconnecting — see the [Multiplexed WebSocket guide](/guides/multiplexed-websocket). The `plays`, `stats`, and `live` channels require the live game state entitlement (**Ultra plan or higher**) — the same entitlement as the REST [plays endpoint](/api-reference/generated/v2-events/get-play-by-play-for-an-event). The `markets` and `scores` channels are available to any WebSocket-entitled key. ### Subscribing Send a subscribe action with a client-chosen `id`, the `channel`, and optional filters: ```json theme={null} { "action": "subscribe", "id": "p1", "channel": "plays", "params": { "sport_ids": [3], "event_ids": ["816efd1e5767d7133b5bc70c77173a18"] } } ``` The server acknowledges with: ```json theme={null} { "type": "subscribed", "id": "p1", "sequence": 42, "message": "subscribed to plays" } ``` | Channel | Delivers | | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `markets` | Market price updates (same payloads as `/api/v2/ws/markets`) | | `scores` | Score and event-status updates (no live game state fields) | | `plays` | Play-by-play deltas for live games (Ultra+) | | `stats` | Changed team and player box-score rows (`meta.type=game_stats`) for live games (Ultra+) | | `live` | Everything in-game on one subscription: score/status deltas including the `live_game_state` surface (`meta.type=score`), play-by-play deltas (`meta.type=play`), and game-stat deltas (`meta.type=game_stats`) (Ultra+). `live_game_state` and `game_state` are accepted aliases. | Filters supported in `params`: `sport_ids`, `event_ids` (plus `market_ids` and `affiliate_ids` on the `markets` channel). The `stats` channel does not support `stats_ids`, team, or player filters; `market_ids` and `affiliate_ids` do not apply to it. ### Snapshots Add `"snapshot": true` to `params` to receive the current state before live deltas begin. The server sends the `subscribed` ack, one or more `snapshot` frames, then `snapshot_complete`; deltas start after that. Each snapshot covers your subscription scope only — `markets` snapshots use the same filters and market defaults as the REST events endpoints (each frame carries `scope.requested_market_ids` / `scope.returned_market_ids`), `live` snapshots include the current score/status and live game state, and `plays` snapshots return up to 500 current plays with a `cursor` holding the oldest/newest play sequence. Snapshot requests need a bounded scope: `event_ids`, or `sport_ids` plus `date` (play snapshots require `event_ids`). An existing subscription can request a fresh snapshot at any time without reconnecting: ```json theme={null} { "action": "snapshot", "id": "p1" } ``` Snapshot frames are metered as data points by category, the same as the equivalent REST reads. The `stats` channel is delta-only and does not support snapshots, resync, resume, or replay. A snapshot request returns `snapshot_error` with code `unsupported_snapshot`, and a `live` snapshot does not contain a game-stat baseline. On first connect, reconnect, or a detected gap, fetch both [`GET /api/v2/events/{eventID}/stats`](/api-reference/generated/v2-stats/get-team-game-stats-for-an-event) and [`GET /api/v2/events/{eventID}/players/stats`](/api-reference/generated/v2-stats/get-player-game-stats-for-an-event), then apply new deltas. ### Play messages Each play arrives wrapped in a delta envelope tagged with your subscription `id`: ```json theme={null} { "type": "delta", "id": "p1", "sequence": 42, "sub_sequence": 7, "delta_last_id": "...", "data": { "meta": { "type": "play" }, "data": { "sport_id": 3, "event_id": "816efd1e5767d7133b5bc70c77173a18", "sequence": 214, "period": 6, "half_indicator": "bottom", "type": "single", "description": "Bobby Witt Jr. singles on a line drive to center field.", "score_away_after": 2, "score_home_after": 3 } } } ``` To stop receiving a channel, send `{"action": "unsubscribe", "id": "p1"}`. ### Game stats messages Subscribe on the multiplexed endpoint, not the dedicated `/api/v2/ws/markets` feed, and scope the subscription by sport, event, or both: ```json theme={null} { "action": "subscribe", "id": "nba-stats", "channel": "stats", "params": { "sport_ids": [4], "event_ids": ["EVENT_ID"] } } ``` For supported live games, team and player box-score changes stream at play latency — typically within a few seconds of the corresponding play-by-play update (live game data as a whole trails the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay). Each frame contains only stat rows whose value changed; it is not a complete box score: ```json theme={null} { "type": "delta", "id": "nba-stats", "sequence": 43, "sub_sequence": 8, "delta_last_id": "...", "data": { "meta": { "type": "game_stats", "version": "v2", "timestamp": 1785529872 }, "data": { "event_id": "EVENT_ID", "sport": "nba", "sport_id": 4, "complete": false, "updated_at": "2026-07-31T20:31:12Z", "team_stats": [ { "team": { "team_id": 42, "name": "Boston", "abbreviation": "BOS", "is_away": true, "is_home": false }, "meta": { "complete": false, "event_id": "EVENT_ID" }, "stats": [ { "team_id": 42, "stat_id": 1, "stat": { "id": 1, "name": "points", "display_name": "Points", "category": "scoring", "abbreviation": "PTS", "description": "Points scored", "sport_id": 4 }, "value": "83" } ] } ], "player_stats": [ { "player": { "id": 1002, "sport_id": 4, "team_id": 42, "display_name": "Jayson Tatum" }, "meta": { "complete": false, "event_id": "EVENT_ID" }, "stats": [ { "player_id": 1002, "stat_id": 1, "stat": { "id": 1, "name": "points", "display_name": "Points", "category": "scoring", "abbreviation": "PTS", "description": "Points scored", "sport_id": 4 }, "value": "31" } ] } ] } } } ``` | Field | Description | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `event_id`, `sport`, `sport_id` | Event and sport identity for filtering and cache routing | | `complete` | Whether the event's box score is marked complete. A zero-row frame with `complete: true` is the terminal completion marker | | `updated_at` | RFC 3339 timestamp on a full-push delta | | `team_stats[]`, `player_stats[]` | Groups that changed in this frame; an omitted group did not change | | `team` / `player` | Canonical owner identity for the group | | group `meta` | The group's `event_id` and `complete` state, matching the REST response shape | | `stats[]` | Changed rows only. Upsert each row by owner ID plus `stat_id`; do not replace the full group | | `stat` | Embedded stat dictionary (`id`, name/display fields, category, abbreviation, description, and `sport_id`) | | `value` | Raw stat value, always encoded as a JSON string (for example, `"31"` or `"38-79"`) | Each changed row across `team_stats[].stats` and `player_stats[].stats` consumes one data point in the `stats` category, including when the frame arrives through the `live` channel. A valid `game_stats` frame with no nested rows — either a completion marker or an invalidation fallback — consumes one stats data point. If one inbound frame matches overlapping `stats` and `live` subscriptions on the same connection, it is delivered to both subscriptions but billed once on that connection. A `game_stats` frame with both stat arrays absent has one of two meanings: * If `complete` is `true`, it is the terminal completion marker. It includes `event_id`, `sport`, `sport_id`, and an RFC 3339 `updated_at`. Mark the cached box score complete; repeated markers are idempotent. Refetch only if you do not have a baseline or your normal gap policy requires it. * If `complete` is absent, it is the legacy invalidation fallback. It contains `event_id`, `sport_id`, and a numeric Unix-seconds `updated_at`. Refetch both REST box-score resources. Parse `updated_at` tolerantly across row deltas, completion markers, and invalidation fallbacks. The outer `sub_sequence` can help detect a gap while the subscription is live, but neither it nor `delta_last_id` can replay game-stat changes. `delta_last_id` is the markets-delta cursor even when it appears on a stats frame. Recovery is always a REST refetch followed by continued streaming. ### Error codes Errors are returned as `{"type": "error", "id": "...", "code": "...", "message": "..."}`: | Code | Meaning | | -------------------- | --------------------------------------------------------------------------------------------------------- | | `forbidden` | Subscribing to `plays`, `stats`, or `live` without the live game state entitlement (Ultra plan or higher) | | `invalid_channel` | Unknown `channel` value | | `missing_id` | Subscribe action sent without an `id` | | `duplicate_id` | An active subscription already uses this `id` | | `subscription_limit` | Your plan's concurrent subscription cap was reached | | `buffer_overflow` | The multiplexed connection fell behind and will close; reconnect, resubscribe, and catch up from REST | *** ## Heartbeat The WebSocket endpoint sends a heartbeat message every **15 seconds** to keep the connection alive: ```json theme={null} { "meta": { "type": "heartbeat" }, "data": { "now": "2026-02-27T01:15:00Z" } } ``` Your client should detect heartbeats and use them to confirm the connection is healthy. If you stop receiving heartbeats, the connection may have dropped -- reconnect. *** ## Message Queue On the multiplexed endpoint, every non-market subscription has its own **1024-message outbound queue**. The `markets` channel is sized separately for its higher message volume. If a subscription or fan-out queue cannot accept a live frame, the server closes the connection rather than let it continue with a silent gap. When possible, it first sends: ```json theme={null} { "type": "error", "code": "buffer_overflow", "message": "WebSocket client is not reading fast enough; reconnect and catch up via REST." } ``` The following close frame has reason `buffer_overflow:reconnect_and_catchup`. Reconnect, re-send every subscription, and restore current state before applying new deltas. For `stats`, fetch both event game-stat REST resources and merge newly buffered deltas because snapshots and replay are unsupported. Keep handlers fast and offload heavy work asynchronously. Narrow each subscription with the supported sport, event, market, and affiliate filters to reduce queue pressure. *** ## Client Examples ```javascript JavaScript (Browser / Node.js) theme={null} const ws = new WebSocket( "wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&sport_ids=4&market_ids=1,2,3" ); ws.onopen = () => { console.log("Connected to TheRundown WebSocket"); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); // Skip heartbeats if (msg.meta?.type === "heartbeat") return; const d = msg.data; console.log( `Event ${d.event_id} | market=${d.market_id} aff=${d.affiliate_id}` ); console.log( ` line=${d.line} price=${d.price} (was ${d.previous_price}, delta=${d.price_delta})` ); }; ws.onerror = (error) => { console.error("WebSocket error:", error); }; ws.onclose = (event) => { console.log(`Disconnected: code=${event.code} reason=${event.reason}`); // Implement reconnection logic here }; ``` ```python Python (websockets library) theme={null} import asyncio import json import websockets API_KEY = "YOUR_API_KEY" URL = f"wss://therundown.io/api/v2/ws/markets?key={API_KEY}&sport_ids=4" async def listen(): async with websockets.connect(URL) as ws: print("Connected to TheRundown WebSocket") async for message in ws: msg = json.loads(message) if msg.get("meta", {}).get("type") == "heartbeat": continue d = msg["data"] print( f"Event {d['event_id']} | market={d['market_id']} aff={d['affiliate_id']}" ) print( f" line={d['line']} price={d['price']}" f" (was {d['previous_price']}, delta={d['price_delta']})" ) asyncio.run(listen()) ``` ```javascript Node.js (ws library) with Reconnection theme={null} const WebSocket = require("ws"); const API_KEY = "YOUR_API_KEY"; const URL = `wss://therundown.io/api/v2/ws/markets?key=${API_KEY}&sport_ids=4`; function connect() { const ws = new WebSocket(URL); ws.on("open", () => console.log("Connected")); ws.on("message", (raw) => { const msg = JSON.parse(raw); if (msg.meta?.type === "heartbeat") return; const d = msg.data; console.log(`Update: event=${d.event_id} market=${d.market_id} price=${d.price}`); }); ws.on("close", () => { console.log("Disconnected, reconnecting in 3s..."); setTimeout(connect, 3000); }); ws.on("error", (err) => { console.error("WebSocket error:", err.message); ws.close(); }); } connect(); ``` *** If WebSocket is not an option for your architecture, use the [REST delta endpoints](/guides/efficient-polling) to poll for changes efficiently. For the full list of market types you can filter on, see [Market IDs](/reference/markets). ## Best Practices The unfiltered market feed can be very high volume. Always apply `sport_ids`, `market_ids`, or `event_ids` filters to receive only the data you need. On the multiplexed endpoint, queue overflow closes the connection and requires a reconnect plus state recovery. WebSocket connections can drop due to network issues, server deployments, idle timeouts, or multiplexed queue overflow. Always implement reconnection logic with exponential backoff (e.g., 1s, 2s, 4s, 8s, max 30s). After `buffer_overflow:reconnect_and_catchup`, restore current state from REST before applying new deltas. If you have not received any message (including heartbeats) for 30+ seconds, assume the connection is dead and reconnect. Do not wait for the WebSocket `close` event, as it may not fire reliably in all network conditions. Keep your message handler fast. Parse the JSON and push work to a queue or separate processing thread. On the multiplexed endpoint, a slow handler can fill a subscription's outbound queue, causing the server to close the connection rather than continue after a silent gap. # Authentication Source: https://docs.therundown.io/authentication Learn how to authenticate your requests to TheRundown API using API keys. TheRundown API supports two methods of authentication. Choose the method that best fits your application architecture. ## Authentication Methods ### Query Parameter The simplest approach is to pass your API key as a query parameter. Append `key` to any request URL. ``` https://therundown.io/api/v2/events?key=YOUR_API_KEY ``` Query parameter authentication is convenient for testing but may expose your key in server logs and browser history. Prefer header-based authentication in production. ### Request Header Pass your API key in the `X-TheRundown-Key` request header. This is the recommended method for server-side applications. ``` X-TheRundown-Key: YOUR_API_KEY ``` ## Code Examples ```bash curl (Query Parameter) theme={null} curl "https://therundown.io/api/v2/sports/2/events/2026-02-12?key=YOUR_API_KEY" ``` ```bash curl (Header) theme={null} curl -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/sports/2/events/2026-02-12" ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" # Using header authentication (recommended) headers = { "X-TheRundown-Key": API_KEY } response = requests.get(f"{BASE_URL}/sports/2/events/2026-02-12", headers=headers) data = response.json() print(data) ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; // Using header authentication (recommended) const response = await fetch(`${BASE_URL}/sports/2/events/2026-02-12`, { headers: { "X-TheRundown-Key": API_KEY, }, }); const data = await response.json(); console.log(data); ``` ## Public Endpoints The following endpoints do **not** require authentication and can be called without an API key: | Endpoint | Description | | ------------------------ | ---------------------------------------------------- | | `GET /api/v2/sports` | Returns the list of available sports and their IDs. | | `GET /api/v2/affiliates` | Returns the list of available sportsbook affiliates. | These endpoints are useful for bootstrapping your application with reference data before making authenticated requests. ## Security Best Practices API keys embedded in frontend JavaScript, mobile app bundles, or public repositories can be extracted by anyone. Always route API calls through your own backend server. Store your API key in an environment variable rather than hardcoding it in source files. This prevents accidental commits to version control and makes key rotation straightforward. ```bash theme={null} # .env file (never commit this) THERUNDOWN_API_KEY=your_key_here ``` ```python Python theme={null} import os api_key = os.environ["THERUNDOWN_API_KEY"] ``` ```javascript Node.js theme={null} const apiKey = process.env.THERUNDOWN_API_KEY; ``` If you suspect your API key has been exposed, contact TheRundown support immediately to rotate your key. Update all services that reference the old key as part of the rotation. Maintain distinct API keys for development, staging, and production. This limits the blast radius if a non-production key is leaked and makes it easier to track usage per environment. # Changelog Source: https://docs.therundown.io/changelog Recent updates, new features, and improvements to TheRundown API. ## 2026 ### August 2026 * **Futures & outrights (early access)** — New `GET /api/v2/sports/{sportID}/futures` endpoint serving championship and tournament-winner competition events in the standard V2 market shape, priced across tracked sportsbooks. Competitions are interval events (`event_date` start, `settle_by` settlement horizon) with per-market `settlement` state once graded; date filtering uses interval overlap and paging is cursor-based. Live now for NFL, MLB, NCAAF, NHL, NBA, NCAAB, WNBA, EPL, PGA Tour golf (new sport ID `40`, including Top 5/10/20 Finish, Make The Cut, and First Round Leader markets), and Formula 1 season championships (new sport ID `41`). Price changes flow through the existing `/api/v2/markets/delta` feed — pass futures market IDs (e.g. `1141`) explicitly, as they are not in the delta default set. Early access on **Ultra plans and higher**; coverage is expanding. See the [Futures guide](/guides/futures). ### July 2026 * **Real-time game stats WebSocket** — Live team and player box-score changes now stream as row-level deltas on the new `stats` channel of the [multiplexed V2 WebSocket](/guides/multiplexed-websocket), and the combined `live` channel includes the same `game_stats` frames. Stat changes now stream at play latency — typically within a few seconds of the corresponding play-by-play update — instead of the previous roughly five-minute refresh path. Like all live game data, stats trail the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay. Available on Ultra plans and higher; each changed stat row counts as one stats data point. * **Richer game-stat context** — Team game-stat responses now identify the canonical home and away sides with `team.is_home` and `team.is_away`. MLB game stats also distinguish the starting pitcher (`startingPitcher`) from a position player used as a pitcher (`positionPlayerPitching`); discover their current IDs through `GET /api/v2/stats` rather than hard-coding them. * **MLB doubleheader and makeup metadata** — Event schedule objects now expose `game_number` for true doubleheaders and `game_type` (`doubleheader`, `makeup`, or `doubleheader_makeup`), so integrations no longer need to parse event headlines. * **Self-service downgrades** -- You can now schedule a plan downgrade from your dashboard. Downgrades take effect at the end of the current billing period — you keep your current tier until then, nothing extra is charged, and a scheduled downgrade can be canceled any time before it applies. * **Sportsbook source IDs** -- V2 event responses now include `affiliate_source_ids` (each sportsbook's own identifier for the event), and price objects include `source_id` where the book exposes one. Useful for deep-linking and joining against sportsbook-keyed datasets. * **NFL Preseason and NBA Summer League** -- 2026 NFL Preseason events now flow under a dedicated sport ID `25`, separate from the NFL regular season (`2`). NBA Summer League is live again under sport ID `32`, as in previous seasons. ### June 2026 * **Live game state and play-by-play — Ultra tier and above** -- New real-time game-state data, available exclusively on **Ultra plans and higher**. Live event payloads embed a `live_game_state` snapshot (current inning/quarter, balls-strikes-outs and base runners for MLB, down & distance for football, possession and power-play detail for other sports), and the new `GET /api/v2/events/{eventID}/plays` endpoint returns the full play-by-play timeline with running scores. Plays also stream in real time via the new `plays` channel on the V2 WebSocket. Coverage spans MLB, NBA (including Summer League), WNBA, NCAAB, NFL, NCAAF, NHL, and soccer, with per-play player attribution rolling out progressively. * **Included data points raised 2.5x on all paid tiers** -- Monthly allowances increased at no price change: Starter 5M, Pro 25M, Ultra 100M, Super 250M, Mega 500M, Max 2.5B data points per month. Overage rates, burst limits, and data delays are unchanged. * **Weekly billing plans** -- Every paid API tier is now available on a weekly billing cadence at a premium over monthly — useful for covering a single tournament or a stretch of a season. Weekly plans include a proportional weekly share of the tier's monthly allowance, metered over the 7-day billing window; the `X-Datapoints-Period` header reads `weekly` on these plans. See [Weekly Billing](/rate-limits#weekly-billing). * **Player props now require Starter or higher** -- Player prop markets are included on all paid plans starting at Starter, and are no longer returned on Free keys. * **`event_status` include filter** -- New query parameter on V1 and V2 sport/date event, openers, and closing endpoints. Pass `event_status=STATUS_IN_PROGRESS,STATUS_HALFTIME` (comma-separated) to receive only events in the listed statuses. Complements `exclude_status`, and is applied before it when both are present. * **`market_ids` capped at 12 per request** -- REST market endpoints now return a `400` when more than 12 market IDs are requested (previously extra IDs were silently ignored). Split larger requests into batches of 12, or omit the parameter: odds endpoints default to the core markets, and market-definition endpoints return all available definitions. * **3-Way Result (563) in default markets for soccer and NHL** -- Soccer leagues and NHL now default `market_ids` to `1,2,3,563`. The soccer 3-way moneyline (home/draw/away) is served on market `1` as three participants, matching long-standing V1 behavior; market `563` carries the NHL 60-minute (regulation-time) line. * **`is_main_line` on the markets delta feed** -- Delta entries now always include `is_main_line`, so main-line switches can be tracked from the delta feed alone without refetching snapshots. * **Upgrade hints on `429` responses** -- Rate-limit and data-point-cap responses now include `upgrade_url` and `upgrade_message` fields alongside the existing `error`, `limit`, and `Retry-After` information. * **Affiliate `status` field** -- The `GET /api/v2/affiliates` (and `/api/v2/sportsbooks`) response now includes a `status` for each sportsbook: `healthy` (live and verified), `unhealthy` (the feed is currently degraded or down), or `beta` (a new integration whose coverage we are still verifying). Use it to surface book health in your UI or to skip beta books in production. Treat unrecognized values as unknown — new statuses may be added without notice, so don't hard-code the set on the client side. * **Affiliate `regions` now opt-in** -- `GET /api/v2/affiliates` returns the `regions` array only when you pass `include=regions`. The default response omits it; add the parameter if your integration reads region data. * **FIFA World Cup 2026 coverage** -- Broad market coverage for the World Cup (sport ID `18`) across tracked sportsbooks, including 1X2, asian handicap, totals and first-half totals, both-teams-to-score, correct score, winning margin, goalscorer props, corners, and cards. ### May 2026 * **ATP and WTA Tennis leagues** -- Added ATP Tennis (sport ID `38`) and WTA Tennis (sport ID `39`) as first-class leagues. Full-match tennis markets use full-game periods (`0` prematch, `7` live); set-specific markets use period IDs to distinguish Set 1 and Set 2, including prematch Set 1/Set 2 (`3`/`4`) and live Set 1/Set 2 (`15`/`16`). * **Hard Rock Bet affiliate** -- Hard Rock Bet is now available as affiliate ID `28` in beta, with coverage across NBA, WNBA, MLB, NHL, NFL, soccer, tennis, and UFC including player props and live markets. Coverage is expanding while the integration matures. * **`espn_uid` on V2 events** -- Event payloads now include `espn_uid`, ESPN's universal identifier for the event, when available — useful for joining against ESPN-keyed datasets. Event stats responses also now include `meta.event_id` for easier correlation. ### April 2026 * **theScore Bet affiliate** -- theScore Bet is now available as affiliate ID 24, providing moneyline, spread/runline/puckline, and total markets for NBA, MLB, and NHL (prematch and live). Expanded coverage of additional supported markets is in progress. * **In-play coverage for LowVig, BetOnline, and Sportsbetting** -- LowVig (11), BetOnline (6), and Sportsbetting (4) now stream live in-play lines across the V2 market catalog, including player props, team props, and alternate lines. Additional market coverage for these books is in progress. ### March 2026 * **Live market variants now default** -- V2 event endpoints now return live moneyline, spread, and total (IDs 41, 42, 43) alongside the standard prematch markets by default. Integrations that pass explicit `market_ids` are unaffected. * **Faster live score updates** -- Live score data now reflects in-game changes much faster. The `score` object on event responses — including `event_status`, `display_clock`, `game_period`, and current scores — updates more frequently during live games. * **Ultra tier latency improvements** -- Ultra tier subscribers now receive WebSocket price updates in real time with no batching delay. REST endpoints for Ultra tier also return fresher data. ### February 2026 * **`exclude_status` parameter** -- New query parameter for V1 and V2 event endpoints. Pass `exclude_status=STATUS_POSTPONED,STATUS_CANCELED` (comma-separated) to filter out postponed and canceled games before they reach your application. * **`delta_last_id` bootstrap** -- V2 markets responses now include `meta.delta_last_id`, a starting cursor for delta polling. Use it to initialize `/api/v2/markets/delta` immediately after a snapshot without missing updates. * **Subscription plan headers** -- V2 responses now include headers that surface your subscription tier, rate limits, allowed bookmaker IDs, and active feature flags. Useful for building adaptive clients and debugging plan entitlements. * **`Retry-After` headers on rate limit responses** -- All `429 Too Many Requests` responses now include a `Retry-After` header indicating when you may retry. * **MLB Spring Training migration** -- MLB Spring Training events have been migrated from sport ID `3` to a dedicated sport ID `30`, isolating preseason games from the regular season. This follows the same pattern used for NBA Summer League (32) and other season-specific sport IDs. If you consume spring training data, update your integration to query sport ID `30`. * **OpenAPI spec published** -- Full [OpenAPI 3.1 specification](https://docs.therundown.io/openapi.yaml) now available for automated client generation and tooling integration. * **Market 553 deprecated — use market 410** -- The `any_time_goal_scorer` market (ID 553) has been consolidated into the `to_score` market (ID 410). Market ID 553 will no longer return data. Update integrations that query market 553 to use market 410 instead. ### January 2026 * **V2 Markets WebSocket** -- New `wss://therundown.io/api/v2/ws/markets` endpoint for real-time market and price updates, replacing the legacy V1 WebSocket. Supports granular filtering by sport, market, event, and affiliate. * **`price_delta` field** -- Pass `include=price_delta` on V2 event endpoints to receive the previous price and direction of change alongside current odds. Useful for animating line movement on page load. * **`live_variant_id` field** -- Market objects now include a `live_variant_id` mapping prematch markets to their live equivalents, enabling seamless prematch-to-live market switching. ## 2025 ### Q4 2025 * **Player prop markets expansion** -- Added combo prop markets: PRA (93), Points + Assists (99), Points + Rebounds (297), Rebounds + Assists (298). Added live player prop markets for points (90), assists (91), three-pointers (92), rebounds (982), blocks (983), and turnovers (984). * **Market line price history** -- New `GET /api/v2/events/{eventID}/markets/history` and `GET /api/v2/events/{eventID}/markets/{marketID}/history` endpoints for full price history with RFC 3339 time range filtering. Opening and closing line endpoints added at `/openers` and `/closing`. ### Q3 2025 * **V2 markets delta endpoint** -- `GET /api/v2/markets/delta` returns only markets that have changed, providing a more efficient alternative to full event polling. * **Team totals market** -- Market ID 94 added for individual team over/under totals. * **Kalshi affiliate** -- Added Kalshi (affiliate ID 25) to tracked sportsbooks. ### Q2 2025 * **V2 API general availability** -- The V2 market-based data model is now the recommended API version. V1 remains available but new features are V2-only. * **Alternate lines support** -- V2 events endpoint returns main and alternate lines. Use `main_line=true` to filter to primary lines only. * **Period-based markets** -- Markets now include `period_id` for half, quarter, and period-specific odds. # Errors Source: https://docs.therundown.io/errors HTTP status codes, error response formats, and troubleshooting guidance for TheRundown API. TheRundown API uses standard HTTP status codes to indicate whether a request succeeded or failed. This page covers every status code you may encounter, the structure of error responses, and how to handle them. ## HTTP Status Codes | Status Code | Meaning | Description | | ----------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `200` | OK | The request was successful. The response body contains the requested data. | | `400` | Bad Request | The request contains invalid parameters or is malformed. | | `401` | Unauthorized | Authentication failed. The API key is missing, invalid, or expired. | | `403` | Forbidden | Your plan does not include the requested feature (e.g., live game state on tiers below Ultra), or an account-level limit blocks the request. | | `404` | Not Found | The requested resource does not exist. | | `429` | Too Many Requests | You exceeded a burst throttle or a data-point cap. See [Rate Limits](/rate-limits). | | `500` | Internal Server Error | An unexpected error occurred on the server. | ## Error Response Format When an error occurs, the API returns a JSON object with a message describing the problem. ```json theme={null} { "message": "A human-readable description of the problem." } ``` Some endpoints — notably parameter-validation, rate-limit, and entitlement errors — use an `error` field instead of `message`. Check both fields when handling error responses generically. ## Detailed Status Code Reference ### 200 OK The request succeeded. For list endpoints, the response is a JSON array. For single-resource endpoints, the response is a JSON object. ```json theme={null} { "event_id": "abc123", "sport_id": 2, "teams": { ... }, "score": { ... } } ``` ### 400 Bad Request The request was rejected because one or more parameters are invalid. Check the `message` (or `error`) field for specifics. **Common causes:** * An invalid `sport_id` value * A malformed date format (expected `YYYY-MM-DD`) * An unrecognized query parameter value * More than 12 `market_ids` in a single request ```json theme={null} { "message": "Invalid sport_id: 999. See /v2/sports for valid sport IDs." } ``` Requesting more than 12 market IDs returns: ```json theme={null} { "error": "market_ids accepts at most 12 IDs per request; received 15. Split larger requests into batches of 12 or omit market_ids to use the default 1,2,3" } ``` When `market_ids` is omitted, soccer leagues and NHL default to `1,2,3,563` rather than `1,2,3`, and market-definition endpoints return all available definitions — see [Market IDs](/reference/markets). **How to fix:** Review the request parameters against the API reference documentation. Ensure all required parameters are present and correctly formatted. ### 401 Unauthorized Authentication failed. The API could not verify your identity. **Common causes:** * No API key was provided * The API key is invalid or has been revoked * The `X-TheRundown-Key` header is missing or malformed ```json theme={null} { "message": "unauthorized" } ``` **How to fix:** Verify that your API key is correct and included in the request. See the [Authentication](/authentication) guide for supported methods. ```bash theme={null} # Verify your key is being sent correctly curl -v -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/sports/2/events/2026-02-12" ``` ### 403 Forbidden Your API key is valid, but the request is blocked by a plan entitlement or an account-level limit. Unlike a `401`, re-authenticating will not help. **Common causes:** * Requesting live game state or play-by-play (`/api/v2/events/{eventID}/plays`) on a tier below Ultra * Requesting futures (`/api/v2/sports/{sportID}/futures`) on a tier below Ultra * Subscribing to the multiplexed WebSocket's `plays`, `stats`, or `live` channel without the live game state entitlement * A hard budget limit configured on the account has been reached ```json theme={null} { "error": "Live game state requires Ultra plan or higher", "upgrade_url": "/pricing/api" } ``` The futures entitlement error has the same shape: ```json theme={null} { "error": "Futures markets require Ultra plan or higher", "upgrade_url": "/pricing/api" } ``` **How to fix:** For entitlement errors, upgrade the plan or remove the gated feature from your integration — the [entitlement headers](/rate-limits#usage-headers) on any metered response show what your key can access. For budget-limit errors, raise or clear the account's budget cap from your dashboard. On the multiplexed WebSocket, a channel entitlement failure is delivered inside the open connection as `{"type":"error","code":"forbidden",...}` rather than as another HTTP response. ### 404 Not Found The requested resource does not exist. This typically means the event ID, sport ID, or other identifier in the URL path does not match any record. **Common causes:** * An event ID that does not exist or has been archived * A URL path that is misspelled or references a deprecated endpoint ```json theme={null} { "message": "Event not found: abc123" } ``` **How to fix:** Confirm the resource identifier is correct. Use the appropriate list endpoint to discover valid IDs before requesting a specific resource. ### 429 Too Many Requests TheRundown uses `429` for more than one condition. Read the response body and headers before deciding whether to retry immediately. ```json theme={null} { "error": "Rate limit exceeded", "limit": 2, "upgrade_url": "/pricing/api", "upgrade_message": "You've exceeded your plan's rate limit. Upgrade for a higher requests-per-second limit." } ``` You may also see: ```json theme={null} { "error": "Daily data point limit reached", "limit": 20000, "used": 20000, "upgrade_url": "/pricing/api", "upgrade_message": "You've hit the free tier's daily limit. Upgrade to a paid plan for a higher monthly allowance and rate limits." } ``` or: ```json theme={null} { "error": "Monthly data point limit reached", "limit": 25000000, "used": 25000000, "period": "monthly", "upgrade_url": "/pricing/api", "upgrade_message": "You've reached your plan's monthly data-point limit. Upgrade for a higher allowance, or enable overage billing to keep going." } ``` **How to fix:** * Read `Retry-After` first. * Inspect `X-Datapoints-Used`, `X-Datapoints-Remaining`, `X-Datapoints-Reset`, `X-Tier`, and `X-Rate-Limit`. * If the body says `Rate limit exceeded`, retry after a short backoff. * If the body says `Daily data point limit reached` or `Monthly data point limit reached`, this is a usage-window issue, not a one-second throttle. Retrying immediately will not help. ### 500 Internal Server Error An unexpected error occurred on the server side. This is not caused by your request. ```json theme={null} { "message": "An unexpected error occurred. Please try again later." } ``` **How to fix:** Retry the request after a brief delay. If the error persists, contact support at **[support@therundown.io](mailto:support@therundown.io)** and include the full request URL and timestamp. ## The 0.0001 Sentinel Value The value `0.0001` appearing in odds or line fields is **not** an error. It is a sentinel value indicating that a line is currently unavailable or has not yet been posted by the sportsbook. When a sportsbook has not released a line, or when a previously available line has been taken down, the API returns `0.0001` rather than `null` or omitting the field. This ensures a consistent numeric type across all responses and makes it straightforward to filter in your code. ```json theme={null} { "affiliate_id": 1, "spread": { "point_spread_home": 0.0001, "point_spread_away": 0.0001, "point_spread_home_money": 0.0001, "point_spread_away_money": 0.0001 } } ``` ### How to Handle 0.0001 Filter out the sentinel value when displaying or processing lines. Treat any field equal to `0.0001` as "not available." ```python Python theme={null} SENTINEL = 0.0001 def is_line_available(value): """Return True if the line value is real, not the sentinel.""" return value is not None and value != SENTINEL spread = event["spread"]["point_spread_home"] if is_line_available(spread): print(f"Home spread: {spread}") else: print("Home spread: not available") ``` ```javascript JavaScript theme={null} const SENTINEL = 0.0001; function isLineAvailable(value) { return value !== null && value !== undefined && value !== SENTINEL; } const spread = event.spread.point_spread_home; if (isLineAvailable(spread)) { console.log(`Home spread: ${spread}`); } else { console.log("Home spread: not available"); } ``` ## Troubleshooting Checklist If you are encountering errors, work through this checklist: 1. **Check your API key.** Is it present in the request? Is it valid? Try the key against a public endpoint like `/v2/sports`. 2. **Inspect the full response.** Read the `message` field in the error response for specific guidance. 3. **Review the request URL.** Ensure the path, query parameters, and date formats are correct. 4. **Check your billing and throttle headers.** If you are getting `429` responses, inspect `Retry-After`, `X-Datapoints-Remaining`, `X-Datapoints-Reset`, and `X-Rate-Limit`. 5. **Retry with backoff for 5xx errors.** Server errors are usually transient. Retry after a short delay. 6. **Contact support.** If the issue persists, email **[support@therundown.io](mailto:support@therundown.io)** with the request URL, response body, and timestamp. ## Retryable vs Non-Retryable Errors Not all errors should be retried. Retrying a `401` won't fix a bad API key, but a `502` may resolve on the next attempt. | Status | Retryable? | Action | | ------ | :------------: | ------------------------------------------------------------ | | `400` | No | Fix the request parameters | | `401` | No | Check your API key | | `403` | No | Upgrade your plan or remove the gated feature | | `404` | No | Verify the resource ID or URL path | | `429` | Yes, sometimes | Respect `Retry-After`; do not blindly retry cap-based `429`s | | `500` | Yes | Retry with exponential backoff | | `502` | Yes | Retry with exponential backoff | | `503` | Yes | Retry with exponential backoff | ## Retry Strategy For retryable errors, use **exponential backoff with jitter** to avoid thundering-herd problems when the server recovers. The pattern: wait `base * 2^attempt` seconds, add a random jitter, and cap the maximum delay. Three retries is usually sufficient — if the error persists after that, log it and move on. ```python Python theme={null} import requests import time import random API_KEY = "YOUR_API_KEY" BASE = "https://therundown.io/api/v2" RETRYABLE = {429, 500, 502, 503} def fetch_with_retry(url, params=None, max_retries=3): """Fetch a URL with exponential backoff for transient errors.""" for attempt in range(max_retries + 1): resp = requests.get( url, headers={"X-TheRundown-Key": API_KEY}, params=params, ) if resp.status_code == 200: return resp.json() if resp.status_code not in RETRYABLE or attempt == max_retries: resp.raise_for_status() if resp.status_code == 429: retry_after = resp.headers.get("Retry-After") wait = max(int(retry_after), 1) if retry_after else 60 else: wait = min(2 ** attempt + random.uniform(0, 1), 30) print(f"Retrying in {wait:.1f}s (attempt {attempt + 1}/{max_retries})") time.sleep(wait) # Usage events = fetch_with_retry(f"{BASE}/sports/4/events/2026-02-26") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE = "https://therundown.io/api/v2"; const RETRYABLE = new Set([429, 500, 502, 503]); async function fetchWithRetry(url, maxRetries = 3) { for (let attempt = 0; attempt <= maxRetries; attempt++) { const resp = await fetch(url, { headers: { "X-TheRundown-Key": API_KEY }, }); if (resp.ok) return resp.json(); if (!RETRYABLE.has(resp.status) || attempt === maxRetries) { throw new Error(`API error ${resp.status}: ${await resp.text()}`); } let wait; if (resp.status === 429) { const retryAfter = resp.headers.get("Retry-After"); wait = retryAfter ? Math.max(Number(retryAfter), 1) : 60; } else { wait = Math.min(2 ** attempt + Math.random(), 30); } console.log(`Retrying in ${wait.toFixed(1)}s (attempt ${attempt + 1}/${maxRetries})`); await new Promise((r) => setTimeout(r, wait * 1000)); } } // Usage const events = await fetchWithRetry( `${BASE}/sports/4/events/2026-02-26?key=${API_KEY}` ); ``` ## Complete Error-Handling Wrapper This wrapper combines retry logic with sentinel value filtering into a single utility you can use across your integration. ```python Python theme={null} import requests import time import random API_KEY = "YOUR_API_KEY" BASE = "https://therundown.io/api/v2" SENTINEL = 0.0001 RETRYABLE = {429, 500, 502, 503} def api_request(path, params=None, max_retries=3): """Make an API request with retry logic. Returns parsed JSON.""" url = f"{BASE}/{path.lstrip('/')}" for attempt in range(max_retries + 1): resp = requests.get( url, headers={"X-TheRundown-Key": API_KEY}, params=params, ) if resp.status_code == 200: return resp.json() if resp.status_code not in RETRYABLE or attempt == max_retries: resp.raise_for_status() if resp.status_code == 429: retry_after = resp.headers.get("Retry-After") wait = max(int(retry_after), 1) if retry_after else 60 else: wait = min(2 ** attempt + random.uniform(0, 1), 30) time.sleep(wait) def is_available(price_value): """Return True if the price is real (not the 0.0001 sentinel).""" return price_value is not None and price_value != SENTINEL def get_main_line_prices(event, market_id, affiliate_id): """Extract the main-line price for each participant in a market.""" results = [] for market in event.get("markets", []): if market["market_id"] != market_id: continue for participant in market.get("participants", []): for line in participant.get("lines", []): price_obj = line.get("prices", {}).get(str(affiliate_id)) if not price_obj or not price_obj.get("is_main_line"): continue price = price_obj["price"] results.append({ "participant": participant["name"], "line": line["value"], "price": price if is_available(price) else None, "available": is_available(price), }) return results # Usage data = api_request("sports/4/events/2026-02-26", {"market_ids": "1,2,3"}) for event in data.get("events", []): spreads = get_main_line_prices(event, market_id=2, affiliate_id=19) for s in spreads: if s["available"]: print(f"{s['participant']}: {s['line']} ({s['price']})") else: print(f"{s['participant']}: off the board") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE = "https://therundown.io/api/v2"; const SENTINEL = 0.0001; const RETRYABLE = new Set([429, 500, 502, 503]); async function apiRequest(path, params = {}, maxRetries = 3) { const query = new URLSearchParams({ key: API_KEY, ...params }); const url = `${BASE}/${path.replace(/^\//, "")}?${query}`; for (let attempt = 0; attempt <= maxRetries; attempt++) { const resp = await fetch(url); if (resp.ok) return resp.json(); if (!RETRYABLE.has(resp.status) || attempt === maxRetries) { throw new Error(`API error ${resp.status}: ${await resp.text()}`); } let wait; if (resp.status === 429) { const retryAfter = resp.headers.get("Retry-After"); wait = retryAfter ? Math.max(Number(retryAfter), 1) : 60; } else { wait = Math.min(2 ** attempt + Math.random(), 30); } await new Promise((r) => setTimeout(r, wait * 1000)); } } function isAvailable(priceValue) { return priceValue != null && priceValue !== SENTINEL; } function getMainLinePrices(event, marketId, affiliateId) { const results = []; for (const market of event.markets || []) { if (market.market_id !== marketId) continue; for (const participant of market.participants || []) { for (const line of participant.lines || []) { const priceObj = line.prices?.[String(affiliateId)]; if (!priceObj?.is_main_line) continue; results.push({ participant: participant.name, line: line.value, price: isAvailable(priceObj.price) ? priceObj.price : null, available: isAvailable(priceObj.price), }); } } } return results; } // Usage const data = await apiRequest("sports/4/events/2026-02-26", { market_ids: "1,2,3", }); for (const event of data.events || []) { const spreads = getMainLinePrices(event, 2, 19); for (const s of spreads) { if (s.available) { console.log(`${s.participant}: ${s.line} (${s.price})`); } else { console.log(`${s.participant}: off the board`); } } } ``` For strategies to reduce the number of API calls you make (and the errors you encounter), see the [Efficient Polling guide](/guides/efficient-polling) and [Rate Limits](/rate-limits). # FAQ Source: https://docs.therundown.io/faq Frequently asked questions about TheRundown API — data model, common issues, and integration tips. ## General V1 uses a flat lines model where odds are organized by sportsbook, with separate `moneyline`, `spread`, and `total` objects. V2 uses a market-based model where each market type (moneyline, spread, total, player props, etc.) is a separate entry with participants, lines, and prices nested inside. V2 is recommended for all new integrations — it supports player props, alternate lines, and new market types that V1 cannot represent. See the [V1 to V2 Migration Guide](/guides/v1-to-v2-migration) for a detailed comparison. A price of `0.0001` means the sportsbook has taken the line **off the board** — it is temporarily unavailable. This is not an error. Common causes include pending injury news, line recalculation, or approaching game time. Display it as "Off Board" or "N/A" and never use it in calculations. See [Sentinel Values](/reference/sentinel-values) and [Errors — The 0.0001 Sentinel Value](/errors#the-0-0001-sentinel-value) for handling guidance. Pass the `sport_id` as a path parameter when calling event endpoints: `GET /api/v2/sports/{sportID}/events/{date}`. For market discovery, use `GET /api/v2/sports/{sportID}/markets/{date}` to see which markets have active pricing for a sport on a given date. See [Sport IDs](/reference/sports) for the full list of sport identifiers. Use **WebSocket** when you need real-time updates for live games and your key has WebSocket access (Ultra+ by default). Odds updates are generally sub-second (they come straight from the sportsbooks' own feeds); for supported live games, plays and game-stat changes stream within seconds of each other, and trail the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay. Use **REST polling with delta endpoints** for pre-match monitoring, lower-frequency updates, or as a fallback when the WebSocket disconnects. WebSocket traffic does not increment the HTTP request counter, but pushed messages are still metered as data points. Many production apps use both. See the [Efficient Polling guide](/guides/efficient-polling) for recommended intervals and the [WebSocket reference](/api-reference/v2/websocket) for connection details. Player props are available for NFL, NBA, MLB, NHL, NCAAF, NCAAB, WNBA, UFC, and soccer leagues (IDs 10-19 and 33). CFL and tennis do not currently support player props. Player prop markets require a **Starter plan or higher** — Free keys do not receive them. Use `GET /api/v2/sports/{sportID}/markets/{date}` to check which prop markets are active for a sport on a given day. See the [Market IDs reference](/reference/markets) for prop market IDs like Player Points (29), Player Rebounds (35), and Player Assists (39). ## Data & IDs Soccer leagues (IDs 10-19 and 33) do not use the standard rotation number system that US sportsbooks use for football, basketball, and other sports. Instead, soccer event IDs are generated from the participating team IDs and match date. This means you cannot look up a soccer event by rotation number — use the events-by-date endpoint or search by team ID instead. See [Sports & Coverage](/reference/sports) for more details. Preseason, playoff, and special-event games have their own sport IDs separate from the parent league's regular season. For example, NBA Preseason is sport ID `23`, NBA Playoffs is `24`, and NFL Preseason is `25`. This lets you filter or subscribe to specific parts of a season independently. Season-specific sports share the same data model and endpoints as their parent sport. See the [full list of season-specific IDs](/reference/sports#season-specific-sports). Use `GET /api/v2/sports/{sportID}/futures` — futures (outrights) are served as **competition events** with their own stable `event_id`, separate from the dated game endpoints. Championship winner boards are live for NFL, MLB, NCAAF, NHL, NBA, NCAAB, WNBA, and EPL, plus per-tournament PGA Tour golf (sport ID `40`) and Formula 1 season championships (sport ID `41`). Futures are in **early access** and require an Ultra plan or higher on API keys. See the [Futures guide](/guides/futures) for the data model, window semantics, and the delta polling recipe. Use `GET /api/v2/events/{eventID}/openers` for opening lines and `GET /api/v2/events/{eventID}/closing` for closing lines. For full price history over time, use `GET /api/v2/events/{eventID}/markets/history` or `GET /api/v2/events/{eventID}/markets/{marketID}/history` for chart-ready time series data. These endpoints are available after the event has started or completed. See the [Events reference](/api-reference/v2/events) and [Markets reference](/api-reference/v2/markets) for details. ## Billing & Plans Upgrades take effect **immediately** once payment succeeds. The prorated charge for the remainder of the billing period is invoiced at upgrade time — if the payment fails, the upgrade is not applied and your current plan stays active. As soon as the upgrade completes, your API keys reflect the new tier's data points, rate limits, and feature access with no waiting period. You can schedule a downgrade from your dashboard at any time. Downgrades take effect **at the end of the current billing period** — you keep your current tier's allowance and features until then, and nothing extra is charged when you schedule it. You can cancel a scheduled downgrade any time before the period ends. Yes. Every paid API tier is available on a weekly billing cadence at a premium over the monthly price — useful for covering a single tournament or a few weeks of a season without a month-long commitment. Weekly plans include a proportional weekly share of the tier's monthly data-point allowance (monthly × 12 ÷ 52), metered over the 7-day billing window, with overage at the same per-data-point rates. The `X-Datapoints-Period` header reads `weekly` on these plans. See [Weekly Billing](/rate-limits#weekly-billing). The 7-day trial is available to first-time subscribers only. If you cancel and later re-subscribe, the new subscription starts (and bills) immediately without a trial period. If your account has an outstanding balance (unpaid overage or an open invoice), cancellation is blocked until the balance is settled. Similarly, if a subscription payment has failed and the account is payment-suspended, new API keys cannot be created and plan upgrades are blocked until the payment issue is resolved from your dashboard. ## Integration TheRundown enforces two different limits per API key: **data-point usage** and **requests per second**. Metered responses include headers like `X-Datapoints`, `X-Datapoints-Used`, `X-Datapoints-Remaining`, `X-Tier`, and `X-Rate-Limit`. To stay efficient: use delta endpoints instead of repeated full snapshots, filter by `market_ids` and `affiliate_ids`, cache reference data, and use WebSocket on real-time tiers when you need live updates. When you get a `429`, read `Retry-After` and the billing headers to determine whether you hit a short burst throttle or a usage cap. See [Rate Limits](/rate-limits) and the [Efficient Polling guide](/guides/efficient-polling). Yes. WebSocket traffic does **not** increment the HTTP request counter, but snapshot payloads and pushed updates are still metered as data points. A `game_stats` frame costs one stats data point per changed team or player stat row; a zero-row completion marker or invalidation fallback costs one. Keep subscriptions narrow by filtering to the sports, markets, events, and sportsbooks you actually need. Delta endpoints use cursor-based pagination, and there are two of them with **different, non-interchangeable cursors**: * **`/api/v2/markets/delta`** (price changes) takes an **integer** cursor. Bootstrap it from the integer `meta.delta_last_id` in a `/api/v2/sports/{id}/events/{date}` response, then follow the integer `meta.delta_last_id` returned by each markets-delta response. This is the endpoint for odds polling. * **`/api/v2/delta`** (full event-object changes — status, scores, the whole event) takes an **ordered UUID** cursor (e.g. `11f1-23b3-f4d42784-8057-a3a997572248`) that is only returned by `/api/v2/delta`'s own responses. The events snapshot does not provide it, and passing the integer cursor here returns a `400`. Don't bootstrap with `last_id=0` — cursors that fall too far behind the current head are rejected; always seed from a fresh events snapshot. Each delta entry contains the full updated object, so replace (don't merge) in your local cache. See the [Efficient Polling guide](/guides/efficient-polling#delta-endpoint-bootstrap-flow) for the complete flow. Yes. TheRundown provides a [Model Context Protocol (MCP) server](/mcp) that lets AI assistants like Claude, Cursor, VS Code Copilot, and others search the API documentation directly. The MCP server is **documentation-only**: it helps your assistant find endpoints, parameters, market IDs, and examples, but live sports data still comes from your own API key calling the real API. See the [MCP Server page](/mcp) for setup instructions and example prompts. TheRundown provides official SDKs for [Python](/sdks/python), [JavaScript](/sdks/javascript), and [Go](/sdks/go). These wrap the REST API with typed methods for events, markets, teams, players, and stats. If your language isn't covered, the API is a standard REST interface that works with any HTTP client. # Building an Odds Screen Source: https://docs.therundown.io/guides/building-odds-screen Step-by-step guide to building a real-time odds screen with moneyline, spread, and total columns using TheRundown API and WebSocket. This guide walks through building a complete odds screen from scratch -- fetching sports, loading events, parsing market data, connecting a WebSocket for live updates, and handling price changes in your UI. ## Step 1: Fetch the Sports List Start by loading the list of available sports. This endpoint is public and does not require authentication. ```python Python theme={null} import requests BASE_URL = "https://therundown.io/api/v2" API_KEY = "YOUR_API_KEY" sports_response = requests.get(f"{BASE_URL}/sports") sports = sports_response.json()["sports"] for sport in sports: print(f"{sport['sport_id']}: {sport['sport_name']}") ``` ```javascript JavaScript theme={null} const BASE_URL = "https://therundown.io/api/v2"; const API_KEY = "YOUR_API_KEY"; const sportsResponse = await fetch(`${BASE_URL}/sports`); const { sports } = await sportsResponse.json(); for (const sport of sports) { console.log(`${sport.sport_id}: ${sport.sport_name}`); } ``` Use the sport list to populate a sport selector in your UI. Common sport IDs: | Sport | ID | | ----- | -- | | NFL | 2 | | MLB | 3 | | NBA | 4 | | NCAAB | 5 | | NHL | 6 | ## Step 2: Fetch Events for a Sport and Date Once the user selects a sport, fetch events for that sport on a given date. Include `market_ids=1,2,3` for moneyline, spread, and total. Use `main_line=true` to get only the primary line for each market. Always pass `offset=300` to align the date boundary with US Central Time. Without this, games that tip off late at night may show up under the next day's date. ```python Python theme={null} from datetime import date sport_id = 4 # NBA today = date.today().isoformat() response = requests.get( f"{BASE_URL}/sports/{sport_id}/events/{today}", params={ "key": API_KEY, "market_ids": "1,2,3", "affiliate_ids": "19,23,22", # DraftKings, FanDuel, BetMGM "main_line": "true", "offset": "300", # Central Time } ) events = response.json()["events"] print(f"Found {len(events)} events") ``` ```javascript JavaScript theme={null} const sportId = 4; // NBA const today = new Date().toISOString().split("T")[0]; const params = new URLSearchParams({ key: API_KEY, market_ids: "1,2,3", affiliate_ids: "19,23,22", // DraftKings, FanDuel, BetMGM main_line: "true", offset: "300", // Central Time }); const response = await fetch( `${BASE_URL}/sports/${sportId}/events/${today}?${params}` ); const { events } = await response.json(); console.log(`Found ${events.length} events`); ``` ## Step 3: Discover Available Markets Not every sport or event has the same markets. Before building your odds columns, check which markets actually have data. There are two ways to do this: ### By sport and date Returns all markets with active pricing for a sport on a given date, keyed by sport ID. Use `hide_closed_markets=1` to exclude markets that have been taken off the board. ```bash cURL theme={null} curl "https://therundown.io/api/v2/sports/4/markets/2026-02-12?key=YOUR_API_KEY&offset=300" ``` ```python Python theme={null} markets_response = requests.get( f"{BASE_URL}/sports/{sport_id}/markets/{today}", params={"key": API_KEY, "offset": "300", "hide_closed_markets": "1"} ) for m in markets_response.json().get(str(sport_id), []): print(f"{m['id']:>4} {m['name']:<30} prop={m['proposition']}") ``` ```javascript JavaScript theme={null} const marketsResponse = await fetch( `${BASE_URL}/sports/${sportId}/markets/${today}?key=${API_KEY}&offset=300&hide_closed_markets=1` ); const marketsBySport = await marketsResponse.json(); for (const m of marketsBySport[sportId] || []) { console.log(`${m.id} ${m.name} prop=${m.proposition}`); } ``` ### By event ID Returns only the markets available for a specific event. Useful when building a detail view for a single game. ```bash cURL theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/markets?key=YOUR_API_KEY" ``` ```python Python theme={null} event_markets = requests.get( f"{BASE_URL}/events/{event_id}/markets", params={"key": API_KEY} ).json() for m in event_markets: print(f"{m['id']:>4} {m['name']}") ``` ```javascript JavaScript theme={null} const eventMarkets = await fetch( `${BASE_URL}/events/${eventId}/markets?key=${API_KEY}` ).then((r) => r.json()); for (const m of eventMarkets) { console.log(`${m.id} ${m.name}`); } ``` Use the `event_id` value returned by `GET /api/v2/sports/{sportID}/events/{date}` when calling per-event V2 endpoints. Each market object includes: | Field | Description | | ----------------- | ----------------------------------------------------------------------- | | `id` | Numeric market ID — pass these as `market_ids` when fetching event odds | | `name` | Human-readable name (e.g., "Moneyline", "Player Points") | | `proposition` | `true` for player prop markets, `false` for game-level markets | | `period_id` | Period this market applies to (full game, first half, etc.) | | `live_variant_id` | If set, the corresponding live/in-play market ID | | `description` | Longer description of the market | The three core markets for an odds screen are **1** (Moneyline), **2** (Spread), and **3** (Total). See [Market IDs](/reference/markets) for the full list including player props and live markets. ## Step 4: Parse Markets for Display Each event contains a `markets` array. Index it by `market_id` to pull moneyline (1), spread (2), and total (3) for each sportsbook. ```python Python theme={null} MARKET_MONEYLINE = 1 MARKET_SPREAD = 2 MARKET_TOTAL = 3 def get_price(participant, affiliate_id): """Extract the price from a participant's first line for a given book.""" for line in participant.get("lines", []): p = line.get("prices", {}).get(affiliate_id, {}).get("price") if p == 0.0001: return None, line.get("value") # Off the board return p, line.get("value") return None, None def fmt(price): """Format American odds for display.""" if price is None: return "N/A" return f"+{int(price)}" if price > 0 else str(int(price)) # Build a lookup: market_id -> market object for event in events: away = event["teams"][0]["name"] home = event["teams"][1]["name"] markets = {m["market_id"]: m for m in event.get("markets", [])} # Moneyline ml = markets.get(MARKET_MONEYLINE, {}) ml_prices = {} for p in ml.get("participants", []): price, _ = get_price(p, "19") ml_prices[p["name"]] = fmt(price) # Spread sp = markets.get(MARKET_SPREAD, {}) sp_prices = {} for p in sp.get("participants", []): price, value = get_price(p, "19") sp_prices[p["name"]] = f"{value} ({fmt(price)})" # Total tot = markets.get(MARKET_TOTAL, {}) tot_prices = {} for p in tot.get("participants", []): price, value = get_price(p, "19") side = "O" if p.get("type") == "TYPE_OVER" else "U" tot_prices[side] = f"{value} {fmt(price)}" print(f"\n{away} @ {home}") print(f" ML: {ml_prices.get(away, 'N/A')} / {ml_prices.get(home, 'N/A')}") print(f" Spread: {sp_prices.get(away, 'N/A')} / {sp_prices.get(home, 'N/A')}") print(f" Total: {tot_prices.get('O', 'N/A')} / {tot_prices.get('U', 'N/A')}") ``` ```javascript JavaScript theme={null} const MARKET_MONEYLINE = 1; const MARKET_SPREAD = 2; const MARKET_TOTAL = 3; function getPrice(participant, affiliateId) { const line = participant.lines?.[0]; if (!line) return { price: null, value: null }; const raw = line.prices?.[affiliateId]?.price ?? null; return { price: raw === 0.0001 ? null : raw, value: line.value }; } function fmt(price) { if (price == null) return "N/A"; return price > 0 ? `+${Math.round(price)}` : String(Math.round(price)); } for (const event of events) { const [away, home] = event.teams.map((t) => t.name); // Index markets by ID for direct lookup const markets = Object.fromEntries( (event.markets || []).map((m) => [m.market_id, m]) ); // Moneyline const ml = {}; for (const p of markets[MARKET_MONEYLINE]?.participants || []) { ml[p.name] = fmt(getPrice(p, "19").price); } // Spread const sp = {}; for (const p of markets[MARKET_SPREAD]?.participants || []) { const { price, value } = getPrice(p, "19"); sp[p.name] = `${value} (${fmt(price)})`; } // Total const tot = {}; for (const p of markets[MARKET_TOTAL]?.participants || []) { const { price, value } = getPrice(p, "19"); const side = p.type === "TYPE_OVER" ? "O" : "U"; tot[side] = `${value} ${fmt(price)}`; } console.log(`\n${away} @ ${home}`); console.log(` ML: ${ml[away] || "N/A"} / ${ml[home] || "N/A"}`); console.log(` Spread: ${sp[away] || "N/A"} / ${sp[home] || "N/A"}`); console.log(` Total: ${tot.O || "N/A"} / ${tot.U || "N/A"}`); } ``` ## Step 5: Connect WebSocket for Real-Time Updates Once your initial data is loaded, connect the V2 Markets WebSocket to receive live price updates. Filter by sport to reduce traffic. ```python Python theme={null} import asyncio import json import websockets WS_URL = f"wss://therundown.io/api/v2/ws/markets?key={API_KEY}&sport_ids=4" async def listen_for_updates(): async with websockets.connect(WS_URL) as ws: async for message in ws: msg = json.loads(message) # Skip heartbeat messages if msg.get("meta", {}).get("type") == "heartbeat": continue d = msg["data"] print(f"Update: event={d['event_id']} market={d['market_id']} aff={d['affiliate_id']}") print(f" line={d['line']} price={d['price']} (was {d['previous_price']})") asyncio.run(listen_for_updates()) ``` ```javascript JavaScript theme={null} const WS_URL = `wss://therundown.io/api/v2/ws/markets?key=${API_KEY}&sport_ids=4`; const ws = new WebSocket(WS_URL); ws.onopen = () => { console.log("WebSocket connected"); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); // Skip heartbeat messages if (msg.meta?.type === "heartbeat") return; const d = msg.data; console.log(`Update: event=${d.event_id} market=${d.market_id} aff=${d.affiliate_id}`); console.log(` line=${d.line} price=${d.price} (was ${d.previous_price})`); }; ws.onerror = (error) => { console.error("WebSocket error:", error); }; ws.onclose = () => { console.log("WebSocket closed, reconnecting..."); // Implement reconnection logic here }; ``` ## Step 6: Handle Price Updates in the UI When a WebSocket message arrives, merge the individual price update into your local state. Each message contains a single price change — find the matching event, market, participant, and affiliate, then update the price. ```python Python theme={null} # In-memory state: event_id -> full event dict from the REST API event_state = {e["event_id"]: e for e in events} def apply_price_update(update): """Merge a single WebSocket price update into the local event state.""" event = event_state.get(update["event_id"]) if not event: return market_id = update["market_id"] aff_id = str(update["affiliate_id"]) participant_id = update["normalized_market_participant_id"] new_price = float(update["price"]) for market in event.get("markets", []): if market["market_id"] != market_id: continue for participant in market.get("participants", []): if participant["id"] != participant_id: continue for line in participant.get("lines", []): prices = line.setdefault("prices", {}) prices[aff_id] = { "price": new_price, "is_main_line": update["is_main_line"], "updated_at": update["updated_at"], } print(f"Updated: market={market_id} participant={participant_id} aff={aff_id} price={new_price}") return ``` ```javascript JavaScript theme={null} // In-memory state: eventId -> full event object from the REST API const eventState = new Map(events.map((e) => [e.event_id, e])); function applyPriceUpdate(update) { const event = eventState.get(update.event_id); if (!event) return; const marketId = update.market_id; const affId = String(update.affiliate_id); const participantId = update.normalized_market_participant_id; for (const market of event.markets || []) { if (market.market_id !== marketId) continue; for (const participant of market.participants || []) { if (participant.id !== participantId) continue; for (const line of participant.lines || []) { line.prices[affId] = { price: parseFloat(update.price), is_main_line: update.is_main_line, updated_at: update.updated_at, }; // Re-render the event row in your UI here return; } } } } // Wire up to WebSocket ws.onmessage = (rawMsg) => { const msg = JSON.parse(rawMsg.data); if (msg.meta?.type === "heartbeat") return; applyPriceUpdate(msg.data); }; ``` ## Tips for Production If the WebSocket disconnects, use `GET /api/v2/delta` to fetch event deltas or `GET /api/v2/markets/delta` to fetch market price deltas since your last request. This is much more efficient than refetching the full event list. When a price moves, briefly highlight the cell green (price improved for the bettor) or red (price worsened). This visual cue helps users notice live movement. Always check for `0.0001` before displaying a price. Show "Off Board" or "N/A" instead. See [Sentinel Values](/reference/sentinel-values) for details. The sports and affiliates endpoints return reference data that rarely changes. Cache these responses and refresh once per day to avoid unnecessary API calls. Use `affiliate_ids`, `market_ids`, and `main_line=true` to reduce payload size. Only request the data your UI actually displays. ## Next Steps Deep dive into WebSocket configuration Delta endpoints and cache strategies for when WebSocket isn't available Add player prop markets to your screen Track line movement over time How events, markets, lines, and prices relate Full list of affiliate IDs # Efficient Polling Source: https://docs.therundown.io/guides/efficient-polling Optimize data-point usage with delta endpoints, tight filters, smart polling intervals, and caching strategies. Polling the full events endpoint every few seconds is expensive, burns data points, and can trip your per-second throttle if you scale it across many sports or books. This guide covers how to keep data fresh without paying for or requesting far more than you need. ## Why Polling Efficiency Matters TheRundown usage is not just about request count. The cost of a polling loop depends on: * how many sportsbooks you request * how many markets you include * whether you pull full event payloads or only deltas * how often you refresh A naive loop that fetches full event snapshots every few seconds can waste both data points and burst budget. Delta endpoints solve this by returning **only what changed** since your last request. ## Shrink Every Response First Before you tune polling intervals, make each response smaller. ```bash theme={null} curl -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/sports/4/events/2026-02-26?market_ids=1,2,3&affiliate_ids=19,23&main_line=true&offset=300" ``` Use these levers aggressively: * `market_ids`: biggest control for response size * `affiliate_ids`: only request the books you actually surface * `main_line=true`: skip alternate lines if your product only shows the primary market * event-specific endpoints: if you only care about a handful of games, do not fetch the full slate * `hide_closed_markets=1`: useful during market discovery to avoid off-board clutter ## Delta Endpoint Bootstrap Flow The pattern for using delta endpoints has three stages: ### 1. Fetch the full snapshot Start by loading the complete event data for the sport and date you need. This gives you the initial state and the first delta cursor. ```bash theme={null} curl "https://therundown.io/api/v2/sports/4/events/2026-02-26?key=YOUR_API_KEY&market_ids=1,2,3&affiliate_ids=19,23&main_line=true&offset=300" ``` The response includes `meta.delta_last_id` — save this value. It is an **integer** cursor, and it is the bootstrap cursor for the **`/api/v2/markets/delta`** (price-change) feed. ### 2. Poll the market delta endpoint On each subsequent poll, pass your saved cursor to `/api/v2/markets/delta`. It returns only the prices that changed since that cursor — the most efficient way to keep odds fresh. ```bash theme={null} curl "https://therundown.io/api/v2/markets/delta?key=YOUR_API_KEY&last_id=PREVIOUS_DELTA_LAST_ID&sport_id=4&market_ids=1,2,3" ``` Each response includes its own `meta.delta_last_id` (also an integer) — use that as `last_id` on the next poll. Do **not** bootstrap with `last_id=0`: a cursor that has fallen too far behind the current head is rejected, so always seed from a fresh events snapshot. The two delta endpoints take **different, non-interchangeable cursors**: * **`/api/v2/markets/delta`** (price changes) uses an **integer** cursor. Bootstrap it from the events snapshot's integer `meta.delta_last_id`, then follow the integer cursor in each markets-delta response. This is the endpoint for odds polling. * **`/api/v2/delta`** (full event-object changes: status, scores, the whole event) uses an **ordered UUID** cursor, e.g. `11f1-23b3-f4d42784-8057-a3a997572248`. That cursor is only returned by `/api/v2/delta`'s own responses — the events snapshot does **not** provide it. Passing the integer events cursor to `/api/v2/delta` returns a `400`. For price and odds polling, use `/api/v2/markets/delta`. ### 3. Merge updates into local state Each delta response contains the full updated object — replace (do not partially merge) the corresponding entry in your local cache. Update your cursor to the new `delta_last_id` from the response. Polling futures too? The futures snapshot (`GET /api/v2/sports/{sportID}/futures`) provides the same `meta.delta_last_id` bootstrap cursor, but futures market IDs are **not** in the delta feed's default set — pass them explicitly (e.g. `market_ids=1141`). See the [Futures guide](/guides/futures). ## Choosing Between Event and Market Delta | Endpoint | Returns | Cursor type | Bootstrap source | Best for | | --------------------------- | -------------------------------------------- | ------------ | ------------------------------------------------ | ------------------------------------------------------- | | `GET /api/v2/markets/delta` | Individual price changes only | Integer | Events snapshot `meta.delta_last_id` | Odds-focused apps (recommended for price polling) | | `GET /api/v2/delta` | Full event objects (scores, status, markets) | Ordered UUID | The endpoint's own response `meta.delta_last_id` | Apps that need full event-object changes alongside odds | The market delta is usually the cheapest option because it returns only the specific prices that changed, rather than the entire event object. The cursors are not interchangeable — the integer from the events snapshot bootstraps `markets/delta` only, and `/api/v2/delta` rejects it with a `400`. ## Recommended Polling Intervals Match your polling frequency to your use case. Faster polling uses more data points and increases the chance of hitting your burst limit if you parallelize heavily. | Use Case | Interval | Endpoint | Notes | | ------------------------- | --------- | --------------- | ------------------------------------------ | | Live odds screen | 5–10s | Market delta | Fastest practical interval for REST | | Pre-match odds monitoring | 30–60s | Market delta | Lines move slowly before game time | | Live scores | 15–30s | Event delta | Score updates come in bursts during play | | Pre-match schedules | 5 min | Full events | Schedules rarely change close to game time | | Historical/closing lines | On demand | Openers/closing | Fetch once after the event starts or ends | On a WebSocket-enabled tier, use the [WebSocket endpoint](/api-reference/v2/websocket) instead of polling when you need generally sub-second odds updates or play and game-stat updates at play latency. For supported live games, live data trails the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay. WebSocket traffic does not increment the HTTP request counter, but pushed messages are still metered as data points. ## Cache TTL Recommendations Not all data changes at the same rate. Cache aggressively for reference data and use shorter TTLs for live data. | Data Type | Recommended TTL | Endpoint | | ------------------ | --------------- | -------------------------------------------------- | | Sports list | 24 hours | `GET /api/v2/sports` | | Affiliates list | 24 hours | `GET /api/v2/affiliates` | | Teams | 6 hours | `GET /api/v2/sports/{id}/teams` | | Market definitions | 6 hours | `GET /api/v2/markets` | | Events (pre-match) | 5 minutes | `GET /api/v2/sports/{id}/events/{date}` | | Events (live) | Use delta | `GET /api/v2/delta` or `GET /api/v2/markets/delta` | | Market prices | Real-time | Delta endpoint or WebSocket | ## Staleness Guards Your delta cursor can become stale if you stop polling for an extended period. When this happens, the delta endpoint may return an error or skip events that changed while you were away. **How to detect stale data:** * Track the `updated_at` timestamp on your cached events. If the newest update is more than 5 minutes old during a live game window, your data may be stale. * If a delta response returns an empty result but you know games are in progress, your cursor may have expired. * If you receive an error response from the delta endpoint, re-bootstrap from a full snapshot. **How to recover:** 1. Discard your current delta cursor 2. Fetch a fresh full snapshot from the events endpoint 3. Extract the new `delta_last_id` and resume polling ## Watch Your Usage Headers Every production poller should log and monitor: * `X-Datapoints` * `X-Datapoints-Used` * `X-Datapoints-Remaining` * `X-Datapoints-Reset` * `X-Rate-Limit` That gives you the feedback loop to tune filters and polling intervals before users start hitting limits. ## Code Example: Python Polling Loop This example fetches a full snapshot, then polls the market delta endpoint with automatic fallback to a full refresh when the cursor goes stale. ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE = "https://therundown.io/api/v2" SPORT_ID = 4 MARKET_IDS = "1,2,3" AFFILIATE_IDS = "19,23" POLL_INTERVAL = 5 # seconds # Local cache: event_id -> event data events = {} def fetch_full_snapshot(): """Load the full event list and return the delta cursor.""" resp = requests.get( f"{BASE}/sports/{SPORT_ID}/events/2026-02-26", headers={"X-TheRundown-Key": API_KEY}, params={ "market_ids": MARKET_IDS, "affiliate_ids": AFFILIATE_IDS, "main_line": "true", "offset": "300", } ) data = resp.json() for event in data.get("events", []): events[event["event_id"]] = event cursor = data.get("meta", {}).get("delta_last_id", "0") print(f"Loaded {len(events)} events, cursor={cursor}") return cursor def poll_market_delta(last_id): """Fetch price changes since last_id. Returns new cursor.""" resp = requests.get( f"{BASE}/markets/delta", headers={"X-TheRundown-Key": API_KEY}, params={ "last_id": last_id, "sport_id": SPORT_ID, "market_ids": MARKET_IDS, } ) if resp.status_code != 200: print(f"Delta error {resp.status_code}, re-bootstrapping...") return None # Signal to re-bootstrap data = resp.json() changes = data.get("deltas", []) for change in changes: eid = change.get("event_id") print( f"Price change: {eid} {change.get('market_name')} " f"{change.get('participant_name')} -> {change.get('price')}" ) new_cursor = data.get("meta", {}).get("delta_last_id", last_id) return new_cursor # Bootstrap cursor = fetch_full_snapshot() # Poll loop while True: time.sleep(POLL_INTERVAL) result = poll_market_delta(cursor) if result is None: # Cursor went stale, re-bootstrap cursor = fetch_full_snapshot() else: cursor = result ``` ## WebSocket vs. Polling Decision Guide | Factor | REST Polling | WebSocket | | ------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Latency | 5–60s depending on interval | Odds are generally sub-second; supported live game stats stream within seconds of corresponding play updates (\~15–20s behind on-field action) | | Usage impact | Consumes data points and counts toward HTTP burst limit | Consumes data points but does not increment the HTTP request counter | | Implementation complexity | Simple HTTP requests | Requires connection management, reconnection logic | | Data freshness | As fresh as your poll interval | Real-time | | Reliability | Each request is independent | Must handle disconnects and reconnections | | Best for | Pre-match monitoring, low-frequency updates | Live odds screens, real-time dashboards | Many production applications use **both**: WebSocket for live windows, with delta polling as a fallback when the socket disconnects or for lower-tier keys that do not have WebSocket access. See the [Building an Odds Screen](/guides/building-odds-screen) guide for this pattern in practice. # Futures & Outrights Source: https://docs.therundown.io/guides/futures List championship and tournament-winner boards, page through competitions, and keep futures prices fresh with the markets delta feed. Futures (outrights) are markets on the outcome of an entire competition — who wins the Super Bowl, the Stanley Cup, or this week's PGA Tour event — rather than a single game. TheRundown serves them through a dedicated V2 endpoint, `GET /api/v2/sports/{sportID}/futures`, using the same market → participants → lines → prices shape as game odds. Futures are in **early access**. The endpoint requires an **Ultra plan or higher** on API keys, and coverage is expanding — see [what's covered today](#sports-coverage). Requests on lower tiers return a `403` with an `upgrade_url` (see [Errors](/errors#403-forbidden)). ## Competitions Are Intervals, Not Games A futures event represents a **competition** — a span of time, not a point on the calendar. That changes three things compared to game events: * **Two dates instead of one.** `event_date` is when the competition starts (opening kickoff of the NFL season, Thursday's first tee time), and `settle_by` is the settlement horizon — when the market must have graded (Super Bowl Sunday, the tournament's final round). An NBA championship board opens in October and settles in June. * **A stable `event_id` for the whole competition.** The `event_id` is an opaque hash that stays constant from the day the board opens until settlement. Cache it — it is the public handle for the competition across snapshots and the delta feed. * **No `teams`, no `score`.** A competition has no away/home sides, so futures events carry no `teams` array and no score block. The human-readable identity lives in `schedule.event_name` (e.g. `"NFL Super Bowl Winner (2026 Season)"`). Here is a real (trimmed) NFL championship event: ```json theme={null} { "event_id": "a3d1f9f94f220a45cfd944181569cc46", "sport_id": 2, "event_date": "2026-09-10T00:00:00Z", "settle_by": "2027-02-21T00:00:00Z", "event_status": "STATUS_SCHEDULED", "settlement": {}, "schedule": { "event_name": "NFL Super Bowl Winner (2026 Season)", "season_year": 2026 }, "markets": [ { "id": 4967891, "market_id": 1141, "period_id": 0, "name": "tournament_winner", "market_description": "Tournament Winner", "participants": [ { "id": 61, "type": "TYPE_TEAM", "name": "Buffalo Bills", "lines": [ { "id": "8155509548895b87157633c87df81b3e", "prices": { "22": { "id": "665502535", "price": 1000, "is_main_line": true, "updated_at": "2026-07-29T17:01:46Z" }, "25": { "id": "665857721", "price": 1074, "is_main_line": true, "updated_at": "2026-07-29T18:30:25Z" } } } ] } ] } ] } ``` Season-long team competitions currently report `event_status: "STATUS_SCHEDULED"` for the life of the board — live in-progress status for team seasons is not yet populated on `event_status`, so do not build logic on it. Golf tournaments do transition (`STATUS_SCHEDULED` → `STATUS_IN_PROGRESS` → `STATUS_FINAL`) because each competition spans only a few days. For a season board's live standing, read the [`progress`](#competition-stats-progress) block on the single-competition endpoint instead — its `phase`/`entries` update throughout the season even while `event_status` stays `STATUS_SCHEDULED`. ## Reading the Board Each market is a grid of **participants × sportsbooks**. Every participant (a team, or a golfer — golfers use `TYPE_TEAM` like UFC fighters) carries one line whose `prices` object is keyed by affiliate ID. Prices are American odds, so championship longshots run large: `+50000` is a real price on a 500/1 team. ```bash theme={null} curl "https://therundown.io/api/v2/sports/2/futures?key=YOUR_API_KEY&affiliate_ids=22,25" ``` To render an odds board, walk `markets[].participants[]` and read each participant's `lines[0].prices`: ```python theme={null} import requests resp = requests.get( "https://therundown.io/api/v2/sports/2/futures", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, params={"affiliate_ids": "22,25"}, ) for event in resp.json()["events"]: print(event["schedule"]["event_name"]) for market in event["markets"]: print(f" {market['market_description']} (market {market['market_id']})") for p in market["participants"]: prices = p["lines"][0]["prices"] quotes = ", ".join( f"book {aff}: {q['price']:+}" for aff, q in sorted(prices.items()) ) print(f" {p['name']}: {quotes}") ``` A few field-level notes: * **Snapshot `price` values are numbers** (e.g. `1000` for +1000). The markets delta feed serializes `price` and `previous_price` as strings, so normalize them deliberately when applying deltas. Treat snapshot value `0.0001` as off the board, as everywhere else in the API — see [Sentinel Values](/reference/sentinel-values). * **A price with `closed_at` set** is no longer offered by that book (the golfer missed the cut, the book pulled the number). It is included so you can show the last traded price; filter on `closed_at` absence for a live board. * **Join on `participants[].id`**, not `name` — it is the stable team/golfer identifier. ## The Date Window: Interval Overlap Because competitions are intervals, `from`/`to` filter by **overlap**, not by start date. An event is returned when its `[event_date, settle_by]` span intersects your window: `settle_by >= from` and `event_date <= to`. | You want | Request | Why it works | | ----------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | Everything in flight right now | `GET /api/v2/sports/6/futures` | `from` defaults to **now**; a season future that started months ago still matches because it has not settled yet | | Competitions that settle this month | `?from=2026-08-01&to=2026-08-31&offset=300` | `settle_by` on or after the 1st, started by the 31st | | Settled history for a past window | `?include_settled=true&from=2026-07-01&to=2026-07-25` | see below | Two behaviors worth internalizing: * **`from=today` is not "starts today".** It means "has not finished yet" — that is what makes the default listing "active boards" work. * **`include_settled=true` also lifts the default `from=now`.** Settled competitions' intervals ended in the past, so a default `from` of now would hide exactly the history you asked for. With `include_settled=true` you get settled markets, terminal events, and an unbounded window start — constrain it with explicit `from`/`to` if you only want a slice. Date-only values (`YYYY-MM-DD`) are interpreted in the timezone given by `offset` (minutes from UTC, e.g. `300` for US Central) — the [same convention](/guides/efficient-polling#shrink-every-response-first) as the game endpoints. ## Paging with Cursors Futures paging is **keyset-based** — there is no `skip`/page-number parameter. When more results exist, the response carries `meta.next_cursor`; pass it back verbatim as `cursor`. A response without `next_cursor` is the last page. ```python theme={null} def fetch_all_futures(sport_id, **params): events, cursor = [], None while True: if cursor: params["cursor"] = cursor resp = requests.get( f"https://therundown.io/api/v2/sports/{sport_id}/futures", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, params=params, ) data = resp.json() events.extend(data["events"]) cursor = data.get("meta", {}).get("next_cursor") if not cursor: return events ``` The token is opaque — never parse or construct one. Ordering is deterministic: soonest-to-resolve first (`settle_by`, then `event_date`, then `event_id`), so the first page is always the boards closest to settlement. `limit` defaults to 50 and is capped at 200. ## Keeping Prices Fresh: Snapshot → Delta Futures price changes flow through the same [`/api/v2/markets/delta`](/api-reference/generated/v2-markets/get-market-price-changes-since-a-given-id) feed as game odds, and the futures snapshot hands you the bootstrap cursor directly in `meta.delta_last_id`. Futures market IDs are **not in the delta feed's default market set**. If you poll `/api/v2/markets/delta` without `market_ids`, you will receive game-market changes only and silently miss every futures move. Always pass the futures IDs explicitly, e.g. `market_ids=1141`. The recipe: 1. **Snapshot** — `GET /api/v2/sports/{sportID}/futures`, store the board, save `meta.delta_last_id`. 2. **Poll** — `GET /api/v2/markets/delta?market_ids=1141&last_id=`. Apply each delta row to your board, then save the response's `meta.delta_last_id` for the next poll. 3. **Chain the cursor every poll, even when `deltas` is empty.** The returned watermark advances to the head of the feed regardless of whether any rows matched your filter — an empty response is cheap and keeps your cursor inside the validity window. Delta rows for futures look exactly like game-market deltas. Unlike numeric snapshot prices, their `price` and `previous_price` values are decimal strings: ```json theme={null} { "id": 1595215220, "event_id": "7685a32da628c77e031a2d9701128882", "sport_id": 40, "affiliate_id": 19, "market_id": 1141, "market_name": "tournament_winner", "participant_id": 139667, "participant_type": "TYPE_TEAM", "participant_name": "Stephan Jaeger", "line": "", "price": "8000.0000", "previous_price": "7800.0000", "change_type": "price_change", "updated_at": "2026-08-01T14:39:44Z", "is_main_line": true } ``` Match rows to your cached board by `(event_id, market_id, participant_id, affiliate_id)`. A complete worked loop: ```python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE = "https://therundown.io/api/v2" SPORT_ID = 40 # Golf FUTURES_MARKET_IDS = "1141" boards = {} # event_id -> event def snapshot(): """Load all futures boards and return the delta cursor.""" cursor = None watermark = None while True: params = {"market_ids": FUTURES_MARKET_IDS} if cursor: params["cursor"] = cursor resp = requests.get( f"{BASE}/sports/{SPORT_ID}/futures", headers={"X-TheRundown-Key": API_KEY}, params=params, ) data = resp.json() for event in data["events"]: boards[event["event_id"]] = event watermark = data["meta"]["delta_last_id"] cursor = data["meta"].get("next_cursor") if not cursor: print(f"Loaded {len(boards)} boards, watermark={watermark}") return watermark def poll(last_id): """Apply futures price changes since last_id. Returns the new cursor.""" resp = requests.get( f"{BASE}/markets/delta", headers={"X-TheRundown-Key": API_KEY}, params={"last_id": last_id, "market_ids": FUTURES_MARKET_IDS}, ) if resp.status_code != 200: return None # stale cursor -> re-snapshot data = resp.json() for d in data.get("deltas", []): print(f"{d['participant_name']}: {d['previous_price']} -> {d['price']} " f"(book {d['affiliate_id']})") return data["meta"]["delta_last_id"] # always chain, even on zero rows cursor = snapshot() while True: time.sleep(30) cursor = poll(cursor) or snapshot() ``` Delta cursors do not have a fixed wall-clock lifetime. A cursor is rejected with an HTTP `400` when it falls too far behind the current head; the guard is based on intervening update volume across the platform. On a `400`, re-snapshot the futures endpoint and resume from its fresh `meta.delta_last_id`. Poll every 30 seconds to a few minutes and chain the returned cursor on every response. Empty responses are nearly free and advance your cursor to the head of the stream. ## Futures Market IDs | Market | ID | Sports | Notes | | ------------------ | ---- | ------------------ | --------------------------------------------------------------------------- | | Tournament Winner | 1141 | All futures sports | Championship / outright winner. The one market every futures sport carries. | | Top 5 Finish | 1392 | Golf | Finish position 1–5, ties included | | Top 10 Finish | 1393 | Golf | Finish position 1–10, ties included | | Top 20 Finish | 1394 | Golf | Finish position 1–20, ties included | | Make The Cut | 1395 | Golf | Yes/No per golfer, over/under-carried at line 0.5 | | First Round Leader | 1396 | Golf | Leader after round 1; dead-heat rules on ties | `market_ids` on the futures endpoint is intersected with the futures market set — passing game-market IDs there yields events with empty `markets` arrays (the event list itself still returns). In the other direction, remember that these IDs must be passed **explicitly** on `/api/v2/markets/delta`. ## Sports Coverage Futures are live for the following sports. The number of sportsbooks pricing each board varies by sport while early access expands — check the `prices` keys on a live board rather than assuming a fixed set: | Sport | ID | What runs | | --------------- | -- | --------------------------------------------------------------------- | | NFL | 2 | Super Bowl winner (one board per season) | | MLB | 3 | World Series winner | | NCAAF | 1 | National championship winner | | NHL | 6 | Stanley Cup winner | | NBA | 4 | NBA championship winner | | NCAAB | 5 | National championship winner | | WNBA | 8 | WNBA championship winner | | EPL | 11 | Premier League winner | | Golf (PGA Tour) | 40 | One competition per tournament — winner plus the entry markets above | | Formula 1 | 41 | Two season competitions: Drivers' Champion and Constructors' Champion | Team sports run one championship competition per season. Golf runs a competition per tournament, so expect a rolling list of upcoming events rather than a single board. F1 carries the two season championships only — no per-race markets. ## Settlement Lifecycle A board's life is **open → trade → settle**. The `settlement` object tracks it per market, keyed by market ID: * A fresh board's `settlement` is empty (`{}`) — a market with **no entry has not entered grading**. Entries appear as the competition nears resolution, carrying a pre-grading status: an in-progress golf tournament shows `{ "1141": { "status": "pending" } }`. * Once graded, the entry becomes `"status": "settled"` with `settled_at`, and — for winner-shaped markets — `winning_line` (the winner's canonical name) and `winning_participant_id`. Entry markets like Top 10 or Make The Cut grade per participant, so they settle without a single `winning_line`. * `"settled"` is the only terminal value. Treat any other `status` as "grading in progress" — the vocabulary may grow. A real settled golf competition: ```json theme={null} { "event_id": "f322ba0f4c5993c1f827a1e1b8dbae60", "sport_id": 40, "event_date": "2026-07-23T04:00:00Z", "settle_by": "2026-07-26T04:00:00Z", "event_status": "STATUS_FINAL", "settlement": { "1141": { "status": "settled", "winning_line": "Jackson Koivun", "winning_participant_id": 139644, "settled_at": "2026-07-29T02:37:50Z" }, "1392": { "status": "settled", "settled_at": "2026-07-30T01:41:54Z" }, "1393": { "status": "settled", "settled_at": "2026-07-30T01:41:54Z" }, "1394": { "status": "settled", "settled_at": "2026-07-30T01:41:54Z" }, "1395": { "status": "settled", "settled_at": "2026-07-30T01:41:54Z" }, "1396": { "status": "settled", "settled_at": "2026-07-30T01:41:54Z" } }, "schedule": { "event_name": "3M Open", "season_year": 2026, "league_name": "PGA Tour" } } ``` Settlement changes visibility: * A **settled market** disappears from the default listing (its prices and settlement entry are removed per market). * An event where **every requested market has settled** — or whose status is terminal (final/canceled/abandoned) — disappears entirely. * `include_settled=true` restores all of it, including the graded `settlement` blocks, which is how you fetch results after the fact. ## Fetching One Competition Every entry in a futures listing carries an `event_id`. Fetch that one competition directly with [`GET /api/v2/sports/{sportID}/futures/{eventID}`](/api-reference/generated/v2-futures/get-one-futures-competition) — the same event shape as the listing, plus up to three optional detail-only keys covered below: `progress`, `leaders`, and `player_leaders`. ```bash theme={null} curl "https://therundown.io/api/v2/sports/40/futures/f322ba0f4c5993c1f827a1e1b8dbae60?key=YOUR_API_KEY" ``` This endpoint always serves the competition, even after it settles. The listing hides settled markets and terminal events by default, but a detail fetch is a deep link to a competition you already hold the ID for, and it keeps working after settlement — graded results stay visible in `settlement`. There is no `include_settled` parameter here. An unknown `event_id`, an ID from a different sport, or a game (non-competition) event ID returns a `404`. ## Competition Stats (Progress) Some competitions carry a live, non-odds status document — a leaderboard or a standings table — alongside the price board. It rides in the optional `progress` key on the single-competition endpoint above; the listing endpoint does not carry it. `progress` is **omitted entirely** (never `null`) when no progress document exists yet for the competition. "Priced, no progress" is the normal state for a fresh team-championship board — not a degraded response — so check for the key's presence rather than assuming it is always there. Every `progress` document shares one envelope: ```json theme={null} { "kind": "stroke_play_leaderboard", "as_of": "2026-07-25T18:42:00Z", "phase": { "status": "STATUS_IN_PROGRESS", "label": "Round 3 of 4", "unit": "round", "current": 3, "total": 4 }, "context": { "...": "kind-specific" }, "entries": [ { "...": "kind-specific, ranked" } ] } ``` * **`kind`** picks the shape of `context` and `entries` below. Two kinds are live today: `stroke_play_leaderboard` (golf) and `league_standings` (team-sport championships). * **`phase`** is the same across every kind: `status` uses the same vocabulary as `event_status`, `label` is a ready-to-display phrase (never raw upstream text), and `current`/`total` are `null` when there is no meaningful position — a standings table between games has a `total` but no `current`. * **`entries[].participant_id`** is always the SAME participant ID space `markets[].participants[].id` uses. Joining a progress row directly to a price is the whole point of the block. ### `stroke_play_leaderboard` (golf) A round-3 leaderboard for the same 3M Open shown earlier in [Settlement Lifecycle](#settlement-lifecycle) — Jackson Koivun leads here and goes on to win: ```json theme={null} { "progress": { "kind": "stroke_play_leaderboard", "as_of": "2026-07-25T18:42:00Z", "phase": { "status": "STATUS_IN_PROGRESS", "label": "Round 3 of 4", "unit": "round", "current": 3, "total": 4 }, "context": { "cut_score": -4, "cut_count": 68, "purse": 8400000, "round": 3, "round_state": "in_progress" }, "entries": [ { "participant_id": 139644, "name": "Jackson Koivun", "position": "1", "tied": false, "score_to_par": -14, "today": -4, "thru": 12, "movement": 1, "country": "USA", "total_strokes": 199, "round_state": "active" }, { "participant_id": 139667, "name": "Stephan Jaeger", "position": "2", "tied": false, "score_to_par": -12, "today": -2, "thru": 14, "movement": -1, "country": "GER", "total_strokes": 201, "round_state": "active" } ] } } ``` `context` is round-level state shared by every golfer: `cut_score` and `cut_count` (once the cut is made), `purse`, and the current `round`/`round_state`. Each `entries[]` row is one golfer: `position` (`"1"`; ties render like `"T2"` — check `tied`), `score_to_par`/`today` relative to par, `thru` (holes completed this round, `"F"` once finished, or an ISO-8601 tee time before the round starts), `status` when a golfer is cut or withdraws (`active`/`cut`/`wd`), `total_strokes`, `movement` (position change since the prior update), and `country`. ### `league_standings` (team-sport championships) A mid-season NFL standings snapshot, on the same event as the moneyline board at the top of this guide: ```json theme={null} { "progress": { "kind": "league_standings", "as_of": "2026-08-07T09:05:00Z", "phase": { "status": "STATUS_IN_PROGRESS", "label": "Week 14", "unit": "week", "current": 14, "total": 18 }, "context": {}, "entries": [ { "participant_id": 61, "name": "Buffalo Bills", "rank": 1, "wins": 10, "losses": 3, "ties": 0, "win_pct": 0.769, "points": 0, "conference": "AFC", "division": "AFC East", "seed": 1, "streak": "W3" }, { "participant_id": 63, "name": "New England Patriots", "rank": 3, "wins": 7, "losses": 6, "ties": 0, "win_pct": 0.538, "points": 0, "conference": "AFC", "division": "AFC East", "seed": 6, "streak": "L1" } ] } } ``` Core fields (`participant_id`, `name`, `rank`, `wins`, `losses`, `ties`, `win_pct`, `points`) are always present; sport-specific fields — `conference`, `division`, `group_rank`, `seed`, `games_behind`, `streak`, `clinch`, `points_for`, `points_against`, `home_record`, `away_record`, `last_ten`, and (soccer) `position`, `goal_difference`, `goals_for`, `goals_against` — appear only when applicable. `rank` is league-wide, not scoped to a conference or division. ## Leaders: Curated Season-Stat Leaderboards Two more optional detail-only keys on the single-competition endpoint, `leaders` and `player_leaders`, surface a small, hand-verified set of season-stat leaderboards alongside the odds board — team-grain and player-grain respectively. Both share one shape: ```json theme={null} { "stat": "Total Yards", "stat_id": 1120, "season_year": 2025, "entries": [ { "participant_id": 61, "name": "Buffalo Bills", "value": 6432, "rank": 1 }, { "participant_id": 63, "name": "New England Patriots", "value": 5211, "rank": 2 } ] } ``` * **`stat`** is our own curated display name — never an upstream stat's raw field or column name. **`stat_id`** is our internal identifier, exposed the same way `market_id` is elsewhere. **`season_year`** is the most recent season with data, resolved per request rather than hardcoded. * **`entries`** holds the top 5 ranked rows. Most stats rank highest-value-first; a few (ERA, goals-against average) rank the *lowest* value as `rank: 1` — check the stat before assuming direction. **`leaders`** (team-grain) is competition-scoped: `entries[].participant_id` is the SAME participant ID space `progress` and `markets[].participants[].id` use. Curated today for NFL, NCAAF, NBA, WNBA, NCAAB, MLB, NHL, MLS, EPL, Ligue 1, Bundesliga, La Liga, and Serie A. Win/loss/tie records and standings points are never sourced here — that is `progress`'s `league_standings` job. **`player_leaders`** (player-grain) differs in two ways: it is **league-wide, not competition-scoped** (player season stats carry no team linkage, so entries are the sport's overall leaders for the labeled season, not just this competition's two teams), and `entries[].participant_id` is the **player** ID space — the same normalized player IDs player-prop market participants use, not the team ID space `leaders`/`progress` share. Curated today for NFL, NCAAF, NBA, WNBA, NCAAB, MLB, and NHL; soccer is excluded (no reliable per-player goals/assists season data upstream). ```json theme={null} { "player_leaders": [ { "stat": "Passing Yards", "stat_id": 1101, "season_year": 2025, "entries": [ { "participant_id": 15847, "name": "Josh Allen", "value": 4306, "rank": 1 } ] } ] } ``` Both `leaders` and `player_leaders` are **omitted entirely** (never an empty array) when the sport has no curated stat set, the board has no resolvable team participant, or no season-stat rows exist yet — the same omission contract as `progress`. ## Common Pitfalls `progress` is omitted entirely — never present as `null` — when no progress document exists yet for that competition. This is normal for a freshly opened board or a sport without a collector yet, not an error. Check `leaders`/`player_leaders` and the odds board in the meantime, and poll again later. Futures market IDs are not in the delta feed's default set. Pass them explicitly: `market_ids=1141` (add the golf entry IDs if you track them). This is the single most common futures integration bug. Your cursor fell too far behind the current head. The guard is based on intervening update volume, not a fixed wall-clock expiry. Re-snapshot the futures endpoint, take the fresh `meta.delta_last_id`, and resume. To avoid it, poll every 30 seconds to a few minutes and chain the returned cursor on every response — including empty ones, which still advance the watermark to head. Working as designed: the date window filters by **interval overlap**, and a season future that started in September still overlaps today because it has not settled. `from` bounds `settle_by` ("has not finished"), `to` bounds `event_date` ("has started by"). `include_settled=true` also lifts the implicit `from=now` — otherwise settled history (whose intervals ended in the past) would be unreachable. Add explicit `from`/`to` bounds to scope the history you want. Settled markets and terminal events are hidden by default. The board did not disappear — fetch it with `include_settled=true` to see the graded `settlement` block, including the winner. Expected for team-sport season competitions today — live status transitions for season-long boards are not yet populated. Use `settle_by` and the `settlement` block, not `event_status`, to reason about where a season board stands. ## Billing Futures responses are metered as data points like every other odds endpoint — the standard usage headers (`X-Datapoints`, `X-Datapoints-Used`, `X-Datapoints-Remaining`) apply, and your plan's data delay applies to futures prices as well. Filter with `affiliate_ids` and `market_ids` to keep snapshot costs down, and lean on the delta feed for updates. See [Rate Limits](/rate-limits) and the [Efficient Polling guide](/guides/efficient-polling). # Getting Live Odds Source: https://docs.therundown.io/guides/getting-live-odds Fetch real-time moneylines, spreads, and totals from multiple sportsbooks using the V2 events endpoint. This guide walks through fetching current odds for a sport, understanding the response structure, and filtering by sportsbook and market type. ## Fetching Events with Odds The primary endpoint for odds data is: ``` GET /api/v2/sports/{sportID}/events/{date} ``` Use the `market_ids` parameter to specify which market types to include. The three core markets are: | Market | ID | Description | | ------------------ | -- | ----------------- | | Moneyline | 1 | Who wins the game | | Point Spread | 2 | Handicap / spread | | Total (Over/Under) | 3 | Combined score | Always include `offset=300` to align the date boundary with US Central Time. Without it, games starting late at night may appear under the wrong date. The offset is in minutes (300 = 5 hours). ```bash cURL theme={null} curl "https://therundown.io/api/v2/sports/4/events/2026-02-12?key=YOUR_API_KEY&market_ids=1,2,3&offset=300" ``` ```python Python theme={null} import requests from datetime import date API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", params={ "key": API_KEY, "market_ids": "1,2,3", "offset": "300", # Central Time } ) data = response.json() for event in data["events"]: home = event["teams"][1]["name"] away = event["teams"][0]["name"] print(f"{away} @ {home}") for market in event.get("markets", []): print(f" Market: {market['name']} (ID: {market['market_id']})") for participant in market["participants"]: for line in participant["lines"]: for aff_id, price in line["prices"].items(): print(f" {participant['name']}: {price['price']} ({aff_id})") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const today = new Date().toISOString().split("T")[0]; const response = await fetch( `${BASE_URL}/sports/4/events/${today}?key=${API_KEY}&market_ids=1,2,3&offset=300` ); const data = await response.json(); for (const event of data.events) { console.log(`${event.teams[0].name} @ ${event.teams[1].name}`); for (const market of event.markets || []) { console.log(` Market: ${market.name} (ID: ${market.market_id})`); for (const participant of market.participants) { for (const line of participant.lines) { for (const [affId, price] of Object.entries(line.prices)) { console.log(` ${participant.name}: ${price.price} (${affId})`); } } } } } ``` ## Understanding the Response Structure The V2 response follows a nested hierarchy: ``` event ├── score # Live status, clock, and current score └── markets[] # Array of market types (moneyline, spread, total, etc.) ├── market_id # Numeric market identifier ├── name # Human-readable market name └── participants[] # Teams or players in this market ├── id # Unique participant identifier ├── type # "TYPE_TEAM", "TYPE_PLAYER", or "TYPE_RESULT" ├── name # Participant name (team or player) └── lines[] # Available lines for this participant ├── value # Line value (e.g., "-3.5" for spread, empty for moneyline) └── prices{} # Map of affiliate_id → price object ├── price # American odds (e.g., -110, +150) ├── is_main_line # Whether this is the primary line └── updated_at # When this price was last updated ``` Here is a condensed example response for a single event: ```json theme={null} { "events": [ { "event_id": "abc123", "sport_id": 4, "score": { "event_status": "STATUS_IN_PROGRESS", "score_away": 62, "score_home": 55, "display_clock": "4:32", "game_period": 2, "event_status_detail": "4:32 - 2nd", "updated_at": "2026-02-12T18:30:00Z" }, "teams": [ { "team_id": 1, "name": "Boston Celtics" }, { "team_id": 2, "name": "Los Angeles Lakers" } ], "markets": [ { "market_id": 1, "name": "Moneyline", "period_id": 0, "participants": [ { "id": 1, "type": "TYPE_TEAM", "name": "Boston Celtics", "lines": [ { "value": "", "prices": { "19": { "price": -150, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" }, "23": { "price": -145, "is_main_line": true, "updated_at": "2026-02-12T18:28:00Z" } } } ] }, { "id": 2, "type": "TYPE_TEAM", "name": "Los Angeles Lakers", "lines": [ { "value": "", "prices": { "19": { "price": 130, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" }, "23": { "price": 125, "is_main_line": true, "updated_at": "2026-02-12T18:28:00Z" } } } ] } ] }, { "market_id": 2, "name": "Point Spread", "period_id": 0, "participants": [ { "id": 1, "type": "TYPE_TEAM", "name": "Boston Celtics", "lines": [ { "value": "-3.5", "prices": { "19": { "price": -110, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" } } } ] } ] } ] } ] } ``` Use `score.event_status` to identify live games, `score.display_clock` and `score.game_period` for the on-screen state, and `score.updated_at` to judge freshness. Some live score metadata, especially `venue_name` and `venue_location`, may be empty strings until the upstream feed provides them. ## Filtering by Sportsbook Use the `affiliate_ids` parameter to limit results to specific sportsbooks. This reduces payload size and focuses on the books you care about. ```bash theme={null} # Only DraftKings (19) and FanDuel (23) curl "https://therundown.io/api/v2/sports/4/events/2026-02-12?\ key=YOUR_API_KEY&market_ids=1,2,3&affiliate_ids=19,23&offset=300" ``` Common affiliate IDs: | Sportsbook | ID | | ------------ | -- | | DraftKings | 19 | | FanDuel | 23 | | BetMGM | 22 | | theScore Bet | 24 | | Pinnacle | 3 | See [Sportsbook IDs](/reference/sportsbooks) for the full list. ## Main Lines vs. Alternates By default, the API returns both main lines and alternate lines. Alternate lines are additional spread/total values offered by sportsbooks (e.g., -2.5, -3.0, -3.5, -4.0 for a spread market). To fetch only the primary line for each market, add `main_line=true`: ```bash theme={null} curl "https://therundown.io/api/v2/sports/4/events/2026-02-12?\ key=YOUR_API_KEY&market_ids=1,2,3&main_line=true&offset=300" ``` When `main_line=true` is set, each participant will have at most one line object per market, and the `is_main_line` field on each price will be `true`. This is recommended for odds screens where you only need the consensus line. ## Handling the 0.0001 Sentinel Value A price value of `0.0001` means the line is **off the board** -- the sportsbook has temporarily removed it. This commonly happens when: * A key injury is being evaluated * The book is recalculating after sharp action * The market is approaching game time suspension Never display `0.0001` to end users or use it in calculations. Show "Off Board" or "N/A" instead. ```python Python theme={null} def format_price(price_value): """Format a price for display, handling sentinel values.""" if price_value == 0.0001: return "Off Board" if price_value > 0: return f"+{int(price_value)}" return str(int(price_value)) # Usage for event in data["events"]: for market in event.get("markets", []): for participant in market["participants"]: for line in participant["lines"]: for aff_id, price_obj in line["prices"].items(): display = format_price(price_obj["price"]) print(f"{participant['name']}: {display}") ``` ```javascript JavaScript theme={null} function formatPrice(priceValue) { if (priceValue === 0.0001) return "Off Board"; if (priceValue > 0) return `+${Math.round(priceValue)}`; return String(Math.round(priceValue)); } // Usage for (const event of data.events) { for (const market of event.markets || []) { for (const participant of market.participants) { for (const line of participant.lines) { for (const [affId, priceObj] of Object.entries(line.prices)) { const display = formatPrice(priceObj.price); console.log(`${participant.name}: ${display}`); } } } } } ``` ## Full Working Example Here is a complete example that fetches NBA odds, filters to DraftKings and FanDuel, and prints a formatted table: ```python Python theme={null} import requests from datetime import date API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" BOOKS = {"19": "DraftKings", "23": "FanDuel"} response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", params={ "key": API_KEY, "market_ids": "1,2,3", "affiliate_ids": "19,23", "main_line": "true", "offset": "300", # Central Time } ) data = response.json() def fmt(price): if price == 0.0001: return "N/A" return f"+{int(price)}" if price > 0 else str(int(price)) for event in data["events"]: away = event["teams"][0]["name"] home = event["teams"][1]["name"] print(f"\n{'=' * 60}") print(f"{away} @ {home}") print(f"{'=' * 60}") for market in event.get("markets", []): print(f"\n {market['name']}:") for participant in market["participants"]: for line in participant["lines"]: line_str = f" ({line['value']})" if line.get("value") else "" prices_str = " ".join( f"{BOOKS.get(aid, aid)}: {fmt(p['price'])}" for aid, p in line["prices"].items() ) print(f" {participant['name']}{line_str}: {prices_str}") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const today = new Date().toISOString().split("T")[0]; const BOOKS = { "19": "DraftKings", "23": "FanDuel" }; const params = new URLSearchParams({ key: API_KEY, market_ids: "1,2,3", affiliate_ids: "19,23", main_line: "true", offset: "300", // Central Time }); const response = await fetch(`${BASE_URL}/sports/4/events/${today}?${params}`); const data = await response.json(); function fmt(price) { if (price === 0.0001) return "N/A"; return price > 0 ? `+${Math.round(price)}` : String(Math.round(price)); } for (const event of data.events) { const away = event.teams[0].name; const home = event.teams[1].name; console.log(`\n${"=".repeat(60)}`); console.log(`${away} @ ${home}`); console.log(`${"=".repeat(60)}`); for (const market of event.markets || []) { console.log(`\n ${market.name}:`); for (const participant of market.participants) { for (const line of participant.lines) { const lineStr = line.value ? ` (${line.value})` : ""; const prices = Object.entries(line.prices) .map(([aid, p]) => `${BOOKS[aid] || aid}: ${fmt(p.price)}`) .join(" "); console.log(` ${participant.name}${lineStr}: ${prices}`); } } } } ``` ## Next Steps Turn this data into a full UI Get real-time updates instead of polling Delta endpoints, cache TTLs, and polling intervals How events, markets, lines, and prices relate Track line movement over time All available market types # Historical Odds & Line Movement Source: https://docs.therundown.io/guides/historical-odds Retrieve opening lines, closing lines, and full price history to build line movement charts and analyze odds changes over time. TheRundown API provides several endpoints for accessing historical odds data. You can retrieve full price history for charting, compare opening and closing lines, and filter by time range. ## Endpoints Overview | Endpoint | Description | | --------------------------------------------------------- | -------------------------------------------------------- | | `GET /api/v2/events/{eventID}/markets/history` | Full price history across all markets for an event | | `GET /api/v2/events/{eventID}/markets/{marketID}/history` | Price history for a specific market (ideal for charting) | | `GET /api/v2/events/{eventID}/openers` | Opening lines for a single event | | `GET /api/v2/events/{eventID}/closing` | Closing lines for a single event | | `GET /api/v2/sports/{sportID}/openers/{date}` | Opening lines for all events in a sport on a date | | `GET /api/v2/sports/{sportID}/closing/{date}` | Closing lines for all events in a sport on a date | ## Full Market History Use the full history endpoint to get every recorded price change for an event across all markets and sportsbooks. ```bash theme={null} curl "https://therundown.io/api/v2/events/{eventID}/markets/history?\ key=YOUR_API_KEY&affiliate_ids=19,23" ``` ### Query Parameters | Parameter | Type | Description | | --------------- | ------ | ------------------------------------------------------------ | | `affiliate_ids` | string | Comma-separated sportsbook IDs to include | | `market_ids` | string | Comma-separated market IDs to filter | | `from` | string | Start time in RFC 3339 format (e.g., `2026-02-10T00:00:00Z`) | | `to` | string | End time in RFC 3339 format (e.g., `2026-02-12T23:59:59Z`) | ### Example Response ```json theme={null} { "meta": { "event_id": "abc123", "count": 2 }, "history": [ { "id": 98001, "market_line_price_id": 50001, "event_id": "abc123", "sport_id": 4, "affiliate_id": 19, "market_participant_id": 7001, "market_id": 2, "line": "-3.5", "price": "-110", "change_type": "initial", "updated_at": "2026-02-10T14:30:00Z" }, { "id": 98002, "market_line_price_id": 50001, "event_id": "abc123", "sport_id": 4, "affiliate_id": 19, "market_participant_id": 7001, "market_id": 2, "line": "-4", "price": "-110", "previous_price": "-105", "change_type": "price", "updated_at": "2026-02-11T09:15:00Z" } ] } ``` ## Single Market History (Chart Data) For building a line movement chart, fetch history for a specific market. This returns a chart-optimized response with `series` grouped by sportsbook (keyed by affiliate ID), where each data point uses shorthand fields: `t` (timestamp), `p` (price as a string), and `c` (closed\_at). ```bash theme={null} # Spread history (market_id=2) for a specific event curl "https://therundown.io/api/v2/events/{eventID}/markets/2/history?\ key=YOUR_API_KEY&affiliate_ids=19" ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" EVENT_ID = "abc123" # Fetch spread history from DraftKings (affiliate_id 19) response = requests.get( f"{BASE_URL}/events/{EVENT_ID}/markets/2/history", params={ "key": API_KEY, "affiliate_ids": "19", } ) data = response.json() series = data["series"] # Map keyed by affiliate ID print("Affiliate | Timestamp | Price") print("-" * 65) for aff_id, aff_series in series.items(): for point in aff_series["data"]: print(f"{aff_series['affiliate_name']:>20} | {point['t']} | {point['p']}") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const EVENT_ID = "abc123"; const params = new URLSearchParams({ key: API_KEY, affiliate_ids: "19", }); const response = await fetch( `${BASE_URL}/events/${EVENT_ID}/markets/2/history?${params}` ); const { series } = await response.json(); // series is a map keyed by affiliate ID (string) console.log("Affiliate | Timestamp | Price"); console.log("-".repeat(65)); for (const [affId, affSeries] of Object.entries(series)) { for (const point of affSeries.data) { console.log(`${affSeries.affiliate_name} | ${point.t} | ${point.p}`); } } ``` ## Filtering by Time Range Use `from` and `to` parameters in RFC 3339 format to scope history to a specific window. This is useful for showing line movement in the last 24 hours or during a specific period. ```bash theme={null} # History from the last 24 hours curl "https://therundown.io/api/v2/events/{eventID}/markets/2/history?\ key=YOUR_API_KEY&affiliate_ids=19\ &from=2026-02-11T00:00:00Z\ &to=2026-02-12T00:00:00Z" ``` ```python Python theme={null} from datetime import datetime, timedelta, timezone now = datetime.now(timezone.utc) yesterday = now - timedelta(days=1) response = requests.get( f"{BASE_URL}/events/{EVENT_ID}/markets/2/history", params={ "key": API_KEY, "affiliate_ids": "19", "from": yesterday.strftime("%Y-%m-%dT%H:%M:%SZ"), "to": now.strftime("%Y-%m-%dT%H:%M:%SZ"), } ) data = response.json() series = data["series"] total_points = sum(len(s["data"]) for s in series.values()) print(f"Found {total_points} price changes in the last 24 hours") ``` ```javascript JavaScript theme={null} const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const params = new URLSearchParams({ key: API_KEY, affiliate_ids: "19", from: yesterday.toISOString(), to: now.toISOString(), }); const response = await fetch( `${BASE_URL}/events/${EVENT_ID}/markets/2/history?${params}` ); const { series } = await response.json(); const totalPoints = Object.values(series).reduce((sum, s) => sum + s.data.length, 0); console.log(`Found ${totalPoints} price changes in the last 24 hours`); ``` ## Opening Lines The openers endpoints return the first price posted by each sportsbook for each market. The response uses the same V2 events structure (with `markets`, `participants`, `lines`, and `prices`). ```bash All NBA openers for a date theme={null} curl "https://therundown.io/api/v2/sports/4/openers/2026-02-12?\ key=YOUR_API_KEY&market_ids=1,2,3&affiliate_ids=19,23&offset=300" ``` ```bash Single event openers theme={null} curl "https://therundown.io/api/v2/events/{eventID}/openers?\ key=YOUR_API_KEY&market_ids=1,2,3&affiliate_ids=19,23" ``` ```python Python theme={null} # All openers for a sport + date response = requests.get( f"{BASE_URL}/sports/{sport_id}/openers/{today}", params={ "key": API_KEY, "market_ids": "1,2,3", "affiliate_ids": "19,23", "offset": "300", } ) # Response is { "events": [ { "markets": [...] } ] } for event in response.json().get("events", []): away = event["teams"][0]["name"] home = event["teams"][1]["name"] print(f"\n{away} @ {home}") for market in event.get("markets", []): print(f" {market['name']} (opened)") for participant in market["participants"]: for line in participant["lines"]: for aff_id, price in line["prices"].items(): print(f" {participant['name']}: {line['value']} {price['price']}") ``` ```javascript JavaScript theme={null} // All openers for a sport + date const response = await fetch( `${BASE_URL}/sports/${sportId}/openers/${today}?${new URLSearchParams({ key: API_KEY, market_ids: "1,2,3", affiliate_ids: "19,23", offset: "300", })}` ); const { events } = await response.json(); for (const event of events) { const [away, home] = event.teams.map((t) => t.name); console.log(`\n${away} @ ${home}`); for (const market of event.markets || []) { console.log(` ${market.name} (opened)`); for (const participant of market.participants) { for (const line of participant.lines) { for (const [affId, price] of Object.entries(line.prices)) { console.log(` ${participant.name}: ${line.value} ${price.price}`); } } } } } ``` ## Closing Lines The closing endpoint returns the final price posted before game time. The response uses the same V2 events structure as openers. Closing lines are widely considered the most efficient odds and are useful for evaluating betting performance. ```bash All NBA closing lines for a date theme={null} curl "https://therundown.io/api/v2/sports/4/closing/2026-02-12?\ key=YOUR_API_KEY&market_ids=1,2,3&affiliate_ids=19,23&offset=300" ``` ```bash Single event closing lines theme={null} curl "https://therundown.io/api/v2/events/{eventID}/closing?\ key=YOUR_API_KEY&market_ids=1,2,3&affiliate_ids=19,23" ``` ```python Python theme={null} # All closing lines for a sport + date response = requests.get( f"{BASE_URL}/sports/{sport_id}/closing/{today}", params={ "key": API_KEY, "market_ids": "1,2,3", "affiliate_ids": "19,23", "offset": "300", } ) # Response is { "events": [ { "markets": [...] } ] } for event in response.json().get("events", []): away = event["teams"][0]["name"] home = event["teams"][1]["name"] print(f"\n{away} @ {home}") for market in event.get("markets", []): print(f" {market['name']} (closing)") for participant in market["participants"]: for line in participant["lines"]: for aff_id, price in line["prices"].items(): print(f" {participant['name']}: {line['value']} {price['price']}") ``` ```javascript JavaScript theme={null} // All closing lines for a sport + date const response = await fetch( `${BASE_URL}/sports/${sportId}/closing/${today}?${new URLSearchParams({ key: API_KEY, market_ids: "1,2,3", affiliate_ids: "19,23", offset: "300", })}` ); const { events } = await response.json(); for (const event of events) { const [away, home] = event.teams.map((t) => t.name); console.log(`\n${away} @ ${home}`); for (const market of event.markets || []) { console.log(` ${market.name} (closing)`); for (const participant of market.participants) { for (const line of participant.lines) { for (const [affId, price] of Object.entries(line.prices)) { console.log(` ${participant.name}: ${line.value} ${price.price}`); } } } } } ``` ## Building a Line Movement Chart Here is a complete example that fetches spread history and formats the data for a charting library. The chart endpoint returns `series` as a map keyed by affiliate ID, with each entry containing an `affiliate_name` and `data` array of `{t, p, c}` points. ```python Python theme={null} import requests from datetime import datetime, timezone API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" EVENT_ID = "abc123" # Fetch spread history for multiple books response = requests.get( f"{BASE_URL}/events/{EVENT_ID}/markets/2/history", params={ "key": API_KEY, "affiliate_ids": "19,23", } ) data = response.json() series = data["series"] # Map keyed by affiliate ID (string) # Print chart data — series is already grouped by sportsbook for aff_id, aff_series in series.items(): book_name = aff_series["affiliate_name"] print(f"\n{book_name} (affiliate {aff_id}) Spread Movement:") for point in aff_series["data"]: closed = " [CLOSED]" if point.get("c") else "" print(f" {point['t']}: price={point['p']}{closed}") # To use with matplotlib: # import matplotlib.pyplot as plt # import matplotlib.dates as mdates # # fig, ax = plt.subplots(figsize=(12, 6)) # for aff_id, aff_series in series.items(): # dates = [datetime.fromisoformat(p["t"].replace("Z", "+00:00")) for p in aff_series["data"]] # prices = [float(p["p"]) for p in aff_series["data"]] # ax.step(dates, prices, where="post", label=aff_series["affiliate_name"]) # ax.set_xlabel("Time") # ax.set_ylabel("Price") # ax.legend() # plt.title("Spread Price Movement") # plt.show() ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const EVENT_ID = "abc123"; const params = new URLSearchParams({ key: API_KEY, affiliate_ids: "19,23", }); const response = await fetch( `${BASE_URL}/events/${EVENT_ID}/markets/2/history?${params}` ); const { series } = await response.json(); // series is a map keyed by affiliate ID (string), already grouped by sportsbook // Format for a charting library (e.g., Chart.js, Recharts) const chartDatasets = Object.entries(series).map(([affId, affSeries]) => ({ label: affSeries.affiliate_name, data: affSeries.data.map((p) => ({ x: new Date(p.t), y: parseFloat(p.p), })), })); console.log("Chart datasets:", JSON.stringify(chartDatasets, null, 2)); // To use with Chart.js: // new Chart(ctx, { // type: "line", // data: { datasets: chartDatasets }, // options: { // scales: { // x: { type: "time" }, // y: { reverse: true, title: { text: "Price" } }, // }, // }, // }); ``` ## Comparing Openers to Current Lines A common use case is showing how far a line has moved from its opener. Fetch both the opener and current odds, then compute the difference. Both endpoints return the same V2 events structure with `markets` > `participants` > `lines` > `prices`. ```python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" EVENT_ID = "abc123" # Fetch opener and current data opener_resp = requests.get( f"{BASE_URL}/events/{EVENT_ID}/openers", params={"key": API_KEY, "market_ids": "2", "affiliate_ids": "19"} ) current_resp = requests.get( f"{BASE_URL}/events/{EVENT_ID}", params={"key": API_KEY, "market_ids": "2", "affiliate_ids": "19"} ) # Both return { "events": [ { "markets": [...] } ] } opener_data = opener_resp.json() current_data = current_resp.json() def extract_prices(data): """Extract participant prices from V2 events response.""" results = {} for event in data.get("events", []): for market in event.get("markets", []): for participant in market["participants"]: for line in participant["lines"]: for aff_id, price_obj in line["prices"].items(): key = (participant["id"], participant["name"]) results[key] = { "value": line["value"], "price": price_obj["price"], "is_main_line": price_obj["is_main_line"], } return results opener_prices = extract_prices(opener_data) current_prices = extract_prices(current_data) print("Opening vs Current Spread:") for (pid, name), opener in opener_prices.items(): current = current_prices.get((pid, name)) if current: print(f" {name}: opened {opener['value']} ({opener['price']}) -> now {current['value']} ({current['price']})") ``` ## Next Steps Fetch current odds for today's games Historical data for prop markets too Full list of market types All tracked sportsbooks # Multiplexed WebSocket Source: https://docs.therundown.io/guides/multiplexed-websocket Subscribe to markets, scores, play-by-play, live game stats, and live game state over a single connection to /api/v2/ws. The multiplexed WebSocket at `wss://therundown.io/api/v2/ws` carries multiple logical channels — `markets`, `scores`, `plays`, `stats`, and `live` — over one connection. Instead of encoding filters in the connection URL, you send JSON subscribe messages after connecting, and you can add or remove subscriptions at any time without reconnecting. WebSocket access is enabled on real-time API tiers (Ultra and above) by default. The `plays`, `stats`, and `live` channels additionally require the live game state entitlement, which is also included from Ultra up — see [Rate Limits](/rate-limits#current-api-tier-defaults). Each plan also caps concurrent connections and subscriptions per connection — see the [connection limits table](/api-reference/v2/websocket#overview). ## When to Use the Multiplexed Endpoint | If you need... | Use | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | Market price updates only, with a fixed set of filters | [`/api/v2/ws/markets`](/api-reference/v2/websocket#markets-websocket) — simpler, filters via query parameters | | Scores, play-by-play, live game stats, or live game state streams | `/api/v2/ws` — these channels are only available here | | Multiple feeds with independent filters | `/api/v2/ws` — one subscription per feed, one connection | | To change filters at runtime | `/api/v2/ws` — unsubscribe and resubscribe without reconnecting | ## Connect Authenticate with the `key` query parameter, the same as the dedicated markets stream: ``` wss://therundown.io/api/v2/ws?key=YOUR_API_KEY ``` The connection starts with no subscriptions. Until you subscribe, the only messages you receive are [heartbeats](/api-reference/v2/websocket#heartbeat) every 15 seconds. ## Subscribe to a Channel Send a `subscribe` action with a client-chosen `id`, the `channel`, and optional `params` filters: ```json theme={null} { "action": "subscribe", "id": "nba-markets", "channel": "markets", "params": { "sport_ids": [4], "market_ids": [1, 2, 3] } } ``` | Field | Required | Description | | --------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `action` | Yes | `"subscribe"`, `"unsubscribe"`, or `"snapshot"` (see [Request a Snapshot](#request-a-snapshot)) | | `id` | Yes | Client-chosen identifier, unique among your active subscriptions. Every message for this subscription is tagged with it | | `channel` | `subscribe` only | `"markets"`, `"scores"`, `"plays"`, `"stats"`, or `"live"` | | `params` | No (`subscribe` only) | Filters for this subscription. Without filters, you receive everything the channel carries | The server acknowledges each successful subscription: ```json theme={null} { "type": "subscribed", "id": "nba-markets", "sequence": 12, "message": "subscribed to markets" } ``` ### Channels and Filters | Channel | Delivers | `params` filters | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `markets` | Market price updates — same payloads as [`/api/v2/ws/markets`](/api-reference/v2/websocket#markets-websocket) | `sport_ids`, `event_ids`, `market_ids`, `affiliate_ids` | | `scores` | Score and event-status updates (no live game state fields) | `sport_ids`, `event_ids` | | `plays` | Play-by-play deltas for live games (**Ultra plan or higher**) | `sport_ids`, `event_ids` | | `stats` | Changed team and player box-score rows for live games (**Ultra plan or higher**) | `sport_ids`, `event_ids` | | `live` | Everything in-game on one subscription: score/status deltas including `live_game_state`, play-by-play deltas, and game-stat deltas (**Ultra plan or higher**). `live_game_state` and `game_state` are accepted aliases | `sport_ids`, `event_ids` | Unlike REST query parameters, `params` values are JSON arrays, not comma-separated strings: numbers for `sport_ids`, `market_ids`, and `affiliate_ids` (e.g., `[4, 6]`), strings for `event_ids`. ## Handle Incoming Messages Channel data arrives wrapped in a delta envelope tagged with your subscription `id`. Route messages on the top-level `type` field, then on `id`: ```json theme={null} { "type": "delta", "id": "nba-markets", "sequence": 42, "sub_sequence": 7, "delta_last_id": "...", "data": { "meta": { "type": "market_price", "version": "v2", "timestamp": 1772495104 }, "data": { "event_id": "9b9d0cf6007fdaeb15c3a1888dcfd5df", "affiliate_id": 26, "market_id": 3, "line": "1.5", "price": "-117", "previous_price": "-122.0000", "price_delta": 5, "is_main_line": true, "sport_id": 7, "updated_at": "2026-03-02T23:44:44Z" } } } ``` | Field | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `type` | `"delta"` for channel data. Other top-level types include `"subscribed"`, `"snapshot"`, `"snapshot_complete"`, `"snapshot_error"`, and `"error"` | | `id` | The subscription `id` you chose when subscribing — use it to route the message | | `sequence` | Server-assigned sequence number for messages on this connection | | `sub_sequence` | Message counter within this subscription | | `delta_last_id` | Cursor of the underlying delta feed for this change | | `data` | The channel payload — an object with its own `meta` and `data`, in the same format as the dedicated stream for that channel | Heartbeats arrive in the same format as on the dedicated markets stream — `{"meta": {"type": "heartbeat"}, ...}` — and are not tagged with a subscription `id`. For the full payload field reference per channel, see the [WebSocket reference](/api-reference/v2/websocket#message-format); play-by-play payloads are documented under [Play messages](/api-reference/v2/websocket#play-messages), and box-score deltas under [Game stats messages](/api-reference/v2/websocket#game-stats-messages). ## Request a Snapshot Deltas only tell you what changed — to start from current state, add `"snapshot": true` to `params` when subscribing. Snapshot requests need a bounded scope: `event_ids`, or `sport_ids` plus `date` (`plays` snapshots require `event_ids`): ```json theme={null} { "action": "subscribe", "id": "nba-markets", "channel": "markets", "params": { "sport_ids": [4], "date": "2026-07-19", "market_ids": [1, 2, 3], "snapshot": true } } ``` After the `subscribed` ack, the server sends one or more `snapshot` frames followed by `snapshot_complete`; live deltas begin after that. An active subscription can also request a fresh snapshot at any time without reconnecting: ```json theme={null} { "action": "snapshot", "id": "nba-markets" } ``` Snapshot frames are metered as data points by category, the same as the equivalent REST reads. See [Snapshots](/api-reference/v2/websocket#snapshots) in the reference for per-channel snapshot contents. `stats` is a delta-only channel and does not support snapshots, resync, resume, or replay. Setting `snapshot: true` on an initial `stats` subscribe returns `snapshot_error` with code `unsupported_snapshot` and does not leave that subscription active; subscribe again without `snapshot`. A `live` snapshot also does not include a game-stat baseline. Bootstrap and recover game stats through the two REST event-stat endpoints as described in [Streaming Live Game Stats](#streaming-live-game-stats). ## Unsubscribe Send an `unsubscribe` action with the subscription's `id`: ```json theme={null} { "action": "unsubscribe", "id": "nba-markets" } ``` To change a subscription's filters, unsubscribe and subscribe again. If your application cannot tolerate a gap between the two, subscribe with the new filters under a **different** `id` first, then unsubscribe the old one — you may briefly receive duplicate messages while both are active, so deduplicate during the overlap. ## Handle Errors Errors are returned as `{"type": "error", "id": "...", "code": "...", "message": "..."}`: | Code | Meaning | What to do | | -------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `forbidden` | Subscribing to `plays`, `stats`, or `live` without the live game state entitlement (Ultra plan or higher) | Upgrade your plan, or drop the gated subscription | | `invalid_channel` | Unknown `channel` value | Use `markets`, `scores`, `plays`, `stats`, or `live` | | `missing_id` | Subscribe action sent without an `id` | Include a client-chosen `id` | | `duplicate_id` | An active subscription already uses this `id` | Pick a different `id`, or unsubscribe the existing one first | | `subscription_limit` | Your plan's concurrent subscription cap was reached | Unsubscribe an existing subscription, or consolidate filters into fewer subscriptions | | `buffer_overflow` | A subscription or fan-out queue could not accept a live frame; this connection will close | Reconnect, re-send all subscriptions, and recover current state from REST before applying new deltas | `buffer_overflow` is a connection-level error and can omit `id`. When possible, the server sends it immediately before closing with reason `buffer_overflow:reconnect_and_catchup`. ## Complete Client Example A client that subscribes to NBA market prices and scores on one connection, routes messages by subscription `id`, and resubscribes automatically after a reconnect: ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const WS_URL = `wss://therundown.io/api/v2/ws?key=${API_KEY}`; const SUBSCRIPTIONS = [ { action: "subscribe", id: "nba-markets", channel: "markets", params: { sport_ids: [4], market_ids: [1, 2, 3] }, }, { action: "subscribe", id: "nba-scores", channel: "scores", params: { sport_ids: [4] }, }, ]; function handleDelta(msg) { const payload = msg.data; // { meta: {...}, data: {...} } switch (msg.id) { case "nba-markets": { const d = payload.data; console.log( `Price: event=${d.event_id} market=${d.market_id} ${d.price} (was ${d.previous_price})` ); break; } case "nba-scores": console.log("Score update:", payload.data); break; } } function connect() { const ws = new WebSocket(WS_URL); ws.onopen = () => { console.log("Connected — subscribing"); // Subscriptions only live as long as the connection, // so re-send them on every (re)connect SUBSCRIPTIONS.forEach((sub) => ws.send(JSON.stringify(sub))); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.meta?.type === "heartbeat") return; switch (msg.type) { case "subscribed": console.log(`Subscription ${msg.id} active: ${msg.message}`); break; case "delta": handleDelta(msg); break; case "error": console.error(`Subscription error [${msg.code}]: ${msg.message}`); break; case "snapshot_error": console.error(`Snapshot error [${msg.code}]: ${msg.message}`); break; } }; ws.onclose = () => { console.log("Disconnected, reconnecting in 3s..."); setTimeout(connect, 3000); }; ws.onerror = (error) => { console.error("WebSocket error:", error); ws.close(); }; } connect(); ``` ```python Python theme={null} import asyncio import json import websockets API_KEY = "YOUR_API_KEY" WS_URL = f"wss://therundown.io/api/v2/ws?key={API_KEY}" SUBSCRIPTIONS = [ { "action": "subscribe", "id": "nba-markets", "channel": "markets", "params": {"sport_ids": [4], "market_ids": [1, 2, 3]}, }, { "action": "subscribe", "id": "nba-scores", "channel": "scores", "params": {"sport_ids": [4]}, }, ] def handle_delta(msg): payload = msg["data"] # {"meta": {...}, "data": {...}} if msg["id"] == "nba-markets": d = payload["data"] print( f"Price: event={d['event_id']} market={d['market_id']}" f" {d['price']} (was {d['previous_price']})" ) elif msg["id"] == "nba-scores": print(f"Score update: {payload['data']}") async def listen(): while True: try: async with websockets.connect(WS_URL) as ws: print("Connected — subscribing") # Subscriptions only live as long as the connection, # so re-send them on every (re)connect for sub in SUBSCRIPTIONS: await ws.send(json.dumps(sub)) async for raw in ws: msg = json.loads(raw) if msg.get("meta", {}).get("type") == "heartbeat": continue msg_type = msg.get("type") if msg_type == "subscribed": print(f"Subscription {msg['id']} active: {msg['message']}") elif msg_type == "delta": handle_delta(msg) elif msg_type == "error": print(f"Subscription error [{msg['code']}]: {msg['message']}") elif msg_type == "snapshot_error": print(f"Snapshot error [{msg['code']}]: {msg['message']}") except (websockets.ConnectionClosed, ConnectionError): print("Disconnected, reconnecting in 3s...") await asyncio.sleep(3) asyncio.run(listen()) ``` For production use, replace the fixed 3-second reconnect delay with exponential backoff and jitter — see [Reconnection Best Practices](/guides/websocket-streaming#reconnection-best-practices). ## Streaming Play-by-Play On an Ultra plan or higher, add a `plays` subscription to receive live play-by-play for in-progress games: ```json theme={null} { "action": "subscribe", "id": "mlb-plays", "channel": "plays", "params": { "sport_ids": [3], "event_ids": ["816efd1e5767d7133b5bc70c77173a18"] } } ``` Each play arrives in the delta envelope with `meta.type` of `"play"` in the inner payload — description, period, running score, and (as attribution rolls out) the players involved. For the full play payload, see [Play messages](/api-reference/v2/websocket#play-messages) in the reference. To load the plays that happened before you subscribed, either add `"snapshot": true` to the subscription (`plays` snapshots return up to 500 current plays), or fetch the timeline from the REST endpoint [`GET /api/v2/events/{eventID}/plays`](/api-reference/generated/v2-events/get-play-by-play-for-an-event) and apply streamed plays on top. If you want scores, `live_game_state`, plays, and game stats together for the same games, subscribe to the `live` channel instead of pairing separate subscriptions — it delivers all in-game updates on one subscription (score/status deltas arrive with `meta.type` of `"score"`, plays with `"play"`, and stats with `"game_stats"`), which also conserves your plan's subscription slots. ## Streaming Live Game Stats For supported live games, the `stats` channel pushes changed team and player box-score rows inline at play latency — typically within a few seconds of the corresponding play-by-play update (live game data overall trails the on-field action by roughly 15–20 seconds, in line with the typical broadcast delay). Connect to the multiplexed `/api/v2/ws` endpoint and subscribe by sport, event, or both: ```json theme={null} { "action": "subscribe", "id": "nba-stats", "channel": "stats", "params": { "sport_ids": [4], "event_ids": ["EVENT_ID"] } } ``` `stats_ids`, team IDs, player IDs, `market_ids`, and `affiliate_ids` cannot filter this channel. Each incoming delta has inner `meta.type` of `"game_stats"`; its `team_stats[]` and `player_stats[]` groups contain only rows that changed. Use this bootstrap and recovery flow: 1. Subscribe (or resubscribe) to `stats` and temporarily buffer its delta frames. 2. Fetch the current team box from [`GET /api/v2/events/{eventID}/stats`](/api-reference/generated/v2-stats/get-team-game-stats-for-an-event) and the current player box from [`GET /api/v2/events/{eventID}/players/stats`](/api-reference/generated/v2-stats/get-player-game-stats-for-an-event). 3. Replace your local baseline with those REST responses, then apply the buffered deltas in order. 4. For every later frame, upsert each nested row by its team or player ID plus `stat_id`. Keep `value` as a string and do not replace an entire group with one delta. When normalizing the REST baseline, player rows use the group's `player.id` and the nested `stat.id`; WebSocket player rows additionally carry `player_id` and `stat_id`. 5. If both stat arrays are absent and `complete` is `true`, mark the cached box score complete; repeated completion markers are idempotent. If both arrays are absent and `complete` is not `true`, the frame is an invalidation fallback, so refetch both REST resources. 6. If you detect a `sub_sequence` gap or reconnect after `buffer_overflow`, refetch both REST resources. Neither `sub_sequence` nor `delta_last_id` is a stats replay cursor. ```javascript theme={null} function applyGameStatsDelta(cache, msg, refetchCurrentBox) { const update = msg.data.data; const hasStatArrays = Array.isArray(update.team_stats) || Array.isArray(update.player_stats); const teamGroups = update.team_stats ?? []; const playerGroups = update.player_stats ?? []; cache.team ??= {}; cache.player ??= {}; // A zero-row complete:true frame is the terminal completion marker. if (!hasStatArrays) { if (update.complete === true) { cache.complete = true; return; } // A zero-row frame without complete:true is an invalidation fallback. refetchCurrentBox(update.event_id); return; } if (update.complete === true) cache.complete = true; for (const group of teamGroups) { const teamID = group.team.team_id; cache.team[teamID] ??= { team: group.team, stats: {} }; cache.team[teamID].team = group.team; for (const row of group.stats ?? []) { cache.team[teamID].stats[row.stat_id ?? row.stat.id] = row; } } for (const group of playerGroups) { const playerID = group.player.id; cache.player[playerID] ??= { player: group.player, stats: {} }; cache.player[playerID].player = group.player; for (const row of group.stats ?? []) { cache.player[playerID].stats[row.stat_id ?? row.stat.id] = row; } } } ``` Each changed nested stat row is one data point in the `stats` billing category. A zero-row completion marker or invalidation fallback costs one stats data point. See [Game stats messages](/api-reference/v2/websocket#game-stats-messages) for the full envelope, completion and fallback timestamp variants, and field reference. ## Best Practices Subscriptions live only as long as the connection. After any disconnect, re-send all subscribe messages as soon as the new connection opens — put the subscribe logic in your `onopen` handler so it runs on every connect, as in the example above. You may have missed updates while disconnected. For snapshot-capable channels, resubscribe with `"snapshot": true` so current state arrives before deltas resume, or fetch current state from the REST API (or the [delta endpoints](/guides/efficient-polling)). A `buffer_overflow:reconnect_and_catchup` close always requires this recovery. For `stats`, fetch both REST game-stat resources and then merge new deltas; snapshots and replay are unsupported. Snapshot frames are metered like the equivalent REST reads. An unfiltered subscription can stream more data than you need. Every non-market subscription has its own [1,024-message outbound queue](/api-reference/v2/websocket#message-queue); if a live frame cannot be queued, the connection closes so you can recover instead of continuing with a silent gap. Scope each subscription with `sport_ids` and `event_ids`; market subscriptions also support `market_ids` and `affiliate_ids`. The `id` is how you route incoming messages, so name subscriptions after what they carry (`nba-markets`, `mlb-plays`, `nba-stats`) rather than opaque strings. Keep a map of active ids to handlers and you can add or remove feeds without touching your message loop. Plans cap subscriptions per connection — from 3 on Ultra to 50 on Enterprise (error code `subscription_limit`; see the [connection limits table](/api-reference/v2/websocket#overview)). One `markets` subscription filtered to two sports uses one slot; two single-sport subscriptions use two. The `live` channel can replace separate `scores`, `plays`, and `stats` subscriptions with a single slot. Split subscriptions only when you need to route or manage the feeds independently. ## Next Steps Full protocol details and payload field tables The dedicated markets stream, heartbeats, and reconnection patterns Backfill the play timeline before streaming Bootstrap team and player box scores before streaming deltas REST delta endpoints as a fallback or backfill # Player Props Source: https://docs.therundown.io/guides/player-props Fetch player prop odds including points, rebounds, assists, three-pointers, and combo markets using the V2 markets system. Player prop markets let you get odds on individual player performance -- points scored, rebounds, assists, and more. TheRundown V2 API models player props as markets with `type` of `TYPE_PLAYER`. ## Player Prop Market IDs ### Individual Stats | Market | ID | Description | | ------------------- | -- | ----------------------------------- | | Player Points | 29 | Over/Under on points scored | | Player Turnovers | 33 | Over/Under on turnovers | | Player Rebounds | 35 | Over/Under on rebounds | | Three Pointers Made | 38 | Over/Under on 3PT made | | Player Assists | 39 | Over/Under on assists | | Double Double | 87 | Yes/No on recording a double-double | | Triple Double | 88 | Yes/No on recording a triple-double | | Player Blocks | 98 | Over/Under on blocks | ### Combo Markets Combo markets combine two or three stats into a single over/under line. | Market | ID | Description | | ------------------------- | --- | ------------------------------------ | | Player PRA | 93 | Points + Rebounds + Assists combined | | Player Points + Assists | 99 | Points + Assists combined | | Player Points + Rebounds | 297 | Points + Rebounds combined | | Player Rebounds + Assists | 298 | Rebounds + Assists combined | ### Live / In-Play Variants Live player props use separate market IDs but map to their prematch equivalents. | Market | ID | Prematch Equivalent | | ------------------------------ | --- | ------------------- | | Live Player Points | 90 | 29 | | Live Player Assists | 91 | 39 | | Live Three Pointers | 92 | 38 | | Live Player Rebounds | 982 | 35 | | Live Player Blocks | 983 | 98 | | Live Player Turnovers | 984 | 33 | | Live Double Double | 985 | 87 | | Live Triple Double | 986 | 88 | | Live Player PRA | 987 | 93 | | Live Player Points + Rebounds | 988 | 297 | | Live Player Points + Assists | 989 | 99 | | Live Player Rebounds + Assists | 990 | 298 | ## Discovering Available Prop Markets The tables above are a reference, but not every sport or event will have every prop market. Use the market discovery endpoints to see which prop markets are currently available. ### By sport and date Returns all markets with active pricing for a sport on a given date. Filter to player props by checking the `proposition` field — prop markets have `proposition: true`. ```bash cURL theme={null} curl "https://therundown.io/api/v2/sports/4/markets/2026-02-12?key=YOUR_API_KEY&offset=300" ``` ```python Python theme={null} import requests from datetime import date API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" response = requests.get( f"{BASE_URL}/sports/4/markets/{date.today()}", params={"key": API_KEY, "offset": "300"} ) # Response is keyed by sport ID for market in response.json().get("4", []): if market.get("proposition"): print(f" {market['id']:>4} {market['name']}") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const today = new Date().toISOString().split("T")[0]; const response = await fetch( `${BASE_URL}/sports/4/markets/${today}?key=${API_KEY}&offset=300` ); const marketsBySport = await response.json(); for (const market of marketsBySport["4"] || []) { if (market.proposition) { console.log(` ${market.id} ${market.name}`); } } ``` ### By event ID Returns only the markets available for a specific event. Useful when building a props view for a single game. ```bash cURL theme={null} curl "https://therundown.io/api/v2/events/EVENT_ID/markets?key=YOUR_API_KEY" ``` ```python Python theme={null} event_id = "EVENT_ID" response = requests.get( f"{BASE_URL}/events/{event_id}/markets", params={"key": API_KEY} ) for market in response.json(): if market.get("proposition"): print(f" {market['id']:>4} {market['name']}") ``` ```javascript JavaScript theme={null} const eventId = "EVENT_ID"; const response = await fetch( `${BASE_URL}/events/${eventId}/markets?key=${API_KEY}` ); const markets = await response.json(); for (const market of markets) { if (market.proposition) { console.log(` ${market.id} ${market.name}`); } } ``` Pass the V2 `event_id` from the event payload into per-event endpoints. Do not substitute `event_uuid`. Use the discovered market IDs in the `market_ids` parameter when fetching events to get odds for those props. *** ## Fetching Player Props Request player prop markets by including the relevant `market_ids` in your events request. ```bash cURL theme={null} # NBA player points, rebounds, assists, and 3PT props curl "https://therundown.io/api/v2/sports/4/events/2026-02-12?\ key=YOUR_API_KEY&market_ids=29,35,38,39&affiliate_ids=19,23&offset=300" ``` ```python Python theme={null} import requests from datetime import date API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" # Fetch individual stat props PROP_MARKETS = "29,35,38,39" # Points, Rebounds, 3PT, Assists response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", params={ "key": API_KEY, "market_ids": PROP_MARKETS, "affiliate_ids": "19,23", "offset": "300", } ) data = response.json() for event in data["events"]: home = event["teams"][1]["name"] away = event["teams"][0]["name"] print(f"\n{'=' * 60}") print(f"{away} @ {home}") for market in event.get("markets", []): print(f"\n {market['name']} (ID: {market['market_id']})") for participant in market["participants"]: for line in participant["lines"]: for aff_id, price in line["prices"].items(): over_under = "O" if "Over" in participant.get("name", "") or \ participant.get("type") == "TYPE_OVER" else "U" print( f" {participant['name']}: " f"{over_under} {line.get('value', 'N/A')} " f"({price['price']}) @ {aff_id}" ) ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const today = new Date().toISOString().split("T")[0]; // Fetch individual stat props const PROP_MARKETS = "29,35,38,39"; // Points, Rebounds, 3PT, Assists const params = new URLSearchParams({ key: API_KEY, market_ids: PROP_MARKETS, affiliate_ids: "19,23", offset: "300", }); const response = await fetch( `${BASE_URL}/sports/4/events/${today}?${params}` ); const data = await response.json(); for (const event of data.events) { const away = event.teams[0].name; const home = event.teams[1].name; console.log(`\n${"=".repeat(60)}`); console.log(`${away} @ ${home}`); for (const market of event.markets || []) { console.log(`\n ${market.name} (ID: ${market.market_id})`); for (const participant of market.participants) { for (const line of participant.lines) { for (const [affId, price] of Object.entries(line.prices)) { const overUnder = participant.name.includes("Over") || participant.type === "TYPE_OVER" ? "O" : "U"; console.log( ` ${participant.name}: ${overUnder} ${line.value ?? "N/A"} (${price.price}) @ ${affId}` ); } } } } } ``` ## Understanding Participants in Player Props In player prop markets, participants represent the player (not the team). Each participant has: | Field | Description | | ------ | ---------------------------------------- | | `id` | Unique numeric identifier for the player | | `name` | Player name (e.g., "LeBron James") | | `type` | `TYPE_PLAYER` for player props | Within each participant, the `lines` array contains the over/under values and their prices from each sportsbook. Each line has a `value` field (a string, e.g., `"25.5"`) and a `prices` map keyed by affiliate ID. Each price object includes the `price`, `is_main_line` boolean, and `updated_at` timestamp. **Join player props on `participant.id`, not `name`.** For `TYPE_PLAYER` participants, `id` is the stable player ID — the same key used everywhere else in the API. Fetch the full player record (team, names, position) at `GET /api/v2/players/{player_id}`. Because each player has a distinct `id`, joining on `id` resolves shared-name collisions that joining on `name` cannot. To resolve a name to an `id` once, use the roster at `GET /api/v2/teams/{team_id}/players`. You can also narrow the response server-side with `participant_ids` (comma-separated, max 99) and `participant_type=TYPE_PLAYER`. See the [Participant object](/reference/data-model#participant-object) reference for full definitions. ### Example Response Structure ```json theme={null} { "market_id": 29, "name": "Player Points", "period_id": 0, "participants": [ { "id": 12345, "name": "LeBron James Over", "type": "TYPE_OVER", "lines": [ { "value": "25.5", "prices": { "19": { "price": -115, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" }, "23": { "price": -110, "is_main_line": true, "updated_at": "2026-02-12T18:29:45Z" } } } ] }, { "id": 12345, "name": "LeBron James Under", "type": "TYPE_UNDER", "lines": [ { "value": "25.5", "prices": { "19": { "price": -105, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" }, "23": { "price": -110, "is_main_line": true, "updated_at": "2026-02-12T18:29:45Z" } } } ] } ] } ``` ## Fetching Combo Props Combo markets combine multiple stats. The request pattern is the same -- just use the combo market IDs. ```python Python theme={null} # Fetch combo props: PRA, PA, PR, RA COMBO_MARKETS = "93,99,297,298" response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", params={ "key": API_KEY, "market_ids": COMBO_MARKETS, "affiliate_ids": "19", } ) data = response.json() for event in data["events"]: print(f"\n{event['teams'][0]['name']} @ {event['teams'][1]['name']}") for market in event.get("markets", []): print(f"\n {market['name']}:") for participant in market["participants"]: for line in participant["lines"]: price_obj = line["prices"].get("19", {}) price = price_obj.get("price", "N/A") print(f" {participant['name']}: {line.get('value')} ({price})") ``` ```javascript JavaScript theme={null} // Fetch combo props: PRA, PA, PR, RA const COMBO_MARKETS = "93,99,297,298"; const params = new URLSearchParams({ key: API_KEY, market_ids: COMBO_MARKETS, affiliate_ids: "19", }); const response = await fetch( `${BASE_URL}/sports/4/events/${today}?${params}` ); const data = await response.json(); for (const event of data.events) { console.log(`\n${event.teams[0].name} @ ${event.teams[1].name}`); for (const market of event.markets || []) { console.log(`\n ${market.name}:`); for (const participant of market.participants) { for (const line of participant.lines) { const price = line.prices["19"]?.price ?? "N/A"; console.log(` ${participant.name}: ${line.value} (${price})`); } } } } ``` ## Fetching All Props at Once You can request all prop market IDs in a single call. This is useful if you want to build a comprehensive player props page. ```bash theme={null} # All individual + combo props curl "https://therundown.io/api/v2/sports/4/events/2026-02-12?\ key=YOUR_API_KEY\ &market_ids=29,33,35,38,39,87,88,93,98,99,297,298\ &affiliate_ids=19,23" ``` ## Live Player Props Live player props are available once a game starts. Use the live market IDs to fetch in-play prop odds. ```bash theme={null} # Live player points (90), assists (91), 3PT (92) curl "https://therundown.io/api/v2/sports/4/events/2026-02-12?\ key=YOUR_API_KEY&market_ids=90,91,92&affiliate_ids=19" ``` You can also subscribe to live prop updates via the V2 Markets WebSocket: ```javascript theme={null} const ws = new WebSocket( "wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&sport_ids=4&market_ids=90,91,92" ); ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.meta?.type === "heartbeat") return; console.log(`Live prop update: market=${data.market_id}`); for (const participant of data.participants || []) { console.log(` ${participant.name}`); } }; ``` ## Building a Player Props Display Here is a complete example that organizes props by player for display in a UI. ```python theme={null} import requests from collections import defaultdict from datetime import date API_KEY = "YOUR_API_KEY" BASE_URL = "https://therundown.io/api/v2" MARKET_NAMES = { 29: "Points", 35: "Rebounds", 38: "3PT Made", 39: "Assists", 93: "PRA", 98: "Blocks", 99: "Pts+Ast", 297: "Pts+Reb", 298: "Reb+Ast", } response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", params={ "key": API_KEY, "market_ids": ",".join(str(m) for m in MARKET_NAMES), "affiliate_ids": "19", "main_line": "true", } ) data = response.json() for event in data["events"]: home = event["teams"][1]["name"] away = event["teams"][0]["name"] print(f"\n{'=' * 60}") print(f"{away} @ {home}") print(f"{'=' * 60}") # Group props by player players = defaultdict(list) for market in event.get("markets", []): mid = market["market_id"] market_label = MARKET_NAMES.get(mid, market["name"]) for participant in market["participants"]: # Extract player name (remove "Over"/"Under" suffix) player_name = participant["name"] for suffix in [" Over", " Under"]: player_name = player_name.replace(suffix, "") is_over = "Over" in participant.get("name", "") or \ participant.get("type") == "TYPE_OVER" for line in participant["lines"]: price_obj = line["prices"].get("19", {}) price = price_obj.get("price") if price == 0.0001: price = None players[player_name].append({ "market": market_label, "value": line.get("value"), "side": "Over" if is_over else "Under", "price": price, }) # Display grouped by player for player, props in sorted(players.items()): print(f"\n {player}:") for prop in props: price_str = f"{prop['price']:+d}" if prop["price"] else "N/A" print(f" {prop['market']}: {prop['side']} {prop['value']} ({price_str})") ``` ## Next Steps Full list of all market IDs Core game odds (moneyline, spread, total) Real-time updates for live props Track prop line movement over time # V1 to V2 Migration Guide Source: https://docs.therundown.io/guides/v1-to-v2-migration Understand the differences between V1 and V2 data models and migrate your application from the legacy lines format to the new markets-based system. TheRundown V2 API uses a market-based data model that is more flexible and extensible than V1's flat line structure. This guide covers the key differences and provides a mapping to help you migrate. ## Important Notes Before You Start **V1 only: Use `teams_normalized`, not `teams`.** V1 event responses contain both a `teams` array and a `teams_normalized` array. The `teams` array uses internal book-specific IDs that are inconsistent across sportsbooks and seasons. **Always use `teams_normalized`** in V1 — these are canonical, stable team IDs that work across all endpoints. In V2, the `teams` array already uses normalized IDs, so you can use `event["teams"]` directly. **The V2 delta endpoint is a V1 feature.** Despite living at `/api/v2/delta` and `/api/v2/markets/delta`, these are currently the only V2-path endpoints available and function as upgraded versions of the V1 delta. They return V2-formatted market data but are accessible to V1 API plans. The rest of the V2 API (events, markets, teams, players, stats) requires a V2 subscription. ## Data Model: Lines vs. Markets The fundamental change in V2 is how odds data is structured. ### V1: Flat Lines Model In V1, each event contains a `lines` object keyed by `affiliate_id`. Each affiliate entry has separate `moneyline`, `spread`, and `total` objects. ```json theme={null} { "event_id": "abc123", "lines": { "19": { "affiliate_id": 19, "moneyline": { "moneyline_away": 150, "moneyline_home": -170 }, "spread": { "point_spread_away": 3.5, "point_spread_away_money": -110, "point_spread_home": -3.5, "point_spread_home_money": -110 }, "total": { "total_over": 220.5, "total_over_money": -110, "total_under": 220.5, "total_under_money": -110 } }, "23": { "affiliate_id": 23, "moneyline": { "moneyline_away": 145, "moneyline_home": -165 } } } } ``` ### V2: Markets Model In V2, each event contains a `markets` array. Each market has `participants` with `lines` and `prices` organized by sportsbook. ```json theme={null} { "event_id": "abc123", "markets": [ { "market_id": 1, "name": "Moneyline", "period_id": 0, "participants": [ { "id": 1, "type": "TYPE_TEAM", "name": "Team A", "lines": [ { "value": "", "prices": { "19": { "price": 150, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" }, "23": { "price": 145, "is_main_line": true, "updated_at": "2026-02-12T18:28:00Z" } } } ] }, { "id": 2, "type": "TYPE_TEAM", "name": "Team B", "lines": [ { "value": "", "prices": { "19": { "price": -170, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" }, "23": { "price": -165, "is_main_line": true, "updated_at": "2026-02-12T18:28:00Z" } } } ] } ] } ] } ``` ### Key Structural Differences | Aspect | V1 | V2 | | ------------------- | ------------------------------------- | ------------------------------------------------------------- | | Data access | `event.lines[affiliate_id].moneyline` | `event.markets[].participants[].lines[].prices[affiliate_id]` | | Sportsbook grouping | Top level (by affiliate) | Nested inside lines (prices map) | | Market types | Fixed: moneyline, spread, total | Extensible: any market ID | | Player props | Not supported | Full support via market IDs | | Alternate lines | Limited | Full support, `is_main_line` flag on price | | Period data | `line_periods` object | `period_id` on each market | ## Period Data ### V1: line\_periods V1 uses a nested `line_periods` object for half/quarter data: ```json theme={null} { "lines": { "19": { "moneyline": { "moneyline_away": 150, "moneyline_home": -170 }, "line_periods": { "period_first_half": { "moneyline": { "moneyline_away": 130, "moneyline_home": -150 }, "spread": { "point_spread_away": 2.0, "point_spread_away_money": -110 } }, "period_first_period": { "moneyline": { "moneyline_away": 180, "moneyline_home": -210 } } } } } } ``` ### V2: period\_id V2 includes period data as separate market entries distinguished by `period_id`: ```json theme={null} { "markets": [ { "market_id": 1, "name": "Moneyline", "period_id": 0, "participants": [...] }, { "market_id": 1, "name": "Moneyline", "period_id": 1, "participants": [...] } ] } ``` See [Period IDs](/reference/periods) for the complete list of period identifiers. ## Delta Endpoints There are four delta endpoints available. The V2-path delta endpoints (`/api/v2/delta` and `/api/v2/markets/delta`) are available to all API plans — they were built as upgrades to the V1 delta and happen to live under the `/v2/` path. | Endpoint | Format | Description | | --------------------------- | --------------- | --------------------------------------------------------------------- | | `GET /api/v1/delta` | V1 lines | Returns full V1 events that changed. Use for legacy integrations. | | `GET /api/v1/deltaV2` | V2 markets | Returns V2-formatted data via the V1 path. Useful during migration. | | `GET /api/v2/delta` | V2 events | Returns changed events with V2 market data. | | `GET /api/v2/markets/delta` | V2 markets only | Returns only changed market/price data — most granular and efficient. | The V2 market delta is the most efficient option — it returns individual price changes rather than entire event objects, significantly reducing bandwidth. ### Delta Polling Pattern 1. **Bootstrap the cursor**: Fetch a `/api/v2/sports/{id}/events/{date}` snapshot and read its integer `meta.delta_last_id` — this is the cursor for `markets/delta`. (Don't bootstrap with `last_id=0`; a cursor too far behind the head is rejected.) 2. **Store the cursor**: Each `markets/delta` response returns its own integer `meta.delta_last_id` — save it for the next poll 3. **Subsequent polls**: Pass the stored cursor to get only the price changes since your last poll 4. **Replace, don't merge**: Each delta contains the full updated object — replace your cached version entirely ```bash theme={null} # Most efficient: V2 markets delta (integer cursor from the events snapshot) curl "https://therundown.io/api/v2/markets/delta?\ key=YOUR_API_KEY&last_id=PREVIOUS_DELTA_LAST_ID&sport_id=4&market_ids=1,2,3" ``` `/api/v2/markets/delta` (price changes) and `/api/v2/delta` (full event-object changes) use different cursors: an **integer** for markets delta, an **ordered UUID** for event delta. The integer `meta.delta_last_id` from the events snapshot bootstraps `markets/delta` only — `/api/v2/delta` rejects it. For odds polling, use `markets/delta`. ## WebSocket Endpoints V2 provides a dedicated market-price stream and a multiplexed endpoint for subscribing to multiple data types, while V1 uses a single line-update stream. | Version | Endpoint | Description | | ------- | --------------------------------------- | --------------------------------------------------------------------------- | | V1 | `wss://therundown.io/api/v1/ws` | Single stream, all line updates | | V2 | `wss://therundown.io/api/v2/ws/markets` | Market/price updates | | V2 | `wss://therundown.io/api/v2/ws` | Multiplexed `markets`, `scores`, `plays`, `stats`, and `live` subscriptions | V2 WebSockets support more granular filtering. Market subscriptions accept `sport_ids`, `market_ids`, `event_ids`, and `affiliate_ids`; `scores`, `plays`, `stats`, and `live` subscriptions accept `sport_ids` and `event_ids`. ## Endpoint Mapping Table Use this table to find the V2 equivalent of each V1 endpoint. | V1 Endpoint | V2 Equivalent | Notes | | --------------------------------------- | ----------------------------------------- | ------------------------------- | | `GET /api/v1/sports` | `GET /api/v2/sports` | Same response format | | `GET /api/v1/affiliates` | `GET /api/v2/affiliates` | Same response format | | `GET /api/v1/sports/{id}/events/{date}` | `GET /api/v2/sports/{id}/events/{date}` | Response uses markets model | | `GET /api/v1/events/{id}` | `GET /api/v2/events/{id}` | Response uses markets model | | `GET /api/v1/sports/{id}/events/delta` | `GET /api/v2/markets/delta` | V2 returns market-level changes | | `GET /api/v1/events/{id}/lines` | `GET /api/v2/events/{id}/markets` | Markets replace lines | | `wss://therundown.io/api/v1/ws` | `wss://therundown.io/api/v2/ws/markets` | V2 supports more filters | | N/A | `GET /api/v2/events/{id}/markets/history` | New in V2 | | N/A | `GET /api/v2/events/{id}/openers` | New in V2 | ## Migration Code Example Here is a side-by-side comparison of extracting odds data in V1 vs. V2. ```python V1 (Legacy) theme={null} import requests API_KEY = "YOUR_API_KEY" # V1: Fetch events response = requests.get( "https://therundown.io/api/v1/sports/4/events/2026-02-12", params={"key": API_KEY} ) data = response.json() for event in data["events"]: away = event["teams_normalized"][0]["name"] home = event["teams_normalized"][1]["name"] print(f"{away} @ {home}") lines = event.get("lines", {}) # Access DraftKings lines (affiliate 19) dk = lines.get("19", {}) if dk: ml = dk.get("moneyline", {}) spread = dk.get("spread", {}) total = dk.get("total", {}) print(f" ML: {ml.get('moneyline_away')} / {ml.get('moneyline_home')}") print(f" Spread: {spread.get('point_spread_away')} ({spread.get('point_spread_away_money')})") print(f" Total: O {total.get('total_over')} ({total.get('total_over_money')})") # Access first half lines periods = dk.get("line_periods", {}) first_half = periods.get("period_first_half", {}) if first_half: fh_ml = first_half.get("moneyline", {}) print(f" 1H ML: {fh_ml.get('moneyline_away')} / {fh_ml.get('moneyline_home')}") ``` ```python V2 (Recommended) theme={null} import requests API_KEY = "YOUR_API_KEY" # V2: Fetch events with markets response = requests.get( "https://therundown.io/api/v2/sports/4/events/2026-02-12", params={ "key": API_KEY, "market_ids": "1,2,3", "affiliate_ids": "19", } ) data = response.json() for event in data["events"]: away = event["teams"][0]["name"] home = event["teams"][1]["name"] print(f"{away} @ {home}") for market in event.get("markets", []): mid = market["market_id"] period = market.get("period_id", 0) period_label = "" if period == 0 else f" (Period {period})" print(f" {market['name']}{period_label}:") for participant in market["participants"]: for line in participant["lines"]: price_obj = line["prices"].get("19", {}) price = price_obj.get("price", "N/A") line_val = line.get("value", "") line_str = f" {line_val}" if line_val else "" print(f" {participant['name']}{line_str}: {price}") ``` ## V2 Advantages V2 supports player props (points, rebounds, assists, combos), team totals, and other market types that V1 cannot represent. New markets are added as new market IDs without API changes. V2 returns all available lines (main and alternates) with an `is_main` flag to distinguish the primary line. Use `main_line=true` to filter to main lines only. V2 streams type-specific deltas rather than one mixed event feed: market subscriptions deliver individual price changes, while the multiplexed endpoint offers score, play, and changed game-stat rows. This reduces bandwidth and lets clients subscribe only to the data they use. V2 adds price history and opening lines endpoints that are not available in V1. ## Common Gotchas ### The 0.0001 sentinel value A price of `0.0001` means the sportsbook has taken that line **off the board** — it is not an error. Display it as "Off Board" or "N/A" and never use it in calculations. See [Sentinel Values](/reference/sentinel-values) for details. ### V1 team IDs are unreliable V1 events contain both `teams` and `teams_normalized` arrays. The `teams` array uses internal book-specific IDs that vary by sportsbook and can change between seasons. **Always use `teams_normalized`** for stable, canonical team identifiers. In V2, the `teams` array already uses normalized IDs. ### Null vs. empty vs. zero The API may return `null`, empty strings, empty arrays, or `0` depending on the field and state. Your parsing code should handle all of these: * A missing `score` object means the game hasn't started * An empty `markets` array means no odds are available yet * A `0` value for `game_clock` can mean the period hasn't started or has ended — check `event_status` for context ### line\_value\_is\_participant This flag on market definitions tells you how to interpret the `value` field on lines: * `true` — the participant carries the selection. The line value may be a placeholder such as `"0"` or a label. * `false` — the line value is meaningful and should be displayed when present. It may be a number, player stat threshold, method, round, or other outcome qualifier. ## Next Steps Start using V2 market data V2 hierarchy: events, markets, lines, and prices New in V2: player prop markets V2 WebSocket endpoints Common questions about the API Complete list of V2 market IDs # WebSocket Streaming Source: https://docs.therundown.io/guides/websocket-streaming Connect to TheRundown WebSocket feeds for real-time odds, scores, plays, and game-stat updates. TheRundown provides WebSocket endpoints for real-time data delivery. Instead of polling REST endpoints, open a persistent connection and receive updates as they happen. WebSocket traffic does not increment the HTTP request counter, but pushed messages and snapshots are still metered as data points. WebSocket access is enabled on real-time API tiers (Ultra and above) by default. If your key does not have WebSocket access, use the [market delta endpoint](/guides/efficient-polling) for ongoing updates. ## Available WebSocket Endpoints | Endpoint | URL | Description | | -------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | V2 Markets | `wss://therundown.io/api/v2/ws/markets` | Real-time market/price updates for all V2 data | | V2 Multiplexed | `wss://therundown.io/api/v2/ws` | Subscribe to `markets`, `scores`, `plays`, `stats`, and `live` channels over one connection. The `plays`, `stats`, and `live` channels require an **Ultra plan or higher** | | V1 (Legacy) | `wss://therundown.io/api/v1/ws` | Legacy format line updates | This guide covers the V2 Markets stream. For the multiplexed endpoint — connecting, subscribing to channels, and streaming play-by-play or live game stats — see the [Multiplexed WebSocket guide](/guides/multiplexed-websocket) and the [WebSocket reference](/api-reference/v2/websocket#multiplexed-websocket-channels). ## V2 Markets WebSocket The primary WebSocket for real-time odds data. Delivers individual price updates in a flat format — each message contains one price change for one participant, market, and sportsbook. ### Connection ``` wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY ``` ### Filter Parameters All filters are optional. Without filters, you receive updates for all sports and markets. | Parameter | Type | Description | | --------------- | ------ | -------------------------------------------- | | `key` | string | **Required.** Your API key | | `sport_ids` | string | Comma-separated sport IDs (e.g., `2,4,6`) | | `market_ids` | string | Comma-separated market IDs (e.g., `1,2,3`) | | `event_ids` | string | Comma-separated event IDs for specific games | | `affiliate_ids` | string | Comma-separated sportsbook IDs | ### Example: Filtered Connection ``` wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&sport_ids=4&market_ids=1,2,3&affiliate_ids=19,23 ``` ### Message Format Each message is a JSON object with `meta` (message metadata) and `data` (the price update). Each message represents a single price change — not a full market snapshot. ```json theme={null} { "meta": { "type": "market_price", "version": "v2", "timestamp": 1772495104 }, "data": { "id": 193600383, "event_id": "9b9d0cf6007fdaeb15c3a1888dcfd5df", "affiliate_id": 26, "market_participant_id": 19402291, "market_id": 3, "line": "1.5", "price": "-117", "previous_price": "-122.0000", "price_delta": 5, "is_main_line": true, "normalized_market_participant_id": 10, "normalized_market_participant_type": 3, "sport_id": 7, "updated_at": "2026-03-02T23:44:44Z" } } ``` ### Code Examples ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const ws = new WebSocket( `wss://therundown.io/api/v2/ws/markets?key=${API_KEY}&sport_ids=4&market_ids=1,2,3` ); ws.onopen = () => { console.log("Connected to V2 Markets WebSocket"); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); // Handle heartbeat if (msg.meta?.type === "heartbeat") { console.log("Heartbeat received"); return; } // Process market price update const d = msg.data; console.log( `Update: event=${d.event_id} market=${d.market_id} aff=${d.affiliate_id}` ); console.log( ` line=${d.line} price=${d.price} (was ${d.previous_price}, delta=${d.price_delta})` ); }; ws.onerror = (error) => { console.error("WebSocket error:", error); }; ws.onclose = (event) => { console.log(`WebSocket closed: code=${event.code} reason=${event.reason}`); }; ``` ```python Python theme={null} import asyncio import json import websockets API_KEY = "YOUR_API_KEY" WS_URL = ( f"wss://therundown.io/api/v2/ws/markets" f"?key={API_KEY}&sport_ids=4&market_ids=1,2,3" ) async def listen(): async with websockets.connect(WS_URL) as ws: print("Connected to V2 Markets WebSocket") async for message in ws: msg = json.loads(message) # Handle heartbeat if msg.get("meta", {}).get("type") == "heartbeat": print("Heartbeat received") continue # Process market price update d = msg["data"] print( f"Update: event={d['event_id']} market={d['market_id']} aff={d['affiliate_id']}" ) print( f" line={d['line']} price={d['price']}" f" (was {d['previous_price']}, delta={d['price_delta']})" ) asyncio.run(listen()) ``` ## V1 WebSocket (Legacy) The V1 WebSocket delivers line updates with `meta.type` identifying the market type (e.g., `"moneyline"`, `"spread"`, `"total"`). The `data` object contains market-specific fields with `_delta` suffixes showing the change from the previous value. Use this only if your application has not migrated to V2. ### Connection ``` wss://therundown.io/api/v1/ws?key=YOUR_API_KEY ``` ### Filter Parameters | Parameter | Type | Description | | --------------- | ------ | -------------------------------- | | `key` | string | **Required.** Your API key | | `sport_ids` | string | Comma-separated sport IDs | | `affiliate_ids` | string | Comma-separated sportsbook IDs | | `event_ids` | string | Comma-separated event IDs | | `date` | string | Date filter (e.g., `2026-02-12`) | ### Example Message ```json theme={null} { "meta": { "type": "spread" }, "data": { "point_spread_away": 0.5, "point_spread_home": -0.5, "point_spread_away_delta": 0.4999, "point_spread_home_delta": -0.5001, "point_spread_away_money": 115, "point_spread_away_money_delta": 114.9999, "point_spread_home_money": -175, "point_spread_home_money_delta": -175.0001, "line_id": 17994923, "event_id": "541a8c85c41cd7ec7ecc74521ee71bc4", "sport_id": 12, "affiliate_id": 19, "date_updated": "2026-03-02T23:45:24Z", "event_date": "2026-03-08T19:45:00Z", "format": "American", "period_id": 0 } } ``` ## Heartbeat Handling All WebSocket endpoints send heartbeat messages every **15 seconds** to keep the connection alive. Your client must handle these to avoid treating them as data updates. ```json theme={null} { "meta": { "type": "heartbeat" }, "data": { "now": "2026-02-12T18:30:00Z" } } ``` Heartbeats arrive every 15 seconds. If you do not receive one within 60 seconds (i.e., four missed heartbeats), the connection is likely stale. Close and reconnect. ### Heartbeat Handler Pattern ```javascript JavaScript theme={null} let lastHeartbeat = Date.now(); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.meta?.type === "heartbeat") { lastHeartbeat = Date.now(); return; // Do not process as data } // Process actual price update handleUpdate(msg.data); }; // Monitor heartbeat health setInterval(() => { const elapsed = Date.now() - lastHeartbeat; if (elapsed > 60_000) { console.warn("No heartbeat in 60s, reconnecting..."); ws.close(); // Trigger reconnection logic } }, 10_000); ``` ```python Python theme={null} import time last_heartbeat = time.time() async def listen_with_heartbeat(): global last_heartbeat async with websockets.connect(WS_URL) as ws: async for message in ws: msg = json.loads(message) if msg.get("meta", {}).get("type") == "heartbeat": last_heartbeat = time.time() continue # Check heartbeat staleness if time.time() - last_heartbeat > 60: print("No heartbeat in 60s, reconnecting...") break handle_update(msg["data"]) ``` ## Reconnection Best Practices WebSocket connections can drop due to network issues, server maintenance, or client-side timeouts. Always implement automatic reconnection. ### Recommended Strategy 1. **Exponential backoff** -- start at 1 second, double on each failure, cap at 30 seconds. 2. **Add jitter** -- randomize the delay slightly to avoid thundering herd reconnections. 3. **Reset backoff on success** -- once a connection is established and receives data, reset the delay to 1 second. 4. **Refetch REST state after reconnect** -- you may have missed updates during the disconnection. Fetch the latest state from the REST API to ensure consistency. ```javascript JavaScript theme={null} function createReconnectingWebSocket(url, onMessage) { let ws; let reconnectDelay = 1000; const MAX_DELAY = 30000; function connect() { ws = new WebSocket(url); ws.onopen = () => { console.log("WebSocket connected"); reconnectDelay = 1000; // Reset backoff }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.meta?.type === "heartbeat") return; onMessage(msg.data); }; ws.onclose = () => { const jitter = Math.random() * 1000; const delay = Math.min(reconnectDelay + jitter, MAX_DELAY); console.log(`WebSocket closed. Reconnecting in ${Math.round(delay)}ms...`); setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, MAX_DELAY); connect(); }, delay); }; ws.onerror = (error) => { console.error("WebSocket error:", error); ws.close(); // Triggers onclose -> reconnect }; } connect(); return { close: () => { reconnectDelay = MAX_DELAY + 1; // Prevent reconnection ws.close(); }, }; } // Usage const connection = createReconnectingWebSocket( `wss://therundown.io/api/v2/ws/markets?key=${API_KEY}&sport_ids=4`, (update) => { console.log(`Price update: event=${update.event_id} market=${update.market_id} price=${update.price}`); } ); ``` ```python Python theme={null} import asyncio import json import random import websockets async def connect_with_reconnect(url, on_message): reconnect_delay = 1.0 max_delay = 30.0 while True: try: async with websockets.connect(url) as ws: print("WebSocket connected") reconnect_delay = 1.0 # Reset backoff async for message in ws: msg = json.loads(message) if msg.get("meta", {}).get("type") == "heartbeat": continue await on_message(msg["data"]) except (websockets.ConnectionClosed, ConnectionError) as e: jitter = random.uniform(0, 1) delay = min(reconnect_delay + jitter, max_delay) print(f"WebSocket closed ({e}). Reconnecting in {delay:.1f}s...") await asyncio.sleep(delay) reconnect_delay = min(reconnect_delay * 2, max_delay) except Exception as e: print(f"Unexpected error: {e}") await asyncio.sleep(5) async def handle_update(update): print(f"Price update: event={update['event_id']} market={update['market_id']} price={update['price']}") # Run WS_URL = f"wss://therundown.io/api/v2/ws/markets?key={API_KEY}&sport_ids=4" asyncio.run(connect_with_reconnect(WS_URL, handle_update)) ``` ## Tips for Production Always specify `sport_ids` and `market_ids` in your WebSocket URL. Receiving updates for all sports and markets generates significant traffic that your client may not need. Do not block the WebSocket message handler with slow operations (database writes, API calls). Queue updates and process them in a separate thread or task. After a reconnect, fetch the latest state from the REST API to fill any gaps. WebSocket updates are incremental -- if you miss one, your state may be stale. Track heartbeat intervals and reconnection frequency. Alert if the connection drops repeatedly, which may indicate a network issue or an invalid API key. ## Next Steps Markets, scores, plays, game stats, and live game state over one connection Use WebSocket data in a real-time UI REST API for initial data load WebSocket does not count against REST limits Migrate from V1 WebSocket to V2 # TheRundown Sports API Source: https://docs.therundown.io/introduction Real-time sports betting data, odds, scores, stats, and schedules ## Overview TheRundown API provides comprehensive sports data including real-time odds from 15+ sportsbooks, live scores, game schedules, team and player statistics, and historical line data across major North American and international sports leagues. ### What you can build * **Live odds screens** — Real-time moneylines, spreads, and totals from 15+ sportsbooks * **Scores and schedules** — Live game scores, schedules, and event statuses across all sports * **Line movement trackers** — Historical price data with charting support * **Player prop tools** — Points, rebounds, assists, and combo markets * **Team and player stats** — Season and game-level statistics, with live box-score deltas over WebSocket * **Sports data apps** — Complete sports data platform with scores, odds, stats, and schedules ### Supported sports | Sport | ID | Data Available | | ---------- | --------- | ----------------------------------------------------------------------------- | | NFL | 2 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | NBA | 4 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | MLB | 3 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props, Pitcher Data | | NHL | 6 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | NCAAF | 1 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | NCAAB | 5 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | WNBA | 8 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | UFC/MMA | 7 | Odds, Scores, Schedules, Team Stats, Player Stats | | CFL | 9 | Odds, Scores, Schedules, Team Stats, Player Stats | | Soccer | 10-19, 33 | Odds, Scores, Schedules, Team Stats, Player Stats, Player Props | | ATP Tennis | 38 | Odds, Scores, Schedules, Team Stats, Player Stats | | WTA Tennis | 39 | Odds, Scores, Schedules, Team Stats, Player Stats | Soccer coverage includes MLS, EPL, Ligue 1, Bundesliga, La Liga, Serie A, UEFA Champions League, UEFA Euro, FIFA, J-League, and UEFA Europa League. See [Sport IDs](/reference/sports) for the full list. ### V1 vs V2 **V2 (recommended)** uses a market-based data model where each odds type is a "market" with participants, lines, and prices. This is more flexible and supports player props, alternates, and new market types. **V1 (legacy)** uses a flat line-based model with separate moneyline, spread, and total objects. V1 is still supported but new features are V2-only. Get your first API response in 2 minutes API key setup and auth methods Connect Claude and other AI agents directly to live sports data Complete endpoint documentation with interactive playground How events, markets, lines, and prices relate to each other Sport IDs, market IDs, sportsbook IDs Common questions about the API Download the OpenAPI 3.1 spec for client generation and tooling # MCP Server Source: https://docs.therundown.io/mcp Connect TheRundown API docs to Claude, Cursor, and other AI assistants. TheRundown provides a [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that lets AI assistants search our documentation directly. Ask questions about endpoints, query parameters, response fields, market IDs, billing headers, and integration patterns without leaving your editor. This MCP server is **documentation-only**. It helps your assistant understand the API, but it does not stream live sports data or make authenticated API calls on your behalf. For live data, your agent still needs to call the real API using your own TheRundown key. ## Server URL ``` https://docs.therundown.io/mcp ``` ## Claude Code Add the server from your terminal: ```bash theme={null} claude mcp add --transport http therundown https://docs.therundown.io/mcp ``` ## Cursor Open the command palette and search "Open MCP settings", then add to your `mcp.json`: ```json theme={null} { "mcpServers": { "therundown": { "url": "https://docs.therundown.io/mcp" } } } ``` ## VS Code Create or update `.vscode/mcp.json` in your project: ```json theme={null} { "servers": { "therundown": { "type": "http", "url": "https://docs.therundown.io/mcp" } } } ``` ## Claude Desktop Add this to your Claude Desktop config file: * **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` * **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` ```json theme={null} { "mcpServers": { "therundown": { "command": "npx", "args": [ "mcp-remote", "https://docs.therundown.io/mcp" ] } } } ``` Restart Claude Desktop after saving. ## Windsurf Add to your `~/.codeium/windsurf/mcp_config.json`: ```json theme={null} { "mcpServers": { "therundown": { "command": "npx", "args": [ "mcp-remote", "https://docs.therundown.io/mcp" ] } } } ``` ## What you can ask The best workflow is: 1. Ask MCP which endpoint, filters, and market IDs you need 2. Ask it for a code example in your language 3. Have your agent call the real API with your own key 4. Use the returned billing headers to keep usage under control Once connected, you can ask your AI assistant questions like: * "What is the cheapest way to monitor NBA moneyline, spread, and total on DraftKings and FanDuel?" * "Should I use market delta or WebSocket for live line movement?" * "What headers tell me remaining data points on my key?" * "What are the market IDs for player props in NBA?" * "Show me a Python example for `GET /api/v2/sports/{sportID}/events/{date}` with `market_ids` and `affiliate_ids` filters" * "What does the `0.0001` sentinel value mean and how should I handle it?" The MCP server searches across the API reference, guides, SDK docs, and reference tables to find relevant answers and examples. # Quickstart Source: https://docs.therundown.io/quickstart Get your first API response, understand your usage headers, and choose the right update path. ## 1. Get your API key Sign up at [therundown.io/api](https://therundown.io/api) to get your API key. ## 2. Make your first request ```bash cURL theme={null} curl -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/sports" ``` ```python Python theme={null} import requests response = requests.get( "https://therundown.io/api/v2/sports", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, ) print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch( "https://therundown.io/api/v2/sports", { headers: { "X-TheRundown-Key": "YOUR_API_KEY" }, } ); const data = await response.json(); console.log(data); ``` ```go Go theme={null} package main import ( "fmt" "io" "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://therundown.io/api/v2/sports", nil) req.Header.Set("X-TheRundown-Key", "YOUR_API_KEY") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) fmt.Println(string(body)) } ``` ```ruby Ruby theme={null} require 'net/http' require 'json' uri = URI("https://therundown.io/api/v2/sports") req = Net::HTTP::Get.new(uri) req["X-TheRundown-Key"] = "YOUR_API_KEY" response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http| http.request(req) end puts JSON.parse(response.body) ``` Some reference endpoints are public, but authenticating from the start lets you see the same headers and behavior your production integration will use. You'll receive a list of available sports with their IDs. A shortened example: ```json theme={null} { "sports": [ { "sport_id": 1, "sport_name": "NCAA Football" }, { "sport_id": 2, "sport_name": "NFL" }, { "sport_id": 3, "sport_name": "MLB" }, { "sport_id": 4, "sport_name": "NBA" }, { "sport_id": 5, "sport_name": "NCAA Men's Basketball" }, { "sport_id": 6, "sport_name": "NHL" }, { "sport_id": 38, "sport_name": "TENNIS.ATP" }, { "sport_id": 39, "sport_name": "TENNIS.WTA" } ] } ``` ## 3. Get today's NBA odds The `offset` parameter shifts the date boundary so a "day" aligns with the timezone you care about instead of midnight UTC. Use the offset that matches your market timezone. See the [Events reference](/api-reference/generated/v2-events/get-events-with-markets-for-a-sport-and-date) for examples. ```bash cURL theme={null} curl -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/sports/4/events/2026-02-12?market_ids=1,2,3&affiliate_ids=19,23&main_line=true&offset=300" ``` ```python Python theme={null} import requests from datetime import date response = requests.get( f"https://therundown.io/api/v2/sports/4/events/{date.today()}", headers={"X-TheRundown-Key": "YOUR_API_KEY"}, params={ "market_ids": "1,2,3", # Moneyline, Spread, Total "affiliate_ids": "19,23", "main_line": "true", "offset": "300", # Example offset; adjust for your timezone } ) for event in response.json()["events"]: teams = event["teams"] print(f"{teams[0]['name']} @ {teams[1]['name']}") for market in event.get("markets", []): print(f" {market['name']}") for participant in market["participants"]: for line in participant["lines"]: for aff_id, price in line["prices"].items(): print(f" {participant['name']}: {price['price']}") ``` ```javascript JavaScript theme={null} const today = new Date().toISOString().split("T")[0]; const response = await fetch( `https://therundown.io/api/v2/sports/4/events/${today}?market_ids=1,2,3&affiliate_ids=19,23&main_line=true&offset=300`, { headers: { "X-TheRundown-Key": "YOUR_API_KEY" }, } ); const data = await response.json(); for (const event of data.events) { console.log(`${event.teams[0].name} @ ${event.teams[1].name}`); for (const market of event.markets || []) { console.log(` ${market.name}`); } } ``` In production, `market_ids`, `affiliate_ids`, and `main_line=true` are your biggest levers for controlling payload size and data-point usage. ## 4. Choose your update path Every plan can use delta endpoints for efficient polling: ```bash theme={null} curl -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/markets/delta?sport_id=4&market_ids=1,2,3&last_id=0" ``` If your key has `X-Websocket-Access: true`, you can also connect directly to the WebSocket feed for real-time updates: ```javascript theme={null} const ws = new WebSocket( "wss://therundown.io/api/v2/ws/markets?key=YOUR_API_KEY&sport_ids=4" ); ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.meta?.type === "heartbeat") return; const d = msg.data; console.log(`Price update: event=${d.event_id} market=${d.market_id} price=${d.price}`); }; ``` WebSocket access is enabled on real-time tiers by default. If your key does not have it, use market delta polling instead. ## Next steps Learn about all auth methods Understand data points and `429` responses Step-by-step guide Delta, caching, and cost control All market types and their IDs Real-time data guide # Rate Limits Source: https://docs.therundown.io/rate-limits Understand data-point billing, per-second throttles, usage headers, and the different 429 responses you can receive. TheRundown API enforces usage controls **per API key**. There are two separate systems to understand: * **Data-point billing** tracks how much data you consume across REST and WebSocket traffic. * **Per-second throttling** protects the platform from bursty request patterns. If you are building production workloads, you should monitor both. A key can stay under its requests-per-second ceiling and still run out of included data points, or vice versa. ## How Usage Is Enforced ### 1. Data points Most API endpoints return an `X-Datapoints` header. That number represents how many billable data points the response consumed. In practice: * large snapshots across many sportsbooks and markets cost more than narrow, filtered responses * delta endpoints are usually much cheaper than repeatedly fetching full event payloads * WebSocket messages are also metered as data points, even though they do not increment the HTTP request counter Free keys have a **daily included cap**. Paid plans have **monthly included data points** and, on self-serve tiers, overage pricing after the included amount. Plans billed weekly meter a proportional weekly share of the tier's allowance instead — see [Weekly Billing](#weekly-billing) below. ### 2. Burst throttling Every key also has a per-second request ceiling. This is separate from billing and exists to prevent request storms. If you exceed that burst limit, the API returns `429 Too Many Requests` with a short `Retry-After` value, usually `1`. ### 3. WebSocket behavior WebSocket traffic does **not** increment your HTTP request counter, but it is still part of your metered usage. Treat subscriptions the same way you treat REST queries: filter aggressively and only subscribe to the sports, events, markets, and books you actually need. A `game_stats` frame costs one stats data point for each changed row across `team_stats[].stats` and `player_stats[].stats`; a zero-row terminal completion marker or invalidation fallback costs one stats data point. Heartbeats and in-band usage metadata are free. ## Current API Tier Defaults | Tier | Included data points | Overage | Burst limit | Data delay | WebSocket | | ---------- | -------------------- | ------------ | ------------ | ---------- | --------- | | Free | `20,000/day` | No overage | `1 req/sec` | `5 min` | No | | Starter | `5,000,000/mo` | `$0.002/pt` | `2 req/sec` | `60 sec` | No | | Pro | `25,000,000/mo` | `$0.001/pt` | `5 req/sec` | `30 sec` | No | | Ultra | `100,000,000/mo` | `$0.0005/pt` | `10 req/sec` | Real-time | Yes | | Super | `250,000,000/mo` | `$0.0004/pt` | `15 req/sec` | Real-time | Yes | | Mega | `500,000,000/mo` | `$0.0003/pt` | `20 req/sec` | Real-time | Yes | | Max | `2,500,000,000/mo` | `$0.0001/pt` | `50 req/sec` | Real-time | Yes | | Enterprise | Custom | Custom | Custom | Real-time | Yes | Sportsbook coverage, periods, history access, and other entitlements also vary by tier. Two to be aware of: **player prop markets require Starter or higher** (Free keys do not receive them), and **live game state, play-by-play, and live game-stat streams require Ultra or higher**. The live response headers are the best way to confirm what a specific API key can access right now. ## Weekly Billing Every paid API tier is also available on a **weekly billing cadence** for workloads that don't need a month-long commitment — for example, covering a single tournament or a few weeks of a season. Weekly plans are priced at a premium over the equivalent monthly plan (the commitment ladder is weekly > monthly > annual). On a weekly plan, your included data points are a **proportional weekly share** of the tier's monthly allowance (monthly × 12 ÷ 52 — e.g. Starter weekly includes `1,153,846` data points per week), metered over your 7-day billing window. Overage beyond the weekly allowance is billed at the same per-data-point rates as the monthly tier. The usage headers tell you which window applies to your key: `X-Datapoints-Period` reports `weekly` on weekly-billed plans (with `X-Datapoints-Reset` marking the end of the current 7-day window), `monthly` on monthly and annual plans, and `daily` on the Free tier. ## Usage Headers Metered responses include usage and entitlement headers you can surface in logs, dashboards, and upgrade prompts. | Header | Description | | ----------------------------- | ---------------------------------------------------------------------------------------------- | | `X-Datapoints` | Data points consumed by this response | | `X-Datapoints-Used` | Total data points used in the current billing window | | `X-Datapoints-Remaining` | Included data points remaining before the next reset | | `X-Datapoints-Limit` | Included data points for the current window | | `X-Datapoints-Period` | Cadence of the quota window: `daily` (Free tier), `weekly` (weekly-billed plans), or `monthly` | | `X-Datapoints-Reset` | ISO 8601 timestamp for the current usage window reset | | `X-Datapoints-Credit-Balance` | Bonus data-point credits applied on top of the allowance, when present | | `X-Datapoints-Credit-Expiry` | Expiry timestamp for the credit balance, when present | | `X-Tier` | Current subscription tier | | `X-Rate-Limit` | Allowed requests per second for this key | | `X-Data-Delay-Seconds` | Delay applied to returned data for this key | | `X-Bookmakers` | Sportsbook IDs this key can access, or `all` | | `X-Periods` | Period IDs this key can access, or `all` | | `X-History-Access` | Whether this key can access historical line data | | `X-Live-Odds-Access` | Whether this key can access live odds | | `X-Websocket-Access` | Whether this key can connect to WebSocket feeds | ```bash theme={null} curl -i -H "X-TheRundown-Key: YOUR_API_KEY" \ "https://therundown.io/api/v2/sports/4/events/2026-02-26?market_ids=1,2,3&affiliate_ids=19,23&main_line=true&offset=300" ``` Example headers: ```text theme={null} X-Datapoints: 452 X-Datapoints-Used: 120348 X-Datapoints-Remaining: 4879652 X-Datapoints-Limit: 5000000 X-Datapoints-Period: monthly X-Datapoints-Reset: 2026-03-31T00:00:00Z X-Tier: starter X-Rate-Limit: 2 X-Data-Delay-Seconds: 60 X-Bookmakers: 19,22,23 X-Periods: all X-History-Access: false X-Live-Odds-Access: false X-Websocket-Access: false ``` On `429` responses caused by billing caps, the billing headers remain useful. Read `Retry-After` and the `X-Datapoints-*` headers before deciding whether to retry or upgrade. ## Understanding `429 Too Many Requests` Not every `429` means the same thing. There are three common cases. ### 1. Burst rate limit exceeded You sent too many requests in a short interval for your tier. ```json theme={null} { "error": "Rate limit exceeded", "limit": 2, "upgrade_url": "/pricing/api", "upgrade_message": "You've exceeded your plan's rate limit. Upgrade for a higher requests-per-second limit." } ``` * Look at `Retry-After` before retrying. * Queue or batch work instead of firing many concurrent requests. * Switch recurring update loops to delta polling or WebSocket where appropriate. ### 2. Daily data-point cap reached This is the free-tier hard cap. ```json theme={null} { "error": "Daily data point limit reached", "limit": 20000, "used": 20000, "upgrade_url": "/pricing/api", "upgrade_message": "You've hit the free tier's daily limit. Upgrade to a paid plan for a higher monthly allowance and rate limits." } ``` * `Retry-After` tells you how long until the next daily window begins. * `X-Datapoints-Remaining` will be `0`. * The fix is to wait for reset or move to a paid plan. ### 3. Monthly data-point cap reached This appears on plans or accounts that are configured with a hard monthly cap. ```json theme={null} { "error": "Monthly data point limit reached", "limit": 25000000, "used": 25000000, "period": "monthly", "upgrade_url": "/pricing/api", "upgrade_message": "You've reached your plan's monthly data-point limit. Upgrade for a higher allowance, or enable overage billing to keep going." } ``` * Treat this as a billing-window issue, not a short retry. * Read `Retry-After` and `X-Datapoints-Reset`. * If this is unexpected on a paid account, contact support with the response headers. ## Best Practices ### Filter aggressively The biggest usage lever is almost always response shape. Start every integration by narrowing: * `market_ids` * `affiliate_ids` * `event_ids` * `main_line=true` * `hide_closed_markets=1` where available ### Use delta endpoints for ongoing updates Instead of fetching the full event list repeatedly, bootstrap once and poll: * `GET /api/v2/delta` for changed event objects * `GET /api/v2/markets/delta` for individual price changes See the [Efficient Polling guide](/guides/efficient-polling) for the full pattern. ### Use WebSocket on real-time tiers For live screens, line-movement monitoring, and live box scores, WebSocket is usually the best delivery model once your plan includes it. It avoids HTTP burst limits, but you should still keep subscriptions tight because pushed messages are metered. ### Cache reference data Sports, affiliates, teams, and market definitions change far less often than live odds. Cache them locally and refresh on a longer interval. ### Alert before users run out If you are building on behalf of end users or internal analysts, instrument alerts when usage reaches `70%`, `85%`, and `100%` of included data points. The headers above make this straightforward. ## Need More Throughput? If your workload needs more data points, higher burst limits, or real-time access, contact support at **[support@therundown.io](mailto:support@therundown.io)** or use your dashboard. Include: * Your API key (or the email associated with your account) * Your current tier * Approximate data-point volume per day or month * Required requests-per-second ceiling * Whether you need WebSocket, historical data, or broader sportsbook coverage * A short description of your application or workload # Data Model Source: https://docs.therundown.io/reference/data-model How events, markets, participants, lines, and prices relate to each other in the V2 API. The V2 API organizes sports data in a nested hierarchy. Understanding this structure is essential for parsing event responses, building odds screens, and processing delta updates. ## Hierarchy Overview ``` Event ├── score # Live score, game clock, status ├── teams[] # Away team (index 0), Home team (index 1) ├── schedule # Season info, event name └── markets[] # Array of market types ├── market_id # e.g., 1 = Moneyline, 2 = Spread, 3 = Total ├── period_id # 0 = full game, 1 = first half, etc. └── participants[] # Teams, players, or result types ├── id # Participant identifier ├── type # TYPE_TEAM, TYPE_PLAYER, or TYPE_RESULT └── lines[] # Available lines for this participant ├── id # 32-character hex line identifier ├── value # Line value (e.g., "-3.5") or empty for moneyline └── prices # Map of affiliate_id → price object {affiliate_id}: ├── id # Numeric string price identifier ├── price # American odds (e.g., -110, +150) ├── is_main_line # true if this is the consensus line └── updated_at # ISO 8601 timestamp ``` ## Event Object Each event represents a single game or match. Events are the top-level objects returned by `/api/v2/sports/{sportID}/events/{date}`. | Field | Type | Description | | ---------------------- | ------- | -------------------------------------------------------------------------------------------------------------- | | `event_id` | string | Canonical event identifier string. Use this value in V2 path params, filters, delta consumers, and cache keys. | | `sport_id` | integer | Sport identifier. See [Sport IDs](/reference/sports). | | `event_uuid` | string | Compatibility identifier retained for older integrations. Do not assume it matches `event_id`. | | `event_date` | string | Scheduled start time in ISO 8601 UTC | | `rotation_number_away` | integer | Away team rotation number (not used for soccer) | | `rotation_number_home` | integer | Home team rotation number (not used for soccer) | | `score` | object | Live score and game status. See [Score Object](#score-object) below. | | `teams` | array | Two-element array: `[away_team, home_team]`. See [Team Object](#team-object) below. | | `schedule` | object | Season metadata: `season_type`, `season_year`, `event_name`, `league_name` | | `markets` | array | Array of [Market objects](#market-object) with odds data | For V2 REST endpoints and WebSocket filters, pass the `event_id` value returned in event payloads. Do not substitute `event_uuid`. ### Score Object | Field | Type | Description | | ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- | | `event_status` | string | Current status (e.g., `STATUS_SCHEDULED`, `STATUS_IN_PROGRESS`, `STATUS_FINAL`). See [Event Statuses](/reference/event-statuses). | | `score_away` | integer | Away team score | | `score_home` | integer | Home team score | | `score_away_by_period` | array | Score breakdown by period | | `score_home_by_period` | array | Score breakdown by period | | `venue_name` | string | Arena or stadium name. May be an empty string when unavailable from the live feed. | | `venue_location` | string | City and state. May be an empty string when unavailable from the live feed. | | `game_clock` | integer | Game clock in seconds | | `display_clock` | string | Formatted clock display (e.g., "4:32") | | `game_period` | integer | Current period number | | `broadcast` | string | TV broadcast network | | `event_status_detail` | string | Human-readable status (e.g., "3rd Quarter - 4:32") | | `updated_at` | string | ISO 8601 timestamp of the last score update | Treat `score` as best-effort live feed data. Fields such as `venue_name`, `venue_location`, `broadcast`, `display_clock`, and the period arrays may be empty strings, empty arrays, or `0` until the upstream source provides them. Use `updated_at` to judge freshness. ### Team Object | Field | Type | Description | | -------------- | ------- | ---------------------------------------------------------------- | | `team_id` | integer | Normalized team identifier (stable across seasons and endpoints) | | `name` | string | Full team name (e.g., "Cleveland Cavaliers") | | `mascot` | string | Team mascot (e.g., "Cavaliers") | | `abbreviation` | string | Short abbreviation (e.g., "CLE") | | `record` | string | Current season record (e.g., "42-14") | | `is_away` | boolean | `true` if this is the away team | | `is_home` | boolean | `true` if this is the home team | ## Market Object Each market represents a type of bet (moneyline, spread, total, player prop, etc.). Markets are nested inside events. | Field | Type | Description | | -------------------- | ------- | ------------------------------------------------------------------------------------- | | `id` | integer | Instance identifier for this market on this event | | `market_id` | integer | Canonical market type ID. See [Market IDs](/reference/markets). | | `period_id` | integer | Period this market applies to. `0` = full game. See [Period IDs](/reference/periods). | | `name` | string | Display name (e.g., "Moneyline", "Total Over/Under") | | `market_description` | string | Human-readable description | | `participants` | array | Array of [Participant objects](#participant-object) | ## Participant Object Participants are the entities you can bet on within a market — teams, players, or result types (Over/Under). | Field | Type | Description | | ------- | ------- | ------------------------------------------------------------------------ | | `id` | integer | Participant identifier (team ID, player ID, or `0`/`1` for result types) | | `type` | string | `TYPE_TEAM`, `TYPE_PLAYER`, or `TYPE_RESULT` | | `name` | string | Display name (e.g., "Cleveland Cavaliers", "Donovan Mitchell", "Over") | | `lines` | array | Array of [Line objects](#line-object) | **`id` is the stable, joinable identifier — join on `id`, not `name`.** What `id` points to depends on `type`: * **`TYPE_TEAM`** — `id` is the normalized team ID. It is stable across seasons and endpoints, and matches `event.teams[].team_id`. Fetch the full team at `GET /api/v2/teams/{team_id}`. * **`TYPE_PLAYER`** — `id` is the player ID. Fetch the full player (team, names, position) at `GET /api/v2/players/{player_id}`. * **`TYPE_RESULT`** — `id` is a small outcome index (e.g. `0`/`1` for Over/Under) and is **not** a team or player resource key. Because every distinct team and player has a distinct `id`, joining on `id` resolves shared-name collisions that joining on `name` cannot. To look up an `id` from a name once, use the roster at `GET /api/v2/teams/{team_id}/players` or the team list at `GET /api/v2/sports/{sportID}/teams`. ## Line Object A line represents a specific betting line for a participant. For spreads and totals, the `value` contains the line number. For moneylines, `value` is an empty string. | Field | Type | Description | | -------- | ------ | ------------------------------------------------------------------------- | | `id` | string | 32-character hex identifier for this line | | `value` | string | Line value: `"-3.5"` for spread, `"224.5"` for total, `""` for moneyline | | `prices` | object | Map of affiliate ID → [Price object](#price-object). Keyed by sportsbook. | `line_value_is_participant` tells you where the meaningful selection detail lives. When it is `true`, the participant carries the selection and the line `value` may be a placeholder or label. When it is `false`, display the line `value` when present; it may be a number, threshold, method, round, or other outcome qualifier. ## Price Object A price is the odds offered by a single sportsbook for a specific line. | Field | Type | Description | | -------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Numeric string identifier for this price | | `price` | number | American odds (e.g., `-110`, `+150`). A value of `0.0001` means the line is off the board. See [Sentinel Values](/reference/sentinel-values). | | `is_main_line` | boolean | `true` if this is the primary/consensus line. Use `main_line=true` query param to filter to main lines only. | | `updated_at` | string | ISO 8601 timestamp of the last price update | Fields that appear in delta/history responses: | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------- | | `price_delta` | number | Difference from the previous price (present in some delta responses) | | `closed_at` | string | ISO 8601 timestamp when the line was closed. Empty string if still active. | ## Traversing the Data ### Reading a moneyline price ``` event.markets[0].participants[0].lines[0].prices["19"].price ``` Gives you the DraftKings (affiliate 19) moneyline price for the first participant (away team). ### Reading a spread value and price ``` event.markets[1].participants[0].lines[0].value → "-3.5" event.markets[1].participants[0].lines[0].prices["19"].price → -110 ``` ### Iterating all prices for an event ```python theme={null} for market in event["markets"]: for participant in market["participants"]: for line in participant["lines"]: for affiliate_id, price_obj in line["prices"].items(): print(f"{market['name']} | {participant['name']} | " f"{line['value']} | {affiliate_id}: {price_obj['price']}") ``` ## Delta Responses Delta endpoints return a different shape. Instead of the nested event → market → participant → line → price hierarchy, they return **flat change records**: | Field | Description | | ---------------- | --------------------------------------------- | | `event_id` | Which event changed | | `market_id` | Which market | | `participant_id` | Which participant | | `affiliate_id` | Which sportsbook | | `line` | Line value | | `price` | New price | | `previous_price` | Previous price (empty string if new) | | `change_type` | `"new"`, `"price_change"`, or `"line_change"` | Use the `event_id`, `market_id`, `participant_id`, and `affiliate_id` to locate the correct entry in your local cache and replace the price. See the [Efficient Polling guide](/guides/efficient-polling) for the full update pattern. # Event Status Codes Source: https://docs.therundown.io/reference/event-statuses Every event returned by the API includes a `status` field indicating its current state. Use these values to determine how to display or process an event in your application. | Status | Description | | -------------------------- | ----------------------------------------------------------------- | | `STATUS_SCHEDULED` | The event is scheduled but has not yet started. | | `STATUS_IN_PROGRESS` | The event is currently being played. | | `STATUS_FINAL` | The event has concluded and the final score is available. | | `STATUS_POSTPONED` | The event has been postponed to a later date or time. | | `STATUS_CANCELED` | The event has been permanently canceled and will not be played. | | `STATUS_SUSPENDED` | The event has been temporarily suspended during play. | | `STATUS_DELAYED` | The start of the event has been delayed. | | `STATUS_RAIN_DELAY` | The event is delayed specifically due to rain (common in MLB). | | `STATUS_HALFTIME` | The event is at halftime. | | `STATUS_END_PERIOD` | A period or quarter has just ended. | | `STATUS_END_OF_REGULATION` | Regulation time has ended; overtime may follow. | | `STATUS_OVERTIME` | The event is currently in overtime. | | `STATUS_FIRST_HALF` | The event is in the first half. | | `STATUS_SECOND_HALF` | The event is in the second half. | | `STATUS_FULL_TIME` | Full time reached (soccer). The match may continue to extra time. | | `STATUS_FINAL_AET` | Final after extra time (soccer). | | `STATUS_FINAL_PEN` | Final after penalty shootout (soccer). | | `STATUS_SHOOTOUT` | The event is in a shootout (hockey). | | `STATUS_FORFEIT` | The event was forfeited. | | `STATUS_TBD` | The event date/time is to be determined. | | `STATUS_NOT_AVAILABLE` | No status information is available for this event. | Additional sport-specific statuses may appear in API responses (e.g., MMA fight statuses, cricket-specific statuses). The statuses above cover the most common values. Use the `event_status_detail` field on the score object for a human-readable description of the current state. # Market IDs Source: https://docs.therundown.io/reference/markets Markets represent the type of bet available for an event. Each market has a numeric `market_id` used throughout the API. Markets are split into prematch and live/in-play categories. ## Prematch Markets | Market | ID | Description | | ------------------------- | ---- | ------------------------------------------------- | | Moneyline | 1 | Winner of the game | | Point Spread | 2 | Handicap/spread betting | | Total (Over/Under) | 3 | Combined score total | | Player Points | 29 | Player points scored | | Player Turnovers | 33 | Player turnovers | | Player Rebounds | 35 | Player rebounds | | Three Pointers | 38 | Player three-pointers made | | Player Assists | 39 | Player assists | | Double Double | 87 | Player double-double | | Triple Double | 88 | Player triple-double | | Player PRA | 93 | Points + Rebounds + Assists combo | | Team Totals | 94 | Individual team total score | | Player Blocks | 98 | Player blocks | | 3-Way Result (Regulation) | 563 | NHL 60-minute regulation-time home/draw/away line | | Player Points + Assists | 99 | PA combo | | Player Points + Rebounds | 297 | PR combo | | Player Rebounds + Assists | 298 | RA combo | | Tennis Set 1 Moneyline | 1150 | Winner of Set 1 | | Tennis Set 1 Spread | 1151 | Game handicap within Set 1 | | Tennis Set 1 Total | 1152 | Total games played in Set 1 | | Tennis Set 2 Moneyline | 1153 | Winner of Set 2 | ## Live / In-Play Markets Live markets are the in-play equivalents of prematch markets. They use separate IDs but map back to their prematch counterparts. | Market | ID | Prematch Equivalent | | ------------------------------ | ---- | ------------------- | | Live Moneyline | 41 | 1 | | Live Spread | 42 | 2 | | Live Total | 43 | 3 | | Live Player Points | 90 | 29 | | Live Player Assists | 91 | 39 | | Live Three Pointers | 92 | 38 | | Live Team Totals | 96 | 94 | | Live 3-Way Result | 1105 | 563 | | Live Player Rebounds | 982 | 35 | | Live Player Blocks | 983 | 98 | | Live Player Turnovers | 984 | 33 | | Live Double Double | 985 | 87 | | Live Triple Double | 986 | 88 | | Live Player PRA | 987 | 93 | | Live Player Points + Rebounds | 988 | 297 | | Live Player Points + Assists | 989 | 99 | | Live Player Rebounds + Assists | 990 | 298 | | Live Tennis Set 1 Moneyline | 1154 | 1150 | | Live Tennis Set 1 Spread | 1155 | 1151 | | Live Tennis Set 1 Total | 1156 | 1152 | | Live Tennis Set 2 Moneyline | 1157 | 1153 | ## Sport × Market Availability Not every market is available for every sport. Core markets (moneyline, spread, total) are broadly available, but player props and sport-specific markets vary. Use `GET /api/v2/sports/{sportID}/markets/{date}` to check which markets have active pricing on a given day. | Market | NFL | NBA | MLB | NHL | NCAAF | NCAAB | WNBA | UFC | Soccer | Tennis | | ---------------- | :-: | :-: | :-: | :-: | :---: | :---: | :--: | :-: | :----: | :----: | | Moneyline (1) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Spread (2) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ | ✓ | | Total (3) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Team Totals (94) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | – | ✓ | | Player Props | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | The player prop market IDs listed above (29, 33, 35, 38, 39, etc.) are basketball-centric examples. Each sport has its own set of prop markets — for example, MLB has Player Strikeouts, Player Hits, and Player Home Runs. Use the [market definitions endpoint](/api-reference/v2/markets) or `GET /api/v2/markets` to discover all available markets and their IDs. ## Sport-Specific Notes * **MLB**: Includes pitcher-specific markets. Starting pitcher information affects line availability — lines may show `0.0001` (off the board) until pitchers are confirmed. See [Sentinel Values](/reference/sentinel-values). * **Soccer** (IDs 10-19 and 33): The soccer 3-way moneyline (home/draw/away) is served on **Moneyline (1)** as three participants — the draw is a participant of the market, not a separate market. Soccer endpoints default `market_ids` to `1,2,3,563`. * **NHL** (ID 6): The 60-minute regulation-time line — a genuinely different bet from the full-game moneyline, since regulation excludes overtime and shootouts — is served as **3-Way Result (563)**, with live variant `1105`. NHL endpoints default `market_ids` to `1,2,3,563`. Alternate puck lines appear as **alternate lines under Spread (2)** rather than a separate market. * **Tennis** (IDs 38-39): Match markets use full-game periods (`0` prematch, `7` live). Set-specific markets use tennis-only market IDs such as `moneyline_first_set` (1150), `spread_first_set` (1151), and `totals_first_set` (1152) with set periods such as `3`, `4`, `15`, and `16`. * **UFC/MMA**: Does not have spread markets. Moneyline and total rounds are the primary markets. Method-of-victory and round betting are available as prop markets. * **CFL**: Supports core markets (moneyline, spread, total) but does **not** currently support player props. For the API endpoints that return market data, see the [Markets API reference](/api-reference/v2/markets). For how markets fit into the overall data hierarchy, see the [Data Model](/reference/data-model). ## Participant-Shaped vs. Line-Valued Markets Market definitions include `line_value_is_participant` to tell you where the meaningful selection detail lives. When `line_value_is_participant: true`, the market is participant-shaped: the participant carries the selection, and the line `value` may be a placeholder or label. Moneylines and Yes/No-style result markets commonly work this way. When `line_value_is_participant: false`, the line `value` is meaningful and should be displayed when present. This includes spreads, totals, player stat thresholds, and special markets where the detail is a method, round, or other outcome qualifier. For example, UFC method-of-victory markets use the fighter as the `participant` and expose the method, such as decision or KO/TKO, as the line `value`. # Period IDs Source: https://docs.therundown.io/reference/periods Periods define the time segment of a game that a market applies to. Each period has a numeric `period_id`. Prematch and live periods use separate ID ranges. ## Prematch Periods | Period | ID | Description | | ------------------ | -- | -------------------- | | Full Game | 0 | Prematch full game | | 1st Half | 1 | Prematch first half | | 2nd Half | 2 | Prematch second half | | 1st Quarter/Period | 3 | Prematch Q1 | | 2nd Quarter/Period | 4 | Prematch Q2 | | 3rd Quarter/Period | 5 | Prematch Q3 | | 4th Quarter/Period | 6 | Prematch Q4 | Tennis uses `period_id=0` for prematch full-match markets. Set-specific prematch markets reuse period IDs `3` and `4` for Set 1 and Set 2. ## Live / In-Play Periods | Period | ID | Description | | ---------------- | -- | ----------------- | | Live Full Game | 7 | In-play full game | | Live 1st Half | 13 | In-play 1st half | | Live 2nd Half | 14 | In-play 2nd half | | Live 1st Quarter | 15 | In-play Q1 | | Live 2nd Quarter | 16 | In-play Q2 | | Live 3rd Quarter | 17 | In-play Q3 | | Live 4th Quarter | 18 | In-play Q4 | Tennis uses `period_id=7` for live full-match markets. Live set-specific markets use `period_id=15` for Set 1 and `period_id=16` for Set 2. ## V1 Best-Line Period Names In the V1 best-line endpoint, periods are referenced by string names rather than numeric IDs. The mapping is as follows: | Numeric ID | V1 Period Name | | ---------- | ---------------------- | | 0 | `period_full_game` | | 1 | `period_first_half` | | 2 | `period_second_half` | | 3 | `period_first_period` | | 4 | `period_second_period` | | 5 | `period_third_period` | | 6 | `period_fourth_period` | # Sentinel Values Source: https://docs.therundown.io/reference/sentinel-values TheRundown API uses specific sentinel values to communicate special states within odds and line data. These values are intentional and must be handled explicitly in your application. ## Off the Board: 0.0001 A price or line value of `0.0001` means the market is **off the board** -- the sportsbook has temporarily removed this line from availability. This is **not** an error. It is a valid and expected state in the API response. ### Common Reasons * **Pending injury news** -- a key player's status is uncertain and the book is waiting for confirmation. * **Line adjustment** -- the sportsbook is recalculating the line due to sharp action or new information. * **Approaching game time** -- some books pull lines close to tip-off, kickoff, or first pitch. * **Regulatory or operational holds** -- the book may temporarily suspend the market for internal reasons. ### Best Practices * Display the line as **"Off Board"**, **"N/A"**, or hide it from the UI entirely. * Do **not** show `0.0001` to end users. * Do **not** treat `0.0001` as an actual price or use it in calculations (e.g., implied probability, expected value). * Continue polling the API -- the line may return to a valid price once the sportsbook re-posts it. ### Example In a V2 market response, a sentinel value appears in the `price` field of a price object: ```json theme={null} { "19": { "price": 0.0001, "is_main_line": true, "updated_at": "2026-02-12T18:30:00Z" } } ``` In this example, DraftKings (affiliate ID 19) has taken the line off the board. Your application should display this as unavailable rather than showing the raw value. In a V1 response, the sentinel appears in moneyline, spread, or total fields: ```json theme={null} { "moneyline": { "moneyline_away": 0.0001, "moneyline_home": 0.0001 } } ``` # Sports & Coverage Source: https://docs.therundown.io/reference/sports Each sport supported by TheRundown API is identified by a numeric `sport_id`. Use these IDs when calling endpoints that require a sport parameter (e.g., `/sports/{sport_id}/events`). ## Sport IDs | Sport | ID | Abbreviation | | ---------------------- | -- | ------------ | | NCAA Football | 1 | NCAAF | | NFL | 2 | NFL | | MLB | 3 | MLB | | NBA | 4 | NBA | | NCAA Basketball | 5 | NCAAB | | NHL | 6 | NHL | | UFC/MMA | 7 | UFC | | WNBA | 8 | WNBA | | CFL | 9 | CFL | | MLS | 10 | MLS | | English Premier League | 11 | EPL | | Ligue 1 | 12 | FRA1 | | Bundesliga | 13 | GER1 | | La Liga | 14 | ESP1 | | Serie A | 15 | ITA1 | | UEFA Champions League | 16 | UEFA.CHAMP | | UEFA Euro | 17 | UEFA.EURO | | FIFA | 18 | FIFA | | J-League | 19 | JPN1 | | IPL | 20 | IPL | | T20 | 21 | T20 | | Politics | 22 | Politics | | UEFA Europa League | 33 | UEFA.EL | | ATP Tennis | 38 | TENNIS.ATP | | WTA Tennis | 39 | TENNIS.WTA | | PGA Tour Golf | 40 | GOLF.PGA | | Formula 1 | 41 | RACING.F1 | Use `GET /api/v2/sports` to get the current list of all available sports and their IDs. ## Season-Specific Sports Preseason, playoff, and special-event games have their own sport IDs, separate from their parent league's regular season. This allows you to filter or subscribe to specific parts of a season independently. | Sport | ID | Parent | | ------------------- | -- | ------- | | NBA Preseason | 23 | NBA (4) | | NBA Playoffs | 24 | NBA (4) | | NFL Preseason | 25 | NFL (2) | | NFL Playoffs | 26 | NFL (2) | | NHL Preseason | 27 | NHL (6) | | NHL Playoffs | 28 | NHL (6) | | MLB Spring Training | 30 | MLB (3) | | MLB Playoffs | 31 | MLB (3) | | NBA Summer League | 32 | NBA (4) | **MLB Spring Training migration (February 2026):** MLB Spring Training games have moved from sport ID `3` (MLB) to sport ID `30`. If you previously consumed spring training events under `3`, update your integration to use `30`. Regular-season MLB games remain under sport ID `3`. Season-specific sports share the same feature set and data model as their parent sport. For example, MLB Spring Training (30) supports the same markets, stats, and endpoints as MLB (3). ## Feature Coverage | Feature | NCAAF | NFL | MLB | NBA | NCAAB | NHL | UFC | WNBA | CFL | Soccer (10-19, 33) | Tennis (38-39) | | ----------------- | :---: | :-: | :-: | :-: | :---: | :-: | :-: | :--: | :-: | :----------------: | :------------: | | Events & Scores | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Pre-match Odds | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Live In-play Odds | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ | ✓ | | Player Props | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | – | ✓ | – | | Team Stats | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Player Stats | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Game Stats | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Teams & Rosters | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Schedules | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Opening Lines | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Closing Lines | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Odds History | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Pitcher Data | – | – | ✓ | – | – | – | – | – | – | – | – | | WebSocket Push | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | A **–** in the table means the feature is not available for that sport and there are no current plans to add it. Live team and player box-score changes for supported in-progress events are available through the [`stats` WebSocket channel](/api-reference/v2/websocket#game-stats-messages). Because the stream is delta-only, use the REST game-stat endpoints to load current state when connecting or recovering. Soccer leagues (IDs 10-19 and 33) do not use rotation numbers. Event IDs are generated from team IDs and match dates instead of the standard rotation number system used by US sportsbooks. For example, a soccer match between two teams will have an event ID derived from those team IDs and the match date, rather than a rotation number pair like `501`/`502` used in NFL or NBA. ## Soccer Leagues All soccer leagues share the same data model and endpoints. Team stats, player stats, and game stats are available for all leagues. | League | ID | | ---------------------------------- | -- | | MLS (Major League Soccer) | 10 | | EPL (English Premier League) | 11 | | Ligue 1 (French First Division) | 12 | | Bundesliga (German First Division) | 13 | | La Liga (Spanish First Division) | 14 | | Serie A (Italian First Division) | 15 | | UEFA Champions League | 16 | | UEFA Euro | 17 | | FIFA (International Tournaments) | 18 | | J-League (Japanese First Division) | 19 | | UEFA Europa League | 33 | Soccer events use additional statuses not found in other sports: `STATUS_FULL_TIME`, `STATUS_FINAL_AET` (after extra time), `STATUS_FINAL_PEN` (after penalties), `STATUS_ABANDONED`, and `STATUS_HALFTIME_ET`. See [Event Statuses](/reference/event-statuses) for details. ## Tennis Leagues ATP and WTA tennis are separate sport IDs so integrations can request each tour independently. | League | ID | | ---------- | -- | | ATP Tennis | 38 | | WTA Tennis | 39 | Tennis players are represented through the same home/away event model as team sports. Full-match tennis markets use full-game periods (`0` prematch, `7` live). Set-specific markets use period IDs to identify the set, such as `3` for prematch Set 1, `4` for prematch Set 2, `15` for live Set 1, and `16` for live Set 2. For the API endpoints that return events by sport, see the [Events API reference](/api-reference/v2/events). For answers to common questions about sports, IDs, and data coverage, see the [FAQ](/faq). ## Approximate Season Windows Use this as a rough guide for when each sport's regular season is active. Preseason and playoff windows extend before and after these ranges respectively. Dates vary by year. | Sport | Regular Season | Notes | | -------------- | -------------- | ---------------------------------------- | | NFL | Sep – Jan | 18-week regular season | | NBA | Oct – Apr | Playoffs run Apr–Jun | | MLB | Mar – Sep | Spring training starts Feb (sport ID 30) | | NHL | Oct – Apr | Playoffs run Apr–Jun | | NCAAF | Aug – Dec | Bowl games Dec–Jan | | NCAAB | Nov – Mar | March Madness in Mar–Apr | | WNBA | May – Sep | | | CFL | Jun – Nov | Grey Cup in Nov | | UFC | Year-round | Events most weekends | | MLS | Feb – Oct | | | EPL | Aug – May | | | Soccer (other) | Varies | See individual league schedules | | ATP Tennis | Year-round | Tour calendar varies by tournament | | WTA Tennis | Year-round | Tour calendar varies by tournament | # Sportsbook / Affiliate IDs Source: https://docs.therundown.io/reference/sportsbooks Each sportsbook tracked by TheRundown API is identified by a numeric `affiliate_id`. These are the values returned in API responses within odds/line objects and are used with the `affiliate_ids` filter parameter to request lines from specific books. | Sportsbook | Affiliate ID | | ------------- | ------------ | | Bovada | 2 | | Pinnacle | 3 | | Sportsbetting | 4 | | BetOnline | 6 | | LowVig | 11 | | Bodog | 12 | | Intertops | 14 | | Matchbook | 16 | | YouWager | 18 | | DraftKings | 19 | | Unibet | 21 | | BetMGM | 22 | | FanDuel | 23 | | theScore Bet | 24 | | Kalshi | 25 | | Polymarket | 26 | | Hard Rock Bet | 28 | These IDs are used in the `affiliate_ids` query parameter to filter which sportsbooks are included in the response. For example, `affiliate_ids=19,23` returns lines from DraftKings and FanDuel only. Hard Rock Bet (28) currently reports `status: beta` on `GET /api/v2/affiliates` — coverage is expanding and may change while the integration matures. Each affiliate's current integration health is returned in the `status` field (`healthy`, `unhealthy`, or `beta`). # Go SDK Source: https://docs.therundown.io/sdks/go Use TheRundown API from Go with net/http for REST calls and gorilla/websocket for real-time streaming. An official Go SDK is coming soon. In the meantime, this guide shows how to use TheRundown API directly with the standard `net/http` package and `gorilla/websocket` for WebSocket connections. ## Installation No external dependencies are required for REST calls. For WebSocket support: ```bash theme={null} go get github.com/gorilla/websocket ``` ## Configuration ```go theme={null} package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "time" ) const ( baseURL = "https://therundown.io/api/v2" wsURL = "wss://therundown.io/api/v2/ws/markets" ) var apiKey = os.Getenv("THERUNDOWN_API_KEY") ``` ## Helper Function ```go theme={null} func apiGet(path string, params map[string]string) ([]byte, error) { u, err := url.Parse(baseURL + path) if err != nil { return nil, err } q := u.Query() q.Set("key", apiKey) for k, v := range params { q.Set(k, v) } u.RawQuery = q.Encode() resp, err := http.Get(u.String()) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusTooManyRequests { retryAfter := resp.Header.Get("Retry-After") if retryAfter == "" { retryAfter = "60" } return nil, fmt.Errorf("rate limited, retry after %s seconds", retryAfter) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API error: %d %s", resp.StatusCode, resp.Status) } return io.ReadAll(resp.Body) } ``` ## Getting Sports ```go theme={null} type Sport struct { SportID int `json:"sport_id"` SportName string `json:"sport_name"` } type SportsResponse struct { Sports []Sport `json:"sports"` } func getSports() ([]Sport, error) { body, err := apiGet("/sports", nil) if err != nil { return nil, err } var resp SportsResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, err } return resp.Sports, nil } func main() { sports, err := getSports() if err != nil { fmt.Printf("Error: %v\n", err) return } for _, s := range sports { fmt.Printf("%d: %s\n", s.SportID, s.SportName) } } ``` ## Getting Events with Odds ```go theme={null} type Price struct { Price float64 `json:"price"` IsMainLine bool `json:"is_main_line"` UpdatedAt string `json:"updated_at"` } type Line struct { Value string `json:"value,omitempty"` Prices map[string]Price `json:"prices"` } type Participant struct { ID int `json:"id"` Type string `json:"type"` Name string `json:"name"` Lines []Line `json:"lines"` } type Market struct { MarketID int `json:"market_id"` Name string `json:"name"` PeriodID int `json:"period_id"` Participants []Participant `json:"participants"` } type Team struct { TeamID int `json:"team_id"` Name string `json:"name"` } type Event struct { EventID string `json:"event_id"` SportID int `json:"sport_id"` Teams []Team `json:"teams"` Markets []Market `json:"markets"` } type EventsResponse struct { Events []Event `json:"events"` } func getEvents(sportID int, date string) ([]Event, error) { path := fmt.Sprintf("/sports/%d/events/%s", sportID, date) body, err := apiGet(path, map[string]string{ "market_ids": "1,2,3", "affiliate_ids": "19,23", "main_line": "true", }) if err != nil { return nil, err } var resp EventsResponse if err := json.Unmarshal(body, &resp); err != nil { return nil, err } return resp.Events, nil } func formatPrice(p float64) string { if p == 0.0001 { return "N/A" } if p > 0 { return fmt.Sprintf("+%d", int(p)) } return fmt.Sprintf("%d", int(p)) } func main() { today := time.Now().Format("2006-01-02") events, err := getEvents(4, today) if err != nil { fmt.Printf("Error: %v\n", err) return } for _, event := range events { away := event.Teams[0].Name home := event.Teams[1].Name fmt.Printf("\n%s @ %s\n", away, home) for _, market := range event.Markets { fmt.Printf(" %s:\n", market.Name) for _, p := range market.Participants { for _, line := range p.Lines { for affID, price := range line.Prices { lineStr := "" if line.Value != "" { lineStr = fmt.Sprintf(" (%s)", line.Value) } fmt.Printf(" %s%s: %s @ %s\n", p.Name, lineStr, formatPrice(price.Price), affID) } } } } } } ``` ## WebSocket Streaming ```go theme={null} package main import ( "encoding/json" "fmt" "log" "math" "math/rand" "net/url" "os" "os/signal" "time" "github.com/gorilla/websocket" ) type WSMeta struct { Type string `json:"type"` Timestamp string `json:"timestamp"` } type WSMessage struct { EventID string `json:"event_id"` SportID int `json:"sport_id"` MarketID int `json:"market_id"` MarketName string `json:"market_name"` Participants []Participant `json:"participants"` Meta WSMeta `json:"meta"` } func connectWebSocket() { u, _ := url.Parse(wsURL) q := u.Query() q.Set("key", apiKey) q.Set("sport_ids", "4") q.Set("market_ids", "1,2,3") u.RawQuery = q.Encode() interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) reconnectDelay := 1.0 maxDelay := 30.0 for { conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { jitter := rand.Float64() delay := math.Min(reconnectDelay+jitter, maxDelay) log.Printf("Connection failed: %v. Retrying in %.1fs...", err, delay) time.Sleep(time.Duration(delay * float64(time.Second))) reconnectDelay = math.Min(reconnectDelay*2, maxDelay) continue } log.Println("WebSocket connected") reconnectDelay = 1.0 done := make(chan struct{}) go func() { defer close(done) for { _, message, err := conn.ReadMessage() if err != nil { log.Printf("Read error: %v", err) return } var msg WSMessage if err := json.Unmarshal(message, &msg); err != nil { continue } if msg.Meta.Type == "heartbeat" { continue } fmt.Printf("Update: %s - %s\n", msg.EventID, msg.MarketName) for _, p := range msg.Participants { for _, line := range p.Lines { for affID, price := range line.Prices { fmt.Printf(" %s: %s @ %s\n", p.Name, formatPrice(price.Price), affID) } } } } }() select { case <-done: log.Println("Connection lost, reconnecting...") conn.Close() case <-interrupt: log.Println("Shutting down...") conn.WriteMessage( websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), ) conn.Close() return } } } func main() { connectWebSocket() } ``` ## Error Handling with Retry ```go theme={null} func apiGetWithRetry(path string, params map[string]string, maxRetries int) ([]byte, error) { for attempt := 0; attempt < maxRetries; attempt++ { body, err := apiGet(path, params) if err == nil { return body, nil } // Check if rate limited if err.Error()[:12] == "rate limited" { wait := time.Duration(math.Pow(2, float64(attempt))) * time.Second jitter := time.Duration(rand.Intn(1000)) * time.Millisecond log.Printf("Rate limited. Retrying in %v...", wait+jitter) time.Sleep(wait + jitter) continue } return nil, err } return nil, fmt.Errorf("max retries exceeded") } ``` ## Next Steps Detailed guide on fetching odds Real-time data streaming guide All authentication methods Rate limit details and best practices # JavaScript SDK Source: https://docs.therundown.io/sdks/javascript Use TheRundown API from JavaScript and TypeScript with the Fetch API and WebSocket class. An official JavaScript/TypeScript SDK is coming soon. In the meantime, this guide shows how to use TheRundown API directly with the native Fetch API and WebSocket class -- no dependencies required. ## Configuration ```javascript theme={null} const API_KEY = process.env.THERUNDOWN_API_KEY || "YOUR_API_KEY"; const BASE_URL = "https://therundown.io/api/v2"; const WS_URL = "wss://therundown.io/api/v2/ws/markets"; ``` ## Helper Function ```javascript theme={null} async function apiGet(path, params = {}) { const url = new URL(`${BASE_URL}${path}`); url.searchParams.set("key", API_KEY); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } const response = await fetch(url.toString()); if (response.status === 429) { throw new Error("Rate limited. Check Retry-After header."); } if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } return response.json(); } ``` ## Getting Sports ```javascript theme={null} const { sports } = await apiGet("/sports"); for (const sport of sports) { console.log(`${sport.sport_id}: ${sport.sport_name}`); } ``` ## Getting Events with Odds ```javascript theme={null} const today = new Date().toISOString().split("T")[0]; const data = await apiGet(`/sports/4/events/${today}`, { market_ids: "1,2,3", // Moneyline, Spread, Total affiliate_ids: "19,23", // DraftKings, FanDuel main_line: "true", }); function formatPrice(price) { if (price === 0.0001) return "N/A"; return price > 0 ? `+${Math.round(price)}` : String(Math.round(price)); } for (const event of data.events) { const away = event.teams[0].name; const home = event.teams[1].name; console.log(`\n${away} @ ${home}`); for (const market of event.markets || []) { console.log(` ${market.name}:`); for (const participant of market.participants) { for (const line of participant.lines) { const lineStr = line.value ? ` (${line.value})` : ""; const prices = Object.entries(line.prices) .map(([id, p]) => `${formatPrice(p.price)} @${id}`) .join(" "); console.log(` ${participant.name}${lineStr}: ${prices}`); } } } } ``` ## Filtering by Affiliate ```javascript theme={null} // Only FanDuel lines const data = await apiGet(`/sports/4/events/${today}`, { market_ids: "1,2,3", affiliate_ids: "23", // FanDuel only main_line: "true", }); ``` ## Getting Player Props ```javascript theme={null} const props = await apiGet(`/sports/4/events/${today}`, { market_ids: "29,35,38,39", // Points, Rebounds, 3PT, Assists affiliate_ids: "19", }); for (const event of props.events) { console.log(`\n${event.teams[0].name} @ ${event.teams[1].name}`); for (const market of event.markets || []) { console.log(` ${market.name}:`); for (const participant of market.participants) { for (const line of participant.lines) { const price = line.prices["19"]?.price ?? "N/A"; console.log(` ${participant.name}: ${line.value} (${price})`); } } } } ``` ## Historical Odds ```javascript theme={null} const eventId = "abc123"; // Full market history const data = await apiGet(`/events/${eventId}/markets/history`, { affiliate_ids: "19", }); for (const entry of data.history) { console.log( `${entry.updated_at}: line=${entry.line} price=${entry.price} (${entry.change_type})` ); } // Opening lines const openers = await apiGet(`/events/${eventId}/openers`, { market_ids: "1,2,3", }); // Closing lines const closing = await apiGet(`/events/${eventId}/closing`, { market_ids: "1,2,3", }); ``` ## WebSocket Streaming ```javascript theme={null} function connectWebSocket(options = {}) { const { sportIds = "4", marketIds = "1,2,3", onUpdate } = options; const url = `${WS_URL}?key=${API_KEY}&sport_ids=${sportIds}&market_ids=${marketIds}`; const ws = new WebSocket(url); ws.onopen = () => { console.log("WebSocket connected"); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); // Skip heartbeats if (msg.meta?.type === "heartbeat") return; if (onUpdate) { onUpdate(msg.data); } else { const d = msg.data; console.log(`Update: event=${d.event_id} market=${d.market_id} price=${d.price}`); } }; ws.onerror = (error) => { console.error("WebSocket error:", error); }; ws.onclose = (event) => { console.log(`WebSocket closed: ${event.code}`); }; return ws; } // Usage const ws = connectWebSocket({ sportIds: "4", marketIds: "1,2,3", onUpdate: (update) => { console.log( `Event ${update.event_id}: market=${update.market_id} ` + `aff=${update.affiliate_id} price=${update.price}` ); }, }); ``` ## WebSocket with Auto-Reconnect ```javascript theme={null} function createReconnectingSocket(options = {}) { const { sportIds = "4", marketIds = "1,2,3", onUpdate, onConnect, maxDelay = 30000, } = options; let ws; let reconnectDelay = 1000; let closed = false; function connect() { if (closed) return; const url = `${WS_URL}?key=${API_KEY}&sport_ids=${sportIds}&market_ids=${marketIds}`; ws = new WebSocket(url); ws.onopen = () => { console.log("WebSocket connected"); reconnectDelay = 1000; if (onConnect) onConnect(); }; ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (msg.meta?.type === "heartbeat") return; if (onUpdate) onUpdate(msg.data); }; ws.onclose = () => { if (closed) return; const jitter = Math.random() * 1000; const delay = Math.min(reconnectDelay + jitter, maxDelay); console.log(`Reconnecting in ${Math.round(delay)}ms...`); setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 2, maxDelay); connect(); }, delay); }; ws.onerror = () => { ws.close(); }; } connect(); return { close() { closed = true; ws?.close(); }, }; } // Usage const connection = createReconnectingSocket({ sportIds: "4", marketIds: "1,2,3", onUpdate: (update) => { console.log(`Update: event=${update.event_id} market=${update.market_id} price=${update.price}`); }, onConnect: () => { console.log("Ready to receive updates"); }, }); // Later: connection.close(); ``` ## Error Handling with Retry ```javascript theme={null} async function apiGetWithRetry(path, params = {}, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const url = new URL(`${BASE_URL}${path}`); url.searchParams.set("key", API_KEY); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } const response = await fetch(url.toString()); if (response.ok) { return response.json(); } if (response.status === 429) { const retryAfter = Number(response.headers.get("Retry-After") || "0"); const wait = retryAfter > 0 ? retryAfter * 1000 : Math.pow(2, attempt) * 1000 + Math.random() * 1000; console.log(`Rate limited. Retrying in ${Math.round(wait)}ms...`); await new Promise((resolve) => setTimeout(resolve, wait)); continue; } throw new Error(`API error: ${response.status} ${response.statusText}`); } throw new Error("Max retries exceeded"); } ``` ## TypeScript Types If you are using TypeScript, here are type definitions for the core response objects: ```typescript theme={null} interface Sport { sport_id: number; sport_name: string; } interface Team { team_id: number; name: string; } interface Price { price: number; is_main_line: boolean; updated_at: string; } interface Line { value: string; prices: Record; } interface Participant { id: number; type: string; name: string; lines: Line[]; } interface Market { market_id: number; name: string; period_id: number; participants: Participant[]; } interface Event { event_id: string; sport_id: number; teams: Team[]; markets: Market[]; } interface EventsResponse { events: Event[]; } interface WebSocketMeta { type: "market_price" | "heartbeat"; version: string; timestamp: number; } interface MarketPriceUpdate { id: number; event_id: string; affiliate_id: number; market_participant_id: number; market_id: number; line: string; price: string; previous_price: string; price_delta: number; is_main_line: boolean; normalized_market_participant_id: number; normalized_market_participant_type: number; sport_id: number; updated_at: string; } interface WebSocketMessage { meta: WebSocketMeta; data: MarketPriceUpdate; } ``` ## Next Steps Detailed guide on fetching odds Real-time data streaming guide All authentication methods Rate limit details and best practices # Python SDK Source: https://docs.therundown.io/sdks/python Use TheRundown API from Python with the requests library and websockets for real-time data. An official Python SDK is coming soon. In the meantime, this guide shows how to use TheRundown API directly with the `requests` library for REST calls and `websockets` for real-time streaming. ## Installation ```bash theme={null} pip install requests websockets ``` ## Configuration ```python theme={null} import os API_KEY = os.environ.get("THERUNDOWN_API_KEY", "YOUR_API_KEY") BASE_URL = "https://therundown.io/api/v2" WS_URL = "wss://therundown.io/api/v2/ws/markets" HEADERS = { "X-TheRundown-Key": API_KEY, } ``` ## Getting Sports ```python theme={null} import requests response = requests.get(f"{BASE_URL}/sports", headers=HEADERS) response.raise_for_status() sports = response.json()["sports"] for sport in sports: print(f"{sport['sport_id']}: {sport['sport_name']}") ``` ## Getting Events with Odds ```python theme={null} import requests from datetime import date sport_id = 4 # NBA today = date.today().isoformat() response = requests.get( f"{BASE_URL}/sports/{sport_id}/events/{today}", headers=HEADERS, params={ "market_ids": "1,2,3", # Moneyline, Spread, Total "affiliate_ids": "19,23", # DraftKings, FanDuel "main_line": "true", } ) response.raise_for_status() data = response.json() for event in data["events"]: away = event["teams"][0]["name"] home = event["teams"][1]["name"] print(f"\n{away} @ {home}") for market in event.get("markets", []): print(f" {market['name']}:") for participant in market["participants"]: for line in participant["lines"]: for aff_id, price in line["prices"].items(): p = price["price"] display = "N/A" if p == 0.0001 else f"{p:+d}" if p > 0 else str(int(p)) line_val = f" ({line['value']})" if line.get("value") else "" print(f" {participant['name']}{line_val}: {display} @ {aff_id}") ``` ## Filtering by Affiliate ```python theme={null} # Only DraftKings lines response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", headers=HEADERS, params={ "market_ids": "1,2,3", "affiliate_ids": "19", # DraftKings only "main_line": "true", } ) ``` ## Getting Player Props ```python theme={null} # NBA player points, rebounds, assists, 3PT response = requests.get( f"{BASE_URL}/sports/4/events/{date.today()}", headers=HEADERS, params={ "market_ids": "29,35,38,39", # Points, Rebounds, 3PT, Assists "affiliate_ids": "19", } ) response.raise_for_status() for event in response.json()["events"]: print(f"\n{event['teams'][0]['name']} @ {event['teams'][1]['name']}") for market in event.get("markets", []): print(f" {market['name']}:") for participant in market["participants"]: for line in participant["lines"]: price = line["prices"].get("19", {}).get("price", "N/A") print(f" {participant['name']}: {line.get('value')} ({price})") ``` ## Historical Odds ```python theme={null} event_id = "abc123" # Full market history response = requests.get( f"{BASE_URL}/events/{event_id}/markets/history", headers=HEADERS, params={"affiliate_ids": "19"}, ) history = response.json()["history"] for entry in history: print(f"{entry['updated_at']}: line={entry['line']} price={entry['price']} ({entry['change_type']})") # Opening lines openers = requests.get( f"{BASE_URL}/events/{event_id}/openers", headers=HEADERS, params={"market_ids": "1,2,3"}, ).json() # Closing lines closing = requests.get( f"{BASE_URL}/events/{event_id}/closing", headers=HEADERS, params={"market_ids": "1,2,3"}, ).json() ``` ## WebSocket Streaming ```python theme={null} import asyncio import json import websockets async def stream_odds(sport_ids="4", market_ids="1,2,3"): url = ( f"{WS_URL}?key={API_KEY}" f"&sport_ids={sport_ids}" f"&market_ids={market_ids}" ) async with websockets.connect(url) as ws: print("Connected to WebSocket") async for message in ws: msg = json.loads(message) if msg.get("meta", {}).get("type") == "heartbeat": continue d = msg["data"] print( f"Update: event={d['event_id']} market={d['market_id']}" f" aff={d['affiliate_id']} price={d['price']}" ) asyncio.run(stream_odds()) ``` ## WebSocket with Reconnection ```python theme={null} import asyncio import json import random import websockets async def stream_with_reconnect(sport_ids="4", market_ids="1,2,3"): url = ( f"{WS_URL}?key={API_KEY}" f"&sport_ids={sport_ids}" f"&market_ids={market_ids}" ) reconnect_delay = 1.0 max_delay = 30.0 while True: try: async with websockets.connect(url) as ws: print("WebSocket connected") reconnect_delay = 1.0 async for message in ws: msg = json.loads(message) if msg.get("meta", {}).get("type") == "heartbeat": continue # Process update d = msg["data"] print(f"Update: event={d['event_id']} market={d['market_id']} price={d['price']}") except (websockets.ConnectionClosed, ConnectionError) as e: jitter = random.uniform(0, 1) delay = min(reconnect_delay + jitter, max_delay) print(f"Disconnected ({e}). Reconnecting in {delay:.1f}s...") await asyncio.sleep(delay) reconnect_delay = min(reconnect_delay * 2, max_delay) asyncio.run(stream_with_reconnect()) ``` ## Error Handling ```python theme={null} import requests import time def api_get(path, params=None, max_retries=3): """Make an API request with retry logic for rate limits.""" for attempt in range(max_retries): response = requests.get( f"{BASE_URL}{path}", headers=HEADERS, params=params, ) if response.status_code == 200: return response.json() if response.status_code == 429: wait = (2 ** attempt) + random.uniform(0, 1) print(f"Rate limited. Retrying in {wait:.1f}s...") time.sleep(wait) continue response.raise_for_status() raise Exception("Max retries exceeded") # Usage data = api_get( f"/sports/4/events/{date.today()}", params={"market_ids": "1,2,3"}, ) ``` ## Next Steps Detailed guide on fetching odds Real-time data streaming guide All authentication methods Rate limit details and best practices