openapi: 3.2.0 info: title: Racing odds 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: Racing odds description: Racing odds snapshots, home racing feeds, Server-Sent Events, and WebSocket updates. paths: /racing/events/{event_id}/odds: get: tags: - Racing odds summary: 'Racing odds: Snapshot' operationId: get_racing_odds_snapshot_racing_events__event_id__odds_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: bookmakers in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated bookmaker filter. title: Bookmakers description: Comma-separated bookmaker allow-list. Use `/bookmakers` to discover supported keys. - name: include_links in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Links description: Include bookmaker_link when available, independently of raw payload. Compact clients omit links by default. false removes links including nested raw links. - name: include_raw_payload in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Raw Payload description: Include bookmaker-specific payload. Compact clients default to false and receive runners[].win_odds; Sportsbet raw WIN prices are payload.horse_data[].odds. - name: include_source in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Source description: Include source metadata such as updated_at_ts (Unix seconds). Compact clients omit it by default. - name: include_unavailable in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Unavailable description: Include bookmaker snapshots without compact WIN or PLACE runner prices. This does not restrict odds to status fetching; early status ok may contain valid prices. responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/RacingOddsSnapshotResponse' examples: default: summary: 'Racing odds: Snapshot' value: event_id: race-1001 as_of_ts_ms: 1760000000000 active_runners: 7 total_runners: 8 scratched_runners: 1 runner_count_source: betfair runner_count_updated_at_ts: 1759999700 runner_count_complete: true items: - bookmaker_name: betfair race_id: race-1001 status: ok total_matched: 24567.12 runners: - runner_number: '1' runner_name: Example Runner win_odds: 3.4 place_odds: 1.65 traded_volume: 4100.0 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 bookmaker odds snapshots for one racing event. Cache the last good snapshot with its timestamp and prefer streams for near-jump realtime displays. Compact WIN prices are at items[].runners[].win_odds. Exchange WIN-market volume is at items[].total_matched and matched runner volume, when supplied, is at items[].runners[].traded_volume. These are cumulative traded amounts, not current executable depth. Sportsbet raw prices, when requested, are at items[].payload.horse_data[].odds. Early odds status ok and near-jump status fetching can both contain valid prices. Top-level active_runners, total_runners, scratched_runners, runner_count_source, runner_count_updated_at_ts, and runner_count_complete describe the best available field state. The API key's betting-opportunity bookmaker selection does not filter racing odds. Request include_source=true for snapshot timestamps and include_links=true for bookmaker_link, independently of include_raw_payload. x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. /racing/events/{event_id}/odds/stream: get: tags: - Racing odds summary: 'Racing odds: Stream (SSE)' operationId: stream_racing_odds_racing_events__event_id__odds_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: include_links in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Links description: Include bookmaker_link when available, independently of raw payload. Compact clients omit links by default. false removes links including nested raw links. - name: include_raw_payload in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Raw Payload description: Include bookmaker-specific payload. Compact clients default to false and receive runners[].win_odds; Sportsbet raw WIN prices are payload.horse_data[].odds. - name: include_source in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Source description: Include source metadata such as updated_at_ts (Unix seconds). Compact clients omit it by default. - name: include_unavailable in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Unavailable description: Include bookmaker snapshots without compact WIN or PLACE runner prices. This does not restrict odds to status fetching; early status ok may contain valid prices. - 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/RacingOddsStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Decoded delta message value: event: delta data: event_id: race-1001 resume: 1760000000000-0 changes: - op: upsert bookmaker_name: sportsbet snapshot: bookmaker_name: betfair race_id: race-1001 status: ok total_matched: 24567.12 runners: - runner_number: '1' runner_name: Example Runner win_odds: 3.4 place_odds: 1.65 traded_volume: 4100.0 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 racing odds changes on one event. Reconnect with `since=` and reload the snapshot after `resync`. Each changes[].snapshot uses the same compact runner fields, link flags and odds status meanings as the racing odds snapshot endpoint for compact clients. Non-compact clients with include_raw_payload=true receive raw bookmaker data directly in snapshot (Sportsbet: snapshot.horse_data[].odds), without a payload wrapper.' 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 /racing/events/{event_id}/odds/ws: get: operationId: websocket_racing_events_event_id_odds_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: include_links in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Links description: Include bookmaker_link when available, independently of raw payload. Compact clients omit links by default. false removes links including nested raw links. - name: include_raw_payload in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Raw Payload description: Include bookmaker-specific payload. Compact clients default to false and receive runners[].win_odds; Sportsbet raw WIN prices are payload.horse_data[].odds. - name: include_source in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Source description: Include source metadata such as updated_at_ts (Unix seconds). Compact clients omit it by default. - name: include_unavailable in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Unavailable description: Include bookmaker snapshots without compact WIN or PLACE runner prices. This does not restrict odds to status fetching; early status ok may contain valid prices. - 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/RacingOddsStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Delta message value: event: delta data: event_id: race-1001 resume: 1760000000000-0 changes: - op: upsert bookmaker_name: sportsbet snapshot: bookmaker_name: betfair race_id: race-1001 status: ok total_matched: 24567.12 runners: - runner_number: '1' runner_name: Example Runner win_odds: 3.4 place_odds: 1.65 traded_volume: 4100.0 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/RacingOddsStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Delta message value: event: delta data: event_id: race-1001 resume: 1760000000000-0 changes: - op: upsert bookmaker_name: sportsbet snapshot: bookmaker_name: betfair race_id: race-1001 status: ok total_matched: 24567.12 runners: - runner_number: '1' runner_name: Example Runner win_odds: 3.4 place_odds: 1.65 traded_volume: 4100.0 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: /racing/events/{event_id}/odds/stream tags: - Racing odds summary: 'Racing odds: Stream (WebSocket)' description: WebSocket feed for racing odds changes on one event. Messages mirror the racing odds SSE feed. 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: 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 RateLimitResponse: allOf: - $ref: '#/components/schemas/ErrorResponse' description: Rate-limit response. Retry after the window or reduce polling frequency. RacingOddsStreamEvent: properties: event_id: title: Event Id type: string resume: title: Resume type: string changes: items: $ref: '#/components/schemas/RacingOddsStreamDelta' title: Changes type: array required: - event_id - resume - changes title: RacingOddsStreamEvent type: object RacingOddsBookmakerSnapshot: properties: bookmaker_name: type: string title: Bookmaker Name race_id: type: string title: Race Id status: anyOf: - type: string - type: 'null' title: Status description: Bookmaker snapshot status, distinct from event status. Early prices use ok; near-jump collection uses fetching. Both may contain valid prices; do not require fetching to read odds. Other statuses may occur; inspect runner prices and timestamps. total_matched: anyOf: - type: number - type: 'null' title: Total Matched description: Cumulative amount matched on the exchange WIN market in the source feed's currency. Betfair racing is currently requested in AUD. updated_at_ts: anyOf: - type: integer - type: 'null' title: Updated At Ts description: Snapshot update time in Unix seconds. On compact clients, request include_source=true to include it. payload: additionalProperties: true type: object title: Payload description: Bookmaker-specific payload, omitted when include_raw_payload=false (the compact default). Sportsbet WIN prices use payload.horse_data[].odds, with horse_name and number; payload.runners[].win_odds is not a universal raw schema. examples: - horse_data: - horse_name: Example Runner number: '1' odds: 3.4 status: ok runners: items: properties: runner_number: anyOf: - type: string - type: integer - type: 'null' title: Runner Number runner_name: anyOf: - type: string - type: 'null' title: Runner Name win_odds: anyOf: - type: number - type: string - type: 'null' title: Win Odds description: Decimal WIN price, when supplied. place_odds: anyOf: - type: number - type: string - type: 'null' title: Place Odds description: Decimal PLACE price, when supplied. jockey_name: anyOf: - type: string - type: 'null' title: Jockey Name trainer_name: anyOf: - type: string - type: 'null' title: Trainer Name selection_id: anyOf: - type: string - type: integer - type: 'null' title: Selection Id runner_status: anyOf: - type: string - type: 'null' title: Runner Status is_scratched: anyOf: - type: boolean - type: 'null' title: Is Scratched removal_date: anyOf: - type: string - type: integer - type: number - type: 'null' title: Removal Date adjustment_factor: anyOf: - type: number - type: string - type: 'null' title: Adjustment Factor traded_volume: anyOf: - type: number - type: 'null' title: Traded Volume description: Cumulative exchange-matched amount for this runner in the source feed's currency. Betfair racing is currently requested in AUD. type: object title: RacingOddsRunner description: Compact runner fields; absent source values are omitted from responses. type: array description: Compact prices at items[].runners[].win_odds. Present for priced runners on compact clients, or when include_raw_payload=false. Runners without WIN or PLACE prices are omitted. bookmaker_link: type: string description: Source race URL, when available and include_links=true. Independent of include_raw_payload; also applies to stream snapshots. type: object required: - bookmaker_name - race_id title: RacingOddsBookmakerSnapshot RacingOddsStreamDelta: properties: op: title: Op type: string bookmaker_name: title: Bookmaker Name type: string snapshot: anyOf: - $ref: '#/components/schemas/RacingOddsBookmakerSnapshot' - additionalProperties: true type: object - type: 'null' default: null description: 'Compact clients receive the documented bookmaker snapshot with runners and optional bookmaker_link. Non-compact clients with include_raw_payload=true receive a bookmaker-specific raw object directly here (Sportsbet: snapshot.horse_data[].odds and snapshot.raceID), without a payload wrapper. Absent for removals.' title: Snapshot required: - op - bookmaker_name title: RacingOddsStreamDelta type: object RacingOddsSnapshotResponse: 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 active_runners: anyOf: - type: integer - type: 'null' title: Active Runners total_runners: anyOf: - type: integer - type: 'null' title: Total Runners scratched_runners: anyOf: - type: integer - type: 'null' title: Scratched Runners runner_count_source: anyOf: - type: string - type: 'null' title: Runner Count Source runner_count_updated_at_ts: anyOf: - type: integer - type: 'null' title: Runner Count Updated At Ts runner_count_complete: anyOf: - type: boolean - type: 'null' title: Runner Count Complete items: items: $ref: '#/components/schemas/RacingOddsBookmakerSnapshot' type: array title: Items resume: type: string title: Resume description: Redis stream ID to resume from; pass as 'since' to /stream. type: object required: - event_id - resume title: RacingOddsSnapshotResponse 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. 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 securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: Send your API key in this header on every request.