openapi: 3.2.0 info: title: Odds Sports exchange API description: 'The Odds API V1 provides sports and racing events, bookmaker odds, exchange and prediction-market order books, live price updates, betting opportunity snapshots, and result lookups for production integrations.' version: 1.0.0 license: name: Proprietary url: https://odds-api.net x-workflow-examples: - name: positive_ev_request summary: Find positive EV bets for a league. steps: - GET /v1/events?sport=rugby-league&league=NRL - GET /v1/bets/snapshot?strategies=pos_ev - Use response timestamps and stake/risk controls before showing user-facing picks. - name: arbitrage_request summary: Find arbitrage opportunities across allowed bookmakers. steps: - GET /v1/bookmakers - GET /v1/bets/snapshot?strategies=arbitrage - Show execution-risk caveats; never present arbitrage as guaranteed after limits, voids, or delays. - name: odds_history_request summary: Get line movement for a market. steps: - GET /v1/events/{event_id}/odds/snapshot - Pick a selection_key from an odds line. - GET /v1/events/{event_id}/odds/history?selection_key=... - name: prediction_market_orderbook_request summary: Read and follow a curated prediction-market order book. steps: - GET /v1/events?sport=basketball&league=NBA - GET /v1/events/{event_id}/prediction-markets/orderbook/snapshot?providers=polymarket,kalshi&depth=3 - Connect to the matching /stream or /ws route with since=. x-responsible-gambling: Do not present betting outcomes as risk-free. Always mention execution risk, stale prices, bookmaker limits, voids, delays, and jurisdictional availability in user-facing products. x-production-integration: recommended_flow: - Authenticate backend requests with X-API-Key. - Discover sports, leagues, and bookmakers before storing local filters. - Page sports or racing events with limit and cursor. - Fetch odds snapshots for initial state. Explicit bookmaker filters are complete in one response; otherwise follow bookmaker-complete pages until complete=true. Persist as_of_ts_ms, ttl_seconds, and resume. - Use SSE or WebSocket streams for realtime updates and reconnect with since=. - Use history endpoints for line movement and results endpoints after events finish. recommended_polling_intervals: catalog_metadata: 6-24h sports_events: 5-15m, or 1-5m for active leagues and near-start windows racing_events: 1-5m with narrow time windows odds_snapshots_without_streams: 60-120s, or 15-30s for priority events with strict quota controls betting_opportunity_snapshots: 30-120s depending on alert urgency results_after_start: 1-5m until settled, then back off stream_reconnects: - Snapshot first, then connect to /stream or /ws with matching filters. - Apply delta messages idempotently and persist the newest resume token. - Use heartbeat messages as liveness checks. - Reconnect with jittered exponential backoff and since=. - Reload the snapshot when a resync message indicates the resume token is unavailable. rate_limit_backoff: - Honor Retry-After on 429 when present. - Otherwise start around 2 seconds and double up to about 60 seconds with jitter. - Use X-RateLimit-* response headers when present to tune callers. - Use /usage to detect monthly API-credit quota exhaustion and stop retry loops. cache_strategy: - Cache by endpoint path, event ID, normalized filters, page cursor, and product context. - Respect ttl_seconds when present and always surface as_of_ts_ms. - Use streams to update hot caches and reload snapshots after resync or parse failure. - Prefer stale-while-revalidate displays with a visible timestamp. failure_modes: - 400 invalid request shape, cursor, timestamp, or filter. - 401 missing or invalid API key. - 403 key lacks plan, product, bookmaker, stream, racing, or strategy access. - 404 event, race, result, or selection is unavailable. - 429 rate limit or monthly quota exhaustion. - 5xx transient service failure; retry with backoff and keep last good cached data marked stale. - Stream close or resync; reconnect with since or reload the snapshot. servers: - url: https://api.odds-api.net/v1 description: Production V1 base URL. Set ODDS_API_BASE_URL to override it in SDKs and examples. security: - ApiKeyAuth: [] tags: - name: Sports exchange description: Sports betting exchange order books with back/lay ladders, liquidity, and live updates. paths: /events/{event_id}/exchange/orderbook/snapshot: get: tags: - Sports exchange summary: 'Event exchange order book: Snapshot' operationId: exchange_orderbook_snapshot_events__event_id__exchange_orderbook_snapshot_get security: - ApiKeyAuth: [] parameters: - name: event_id in: path required: true schema: type: string title: Event Id description: Canonical event or race identifier from an event list response. - name: exchanges in: query required: false schema: anyOf: - type: string - type: 'null' description: 'Comma-separated exchange filter: betdaq, betfair, smarkets, matchbook.' title: Exchanges description: Comma-separated exchange allow-list. Supported values are `betdaq`, `betfair`, `smarkets`, and `matchbook`. - name: market_keys in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated market_key allow-list. title: Market Keys description: Comma-separated market key allow-list for odds filters. - name: selection_keys in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated selection_key allow-list. title: Selection Keys description: Comma-separated stable selection identifiers from an exchange order book or odds snapshot. - name: depth in: query required: false schema: type: integer maximum: 20 minimum: 1 description: Price levels per back/lay side. default: 3 title: Depth description: Number of exchange price levels per back/lay side. Use smaller values for lower latency and payload size. - name: include_source in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Source description: When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept. - name: include_unavailable in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Unavailable description: When true, include unavailable or suspended rows where the endpoint supports them. - name: refresh in: query required: false schema: type: boolean description: Request bounded, demand-driven Betfair collection before returning. default: false title: Refresh description: Request bounded, demand-driven Betfair collection before returning. - name: refresh_timeout_seconds in: query required: false schema: type: number maximum: 10.0 minimum: 0.5 default: 5.0 title: Refresh Timeout Seconds responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ExchangeOrderBookSnapshotResponse' examples: default: summary: 'Event exchange order book: Snapshot' value: event_id: '3704597661' as_of_ts_ms: 1760000000000 ttl_seconds: 30 items: - id: betfair::1.23456789::moneyline event_id: '3704597661' exchange: betfair exchange_market_id: '1.23456789' market_key: moneyline market_name: Match Odds bet_type: moneyline period: full time home_team: Home away_team: Away status: open in_play: false total_matched: 24567.12 total_available: 204.8 total_available_source: displayed_ladders currency: AUD observed_at: '2026-08-22T08:00:00Z' selections: - selection_key: moneyline:home exchange_selection_id: '12345' selection_name: Home last_traded_price: 2.08 traded_volume_by_price: - price: 2.08 size: 300.0 available_to_back: - price: 2.08 size: 120.5 available_to_lay: - price: 2.1 size: 84.3 best_back_price: 2.08 best_back_size: 120.5 best_lay_price: 2.1 best_lay_size: 84.3 next_cursor: null resume: 1760000000000-0 '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Credentials are valid but do not allow this resource. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' description: Returns sports betting exchange order books for one event. Supported exchanges are betdaq, betfair, smarkets, and matchbook. Each selection includes back and lay price levels with available size, plus top-of-book summary fields such as best_back_price and best_lay_price. Market identity, team identity, source and observation timestamps, currency, total matched, total available, traded volume, and traded volume by price are retained when supplied by the exchange source. Smarkets volume is reported in GBP and includes its native double_stake_volume when available. Use `depth` to limit executable ladder levels and cache only briefly because exchange liquidity can move quickly. x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. /events/{event_id}/exchange/orderbook/stream: get: tags: - Sports exchange summary: 'Event exchange order book: Stream (SSE)' operationId: exchange_orderbook_stream_events__event_id__exchange_orderbook_stream_get security: - ApiKeyAuth: [] parameters: - name: event_id in: path required: true schema: type: string title: Event Id description: Canonical event or race identifier from an event list response. - name: exchanges in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated exchange filter. title: Exchanges description: Comma-separated exchange allow-list. Supported values are `betdaq`, `betfair`, `smarkets`, and `matchbook`. - name: market_keys in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated market_key allow-list. title: Market Keys description: Comma-separated market key allow-list for odds filters. - name: selection_keys in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated selection_key allow-list. title: Selection Keys description: Comma-separated stable selection identifiers from an exchange order book or odds snapshot. - name: depth in: query required: false schema: type: integer maximum: 20 minimum: 1 default: 3 title: Depth description: Number of exchange price levels per back/lay side. Use smaller values for lower latency and payload size. - name: include_source in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Source description: When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept. - name: include_unavailable in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Unavailable description: When true, include unavailable or suspended rows where the endpoint supports them. - name: since in: query required: false schema: anyOf: - type: string - type: 'null' description: Redis stream ID title: Since description: Resume token from a previous snapshot or stream message. Pass it after reconnecting. - name: catchup in: query required: false schema: type: boolean default: true title: Catchup description: When true, return available missed stream events after `since` before waiting for new events. - name: heartbeat_sec in: query required: false schema: type: integer maximum: 120 minimum: 5 default: 15 title: Heartbeat Sec description: Heartbeat interval in seconds for stream liveness. Valid range is 5-120. - name: max_batch in: query required: false schema: type: integer maximum: 2000 minimum: 1 default: 500 title: Max Batch description: Maximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients. responses: '200': description: Server-Sent Events stream. Each message has an event name and JSON data payload. content: text/event-stream: schema: type: object description: Decoded stream message. SSE transports this as an event line plus JSON data; WebSockets send it as JSON. required: - event - data properties: event: type: string enum: - delta - heartbeat - resync data: oneOf: - $ref: '#/components/schemas/ExchangeOrderBookStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Decoded delta message value: event: delta data: event_id: '3704597661' resume: 1760000000000-0 changes: [] heartbeat: summary: Decoded heartbeat message value: event: heartbeat data: {} resync: summary: Decoded resync message value: event: resync data: event_id: '3704597661' resume: null reason: trimmed '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Credentials are valid but do not allow this resource. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' description: Server-Sent Events feed for sports exchange order book changes on one event. Subscribe after reading the snapshot and pass the snapshot `resume` value as `since` to receive catch-up changes when available. Handle `delta`, `heartbeat`, and `resync`; reload the snapshot after `resync`. x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. x-stream-metering: stream_units: 1 stream_hours_formula: stream_units * open_seconds / 3600 logical_bytes_metered: true quota_policy: hard_cap applies_to: authenticated API-key connections /events/{event_id}/exchange/orderbook/ws: get: operationId: websocket_events_event_id_exchange_orderbook_ws parameters: - name: event_id in: path required: true schema: type: string title: Event Id description: Canonical event or race identifier from an event list response. - name: exchanges in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated exchange filter. title: Exchanges description: Comma-separated exchange allow-list. Supported values are `betdaq`, `betfair`, `smarkets`, and `matchbook`. - name: market_keys in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated market_key allow-list. title: Market Keys description: Comma-separated market key allow-list for odds filters. - name: selection_keys in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated selection_key allow-list. title: Selection Keys description: Comma-separated stable selection identifiers from an exchange order book or odds snapshot. - name: depth in: query required: false schema: type: integer maximum: 20 minimum: 1 default: 3 title: Depth description: Number of exchange price levels per back/lay side. Use smaller values for lower latency and payload size. - name: include_source in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Source description: When true, include per-line/per-bookmaker provenance and capture metadata. Top-level freshness fields are always kept. - name: include_unavailable in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Unavailable description: When true, include unavailable or suspended rows where the endpoint supports them. - name: since in: query required: false schema: anyOf: - type: string - type: 'null' description: Redis stream ID title: Since description: Resume token from a previous snapshot or stream message. Pass it after reconnecting. - name: catchup in: query required: false schema: type: boolean default: true title: Catchup description: When true, return available missed stream events after `since` before waiting for new events. - name: heartbeat_sec in: query required: false schema: type: integer maximum: 120 minimum: 5 default: 15 title: Heartbeat Sec description: Heartbeat interval in seconds for stream liveness. Valid range is 5-120. - name: max_batch in: query required: false schema: type: integer maximum: 2000 minimum: 1 default: 500 title: Max Batch description: Maximum stream messages to read per batch. Default is 500; use smaller batches for low-latency clients. responses: '101': description: WebSocket connection established. Messages are JSON objects. content: application/json: schema: type: object description: Decoded stream message. SSE transports this as an event line plus JSON data; WebSockets send it as JSON. required: - event - data properties: event: type: string enum: - delta - heartbeat - resync data: oneOf: - $ref: '#/components/schemas/ExchangeOrderBookStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Delta message value: event: delta data: event_id: '3704597661' resume: 1760000000000-0 changes: [] heartbeat: summary: Heartbeat message value: event: heartbeat data: {} resync: summary: Resync message value: event: resync data: event_id: '3704597661' resume: null reason: trimmed '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Credentials are valid but do not allow this resource. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '200': description: OpenAPI tooling compatibility response schema. Runtime WebSocket connections upgrade with 101. content: application/json: schema: type: object description: Decoded stream message. SSE transports this as an event line plus JSON data; WebSockets send it as JSON. required: - event - data properties: event: type: string enum: - delta - heartbeat - resync data: oneOf: - $ref: '#/components/schemas/ExchangeOrderBookStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Delta message value: event: delta data: event_id: '3704597661' resume: 1760000000000-0 changes: [] heartbeat: summary: Heartbeat message value: event: heartbeat data: {} resync: summary: Resync message value: event: resync data: event_id: '3704597661' resume: null reason: trimmed x-websocket: true x-stream-equivalent: /events/{event_id}/exchange/orderbook/stream tags: - Sports exchange summary: 'Event exchange order book: Stream (WebSocket)' description: WebSocket feed for sports exchange order book changes on one event. Messages mirror the exchange order book SSE stream payloads. security: - ApiKeyAuth: [] x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. x-stream-metering: stream_units: 1 stream_hours_formula: stream_units * open_seconds / 3600 logical_bytes_metered: true quota_policy: hard_cap applies_to: authenticated API-key connections /events/{event_id}/exchange/markets: get: tags: - Sports exchange summary: 'In-play exchange: Discover markets' description: Lists available Betfair markets across sports, including soccer and cricket. Supply a WagerWise event ID or id_type=betfair with a native Betfair event ID. Filter using market_types; MATCH_ODDS is sorted first. Use the returned opaque market_id values with the multiplexed WebSocket; do not send Betfair market names or IDs. operationId: exchange_markets_events__event_id__exchange_markets_get security: - ApiKeyAuth: [] parameters: - name: event_id in: path required: true schema: type: string title: Event Id description: Canonical event or race identifier from an event list response. - name: exchange in: query required: false schema: type: string pattern: ^betfair$ default: betfair title: Exchange - name: id_type in: query required: false schema: type: string pattern: ^(wagerwise|betfair)$ description: Namespace of event_id; Betfair IDs are numeric event IDs, not market IDs. default: wagerwise title: Id Type description: Namespace of event_id; Betfair IDs are numeric event IDs, not market IDs. - name: market_types in: query required: false schema: anyOf: - type: string maxLength: 1000 - type: 'null' description: Optional comma-separated Betfair market types, e.g. MATCH_ODDS,OVER_UNDER_25. title: Market Types description: Optional comma-separated Betfair market types, e.g. MATCH_ODDS,OVER_UNDER_25. - name: refresh in: query required: false schema: type: boolean default: false title: Refresh responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ExchangeMarketsResponse' '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Credentials are valid but do not allow this resource. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. /exchange/betfair/events/{betfair_event_id}/markets: get: tags: - Sports exchange summary: 'In-play exchange: Discover by Betfair event ID' description: Public Odds API operation. Authenticate with `X-API-Key`. Check timestamps before displaying prices and handle empty, stale, or suspended markets. operationId: betfair_event_markets_exchange_betfair_events__betfair_event_id__markets_get security: - ApiKeyAuth: [] parameters: - name: betfair_event_id in: path required: true schema: type: string title: Betfair Event Id - name: market_types in: query required: false schema: anyOf: - type: string maxLength: 1000 - type: 'null' title: Market Types - name: refresh in: query required: false schema: type: boolean default: false title: Refresh responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/ExchangeMarketsResponse' '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Credentials are valid but do not allow this resource. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. /exchange/orderbooks/ws: get: operationId: websocket_exchange_orderbooks_ws parameters: [] responses: '101': description: WebSocket connection established. Send subscribe/unsubscribe JSON commands. content: application/json: schema: $ref: '#/components/schemas/InPlayExchangeWebSocketMessage' '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '403': description: Credentials are valid but do not allow this resource. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' x-websocket: true x-subscription-command: op: subscribe market_ids: - bfm_... depth: 3 tags: - Sports exchange summary: 'In-play exchange: Multiplexed WebSocket' description: 'One support-approved WebSocket can subscribe and unsubscribe dynamically from up to five opaque market IDs. Send {op: subscribe, market_ids: [...], depth: 3}; the server acknowledges immediately, sends a cached image when available, then deltas and heartbeats. Every order book includes in_play. The handshake uses one API credit; connection time and logical bytes use the plan''s stream quotas.' security: - ApiKeyAuth: [] x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. x-stream-metering: stream_units: 1 stream_hours_formula: stream_units * open_seconds / 3600 logical_bytes_metered: true quota_policy: hard_cap applies_to: authenticated API-key connections /exchange/tennis/scores/ws: get: operationId: websocket_exchange_tennis_scores x-websocket: true x-subscription-command: op: subscribe event_ids: - betfair:123 responses: '101': description: WebSocket established; subscribe to discovered tennis event IDs. content: application/json: schema: oneOf: - $ref: '#/components/schemas/TennisScoreMessage' - type: object required: - type properties: type: type: string enum: - subscribed - unsubscribed - heartbeat - pong - error - warming_timeout - score_unavailable - stale - upstream_unavailable event_id: type: string event_ids: type: array items: type: string active_event_ids: type: array items: type: string warming: type: array items: type: string code: type: string retryable: type: boolean ts: type: integer '403': description: Tennis scores entitlement required. '400': description: Invalid request parameters or body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '401': description: Missing or invalid credentials. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '404': description: Resource was not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' '429': description: Rate limit exceeded. headers: Retry-After: description: Seconds to wait before retrying when supplied by the limiter. schema: type: integer minimum: 1 X-RateLimit-Limit: description: Request bucket capacity for the active limiter bucket when supplied. schema: type: integer X-RateLimit-Remaining: description: Approximate remaining requests in the active limiter bucket when supplied. schema: type: integer minimum: 0 X-RateLimit-Bucket: description: Limiter bucket name that produced the response when supplied. schema: type: string content: application/json: schema: $ref: '#/components/schemas/RateLimitResponse' '500': description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' tags: - Sports exchange summary: 'Live tennis scores: Multiplexed WebSocket' description: 'Requires X-API-Key with tennis_scores_enabled=true. Discover tennis event IDs first using exchange market discovery. Send subscribe/unsubscribe commands with event_ids (maximum 10), or ping. Opening alone starts no collection. The first score is an image; delta contains a complete replacement score. Five-second heartbeats indicate socket liveness only. Scores are ephemeral: no history or replay. received_at is our receipt time; source_timestamp is null. Scores and prices are independent observations and may be delayed or inaccurate. Nullable fields are not inferred. Handle unknown_event_ids, event_limit_exceeded, warming_timeout, score_unavailable, stale and upstream_unavailable. Quota exhaustion closes with 4429; authentication and availability use the standard stream close codes. Reconnect and subscribe again after disconnect; sequence numbers are scoped to the active router process.' security: - ApiKeyAuth: [] x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. x-stream-metering: stream_units: 1 stream_hours_formula: stream_units * open_seconds / 3600 logical_bytes_metered: true quota_policy: hard_cap applies_to: authenticated API-key connections components: schemas: TennisScoreMessage: properties: type: enum: - image - delta title: Type type: string event_id: title: Event Id type: string stream_id: description: Process-session ID that scopes sequence values. pattern: ^[0-9a-f]{32}$ title: Stream Id type: string sequence: description: Monotonic semantic revision within one stream_id. minimum: 1 title: Sequence type: integer update_kind: description: How this complete replacement relates to the last state delivered on this connection. enum: - initial - advance - correction - reaffirmation - reset title: Update Kind type: string score: $ref: '#/components/schemas/TennisScore' freshness: $ref: '#/components/schemas/TennisScoreFreshness' required: - type - event_id - stream_id - sequence - update_kind - score - freshness title: TennisScoreMessage type: object ExchangeMarketSubscription: properties: websocket_url: type: string title: Websocket Url command: additionalProperties: true type: object title: Command max_markets_per_connection: type: integer title: Max Markets Per Connection type: object required: - websocket_url - command - max_markets_per_connection title: ExchangeMarketSubscription ExchangeOrderBookStreamEvent: properties: event_id: title: Event Id type: string resume: title: Resume type: string changes: items: $ref: '#/components/schemas/ExchangeOrderBookStreamDelta' title: Changes type: array required: - event_id - resume - changes title: ExchangeOrderBookStreamEvent type: object ExchangeSelectionBook: properties: selection_key: type: string title: Selection Key exchange_selection_id: anyOf: - type: string - type: 'null' title: Exchange Selection Id selection_name: anyOf: - type: string - type: 'null' title: Selection Name handicap: anyOf: - type: number - type: 'null' title: Handicap side: anyOf: - type: string - type: 'null' title: Side line: anyOf: - type: string - type: 'null' title: Line player_name: anyOf: - type: string - type: 'null' title: Player Name status: anyOf: - type: string - type: 'null' title: Status last_traded_price: anyOf: - type: number - type: 'null' title: Last Traded Price traded_volume: anyOf: - type: number - type: 'null' title: Traded Volume traded_volume_by_price: items: $ref: '#/components/schemas/ExchangePriceLevel' type: array title: Traded Volume By Price available_to_back: items: $ref: '#/components/schemas/ExchangePriceLevel' type: array title: Available To Back available_to_lay: items: $ref: '#/components/schemas/ExchangePriceLevel' type: array title: Available To Lay best_back_price: anyOf: - type: number - type: 'null' title: Best Back Price best_back_size: anyOf: - type: number - type: 'null' title: Best Back Size best_lay_price: anyOf: - type: number - type: 'null' title: Best Lay Price best_lay_size: anyOf: - type: number - type: 'null' title: Best Lay Size type: object required: - selection_key title: ExchangeSelectionBook TennisScoreParticipant: properties: name: title: Name type: string sets: minimum: 0 title: Sets type: integer games: minimum: 0 title: Games type: integer points: title: Points type: string is_serving: anyOf: - type: boolean - type: 'null' title: Is Serving game_sequence: items: type: integer title: Game Sequence type: array required: - name - sets - games - points - is_serving - game_sequence title: TennisScoreParticipant type: object ExchangePriceLevel: properties: price: type: number title: Price size: type: number title: Size type: object required: - price - size title: ExchangePriceLevel ExchangeMarket: properties: market_id: type: string title: Market Id description: Stable opaque ID to pass to the multiplexed orderbook WebSocket. event_id: type: string title: Event Id description: Streaming event identity; may be betfair: for events outside the schedule. wagerwise_event_id: anyOf: - type: string - type: 'null' title: Wagerwise Event Id betfair_event_id: anyOf: - type: string - type: 'null' title: Betfair Event Id exchange: type: string title: Exchange sport: type: string title: Sport market_key: type: string title: Market Key bet_type: type: string title: Bet Type metric: type: string title: Metric period: type: string title: Period source_market_type: type: string title: Source Market Type betting_type: anyOf: - type: string - type: 'null' title: Betting Type market_name: type: string title: Market Name event_name: type: string title: Event Name competition_name: type: string title: Competition Name start_time: anyOf: - type: string - type: 'null' title: Start Time in_play_supported: type: boolean title: In Play Supported ladder_depth_max: type: integer title: Ladder Depth Max number_of_winners: anyOf: - type: integer - type: 'null' title: Number Of Winners total_matched: anyOf: - type: number - type: 'null' title: Total Matched runners: items: $ref: '#/components/schemas/ExchangeMarketRunner' type: array title: Runners type: object required: - market_id - event_id - exchange - sport - market_key - bet_type - metric - period - source_market_type - market_name - event_name - competition_name - in_play_supported - ladder_depth_max title: ExchangeMarket TennisScoreSubscription: properties: websocket_url: type: string title: Websocket Url command: additionalProperties: true type: object title: Command max_events_per_connection: type: integer title: Max Events Per Connection type: object required: - websocket_url - command - max_events_per_connection title: TennisScoreSubscription RateLimitResponse: allOf: - $ref: '#/components/schemas/ErrorResponse' description: Rate-limit response. Retry after the window or reduce polling frequency. InPlayExchangeWebSocketMessage: type: object description: Multiplexed in-play exchange WebSocket message. required: - type additionalProperties: true properties: type: type: string enum: - subscribed - unsubscribed - image - delta - heartbeat - pong - error event_id: type: string nullable: true resume: type: string nullable: true market_ids: type: array items: type: string changes: type: array items: type: object additionalProperties: true TennisScoreFreshness: properties: state: enum: - live - stale title: State type: string age_ms: title: Age Ms type: integer poll_interval_ms: title: Poll Interval Ms type: integer required: - state - age_ms - poll_interval_ms title: TennisScoreFreshness type: object ExchangeOrderBookStreamDelta: properties: op: title: Op type: string orderbook: $ref: '#/components/schemas/ExchangeOrderBookItem' required: - op - orderbook title: ExchangeOrderBookStreamDelta type: object StreamResyncEvent: type: object description: Sent when a resume token is no longer available and the client should reload the snapshot. required: - reason properties: event_id: type: string nullable: true selection_key: type: string nullable: true resume: type: string nullable: true reason: type: string example: trimmed snapshot_id: type: string nullable: true StreamHeartbeat: type: object description: Heartbeat payload. All streams use it as a liveness signal; event-odds streams also include the latest event and bookmaker freshness metadata so a no-price-change capture is observable. additionalProperties: false properties: event_id: type: string nullable: true resume: type: string nullable: true as_of_ts_ms: type: integer nullable: true snapshot_capture_ts_ms: type: integer nullable: true bookmaker_as_of_ts_ms: type: object additionalProperties: type: integer oldest_bookmaker_as_of_ts_ms: type: integer nullable: true target_refresh_interval_seconds: type: integer nullable: true ExchangeMarketRunner: properties: selection_id: type: string title: Selection Id name: type: string title: Name handicap: anyOf: - type: number - type: 'null' title: Handicap sort_priority: anyOf: - type: integer - type: 'null' title: Sort Priority type: object required: - selection_id - name title: ExchangeMarketRunner TennisScore: properties: sport: const: tennis title: Sport type: string provider: const: betfair title: Provider type: string home: $ref: '#/components/schemas/TennisScoreParticipant' away: $ref: '#/components/schemas/TennisScoreParticipant' current_set: anyOf: - type: integer - type: 'null' title: Current Set current_game: anyOf: - type: integer - type: 'null' title: Current Game match_status: anyOf: - type: string - type: 'null' title: Match Status tie_break: anyOf: - type: boolean - type: 'null' title: Tie Break received_at: description: Odds API receipt timestamp, not source time. title: Received At type: string source_timestamp: default: null title: Source Timestamp type: 'null' source_timestamp_available: const: false default: false title: Source Timestamp Available type: boolean required: - sport - provider - home - away - current_set - current_game - match_status - tie_break - received_at title: TennisScore type: object ErrorResponse: type: object required: - detail properties: detail: description: Human-readable error detail. FastAPI may return a string or a validation-error object. oneOf: - type: string - type: object - type: array items: type: object code: type: string description: Optional stable error code when provided by the endpoint. request_id: type: string description: Optional request identifier for support/debugging. ExchangeMarketsResponse: properties: score_subscription: anyOf: - $ref: '#/components/schemas/TennisScoreSubscription' - type: 'null' event_id: type: string title: Event Id exchange: type: string title: Exchange count: type: integer title: Count available_market_types: items: type: string type: array title: Available Market Types markets: items: $ref: '#/components/schemas/ExchangeMarket' type: array title: Markets subscription: $ref: '#/components/schemas/ExchangeMarketSubscription' type: object required: - event_id - exchange - count - available_market_types - markets - subscription title: ExchangeMarketsResponse ExchangeOrderBookItem: properties: id: type: string title: Id market_id: anyOf: - type: string - type: 'null' title: Market Id event_id: type: string title: Event Id exchange: type: string title: Exchange exchange_market_id: type: string title: Exchange Market Id source_market_type: anyOf: - type: string - type: 'null' title: Source Market Type sport: anyOf: - type: string - type: 'null' title: Sport betting_type: anyOf: - type: string - type: 'null' title: Betting Type market_key: type: string title: Market Key market_name: anyOf: - type: string - type: 'null' title: Market Name type: anyOf: - type: string - type: 'null' title: Type bet_type: anyOf: - type: string - type: 'null' title: Bet Type period: anyOf: - type: integer - type: string title: Period period_str: anyOf: - type: string - type: 'null' title: Period Str metric: anyOf: - type: string - type: 'null' title: Metric line: anyOf: - type: string - type: 'null' title: Line home_team: anyOf: - type: string - type: 'null' title: Home Team away_team: anyOf: - type: string - type: 'null' title: Away Team home_team_names: items: type: string type: array title: Home Team Names away_team_names: items: type: string type: array title: Away Team Names status: anyOf: - type: string - type: 'null' title: Status in_play: type: boolean title: In Play default: false total_matched: anyOf: - type: number - type: 'null' title: Total Matched description: Exchange-native matched or displayed market volume. total_available: anyOf: - type: number - type: 'null' title: Total Available description: Native or displayed-ladder total currently available market liquidity. total_available_source: anyOf: - type: string - type: 'null' title: Total Available Source description: Whether total_available is exchange-native or summed from displayed ladders. traded_volume: anyOf: - type: number - type: 'null' title: Traded Volume description: Exchange-native traded market volume. double_stake_volume: anyOf: - type: number - type: 'null' title: Double Stake Volume description: Twice the back stake for each execution, when supplied by Smarkets. currency: anyOf: - type: string - type: 'null' title: Currency description: ISO 4217 currency for monetary volume fields, such as GBP for Smarkets. source_ts: anyOf: - type: string - type: 'null' title: Source Ts source_age_seconds: anyOf: - type: number - type: 'null' title: Source Age Seconds freshness: type: string title: Freshness description: 'Price observation state: live, warming, or stale.' default: stale actively_collected: type: boolean title: Actively Collected default: false source_delayed: anyOf: - type: boolean - type: 'null' title: Source Delayed source_conflate_ms: anyOf: - type: integer - type: 'null' title: Source Conflate Ms observed_at: anyOf: - type: string - type: 'null' title: Observed At description: UTC timestamp when WagerWise observed this order book. selections: items: $ref: '#/components/schemas/ExchangeSelectionBook' type: array title: Selections type: object required: - id - event_id - exchange - exchange_market_id - market_key - period title: ExchangeOrderBookItem ExchangeOrderBookSnapshotResponse: properties: event_id: type: string title: Event Id as_of_ts_ms: anyOf: - type: integer - type: 'null' title: As Of Ts Ms ttl_seconds: anyOf: - type: integer - type: 'null' title: Ttl Seconds items: items: $ref: '#/components/schemas/ExchangeOrderBookItem' type: array title: Items next_cursor: anyOf: - type: string - type: 'null' title: Next Cursor resume: type: string title: Resume description: Redis stream ID to resume from; pass as 'since' to /stream. refresh_state: type: string title: Refresh State default: not_requested fresh_count: type: integer title: Fresh Count default: 0 stale_count: type: integer title: Stale Count default: 0 warming_count: type: integer title: Warming Count default: 0 type: object required: - event_id - resume title: ExchangeOrderBookSnapshotResponse securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: Send your API key in this header on every request.