openapi: 3.2.0 info: title: Odds Racing events 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 events description: Racing event discovery and live racing event updates. paths: /racing/events: get: tags: - Racing events summary: 'Racing events: Search' operationId: list_racing_events_racing_events_get security: - ApiKeyAuth: [] parameters: - name: race_type in: query required: false schema: anyOf: - type: string - type: 'null' description: 'Comma-separated canonical racing types: `horse-racing`, `greyhound-racing`, or `harness-racing`. Legacy shorthand is normalized for backward compatibility.' title: Race Type description: 'Comma-separated canonical racing type filters: `horse-racing`, `greyhound-racing`, or `harness-racing`. Legacy aliases such as `horse`, `thoroughbred`, `greyhound`, `dog`, `dogs`, and `harness` are normalized for backward compatibility.' - name: race_state in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated race states. title: Race State description: Comma-separated racing state filters. - name: race_country in: query required: false schema: anyOf: - type: string - type: 'null' description: 'Comma-separated racing country codes: `AU`, `NZ`, `GB`, or `IE`. Returned combinations depend on the live racing schedule.' title: Race Country description: 'Comma-separated racing country filters: `AU`, `NZ`, `GB`, or `IE`. Returned combinations depend on the live racing schedule.' - name: status in: query required: false schema: anyOf: - type: string - type: 'null' description: Comma-separated statuses. title: Status description: 'Comma-separated event lifecycle statuses. Omit for early discovery: fetching alone excludes open races that may already have prices. This is not the bookmaker odds snapshot status.' - name: start_from in: query required: false schema: anyOf: - type: integer - type: 'null' description: Unix seconds; default now-900. title: Start From description: Unix seconds lower bound for event start time. Use bounded windows in production polling. - name: start_to in: query required: false schema: anyOf: - type: integer - type: 'null' description: Unix seconds; optional. title: Start To description: Unix seconds upper bound for event start time. Keep windows narrow for hot sync jobs. - name: cursor in: query required: false schema: anyOf: - type: string - type: 'null' description: 'Pagination cursor: ''race_start_time:event_id''.' title: Cursor description: Pagination cursor from the previous `next_cursor`. Keep filters identical between pages. - name: limit in: query required: false schema: type: integer maximum: 1000 minimum: 1 default: 200 title: Limit description: Maximum items to return. Respect the caps returned by `/limits`. - name: include_links in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Links description: When true, include bookmaker/deep-link fields such as match links and racing links. - 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. responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/RacingEventListResponse' examples: default: summary: 'Racing events: Search' value: items: - event_id: race-1001 race_type: horse-racing race_country: AU race_state: QLD status: open race_start_time: 1760000000 race_venue: Doomben active_runners: 7 total_runners: 8 scratched_runners: 1 runner_count_source: betfair runner_count_updated_at_ts: 1759999700 runner_count_complete: true next_cursor: null count: 1 '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: 'Searches upcoming and live horse, greyhound, and harness racing events across Australia (`AU`), New Zealand (`NZ`), Great Britain (`GB`), and Ireland (`IE`). Use canonical race types `horse-racing`, `greyhound-racing`, and `harness-racing`; legacy shorthand is normalized for backward compatibility. Returned combinations depend on the live schedule. Racing polling should use narrow time windows, stable cursors, and shorter intervals near jump. Omit status to discover early races: status=fetching excludes events still marked open, even when early odds exist. Event lifecycle status is distinct from each bookmaker odds snapshot status. Runner counts prefer Betfair''s complete scratch-aware field, fall back to another bookmaker or the schedule, and expose source, timestamp, and completeness metadata. Racing odds are not filtered by the API key''s betting-opportunity bookmaker selection.' x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. /racing/events/stream: get: tags: - Racing events summary: 'Racing events: Stream (SSE)' operationId: stream_racing_events_racing_events_stream_get security: - ApiKeyAuth: [] parameters: - name: include_links in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Links description: When true, include bookmaker/deep-link fields such as match links and racing links. - name: include_raw_payload in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Raw Payload description: When true, include raw stored payload/data objects where the endpoint exposes them. - 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: 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/RacingEventsStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Decoded delta message value: event: delta data: 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 racing event inserts, updates, and removals. Store resume tokens and reload the event list if a stream asks the client to resync. x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. x-stream-metering: stream_units: 2 stream_hours_formula: stream_units * open_seconds / 3600 logical_bytes_metered: true quota_policy: hard_cap applies_to: authenticated API-key connections /racing/events/ws: get: operationId: websocket_racing_events_ws parameters: - name: include_links in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Links description: When true, include bookmaker/deep-link fields such as match links and racing links. - name: include_raw_payload in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Raw Payload description: When true, include raw stored payload/data objects where the endpoint exposes them. - 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: 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/RacingEventsStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Delta message value: event: delta data: 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/RacingEventsStreamEvent' - $ref: '#/components/schemas/StreamHeartbeat' - $ref: '#/components/schemas/StreamResyncEvent' examples: delta: summary: Delta message value: event: delta data: 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: /racing/events/stream tags: - Racing events summary: 'Racing events: Stream (WebSocket)' description: WebSocket feed for racing event inserts, updates, and removals. Messages mirror the racing events SSE feed. security: - ApiKeyAuth: [] x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. x-stream-metering: stream_units: 2 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}: get: tags: - Racing events summary: 'Racing events: Event details' operationId: get_racing_event_racing_events__event_id__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: When true, include bookmaker/deep-link fields such as match links and racing links. - name: include_raw_payload in: query required: false schema: anyOf: - type: boolean - type: 'null' title: Include Raw Payload description: When true, include raw stored payload/data objects where the endpoint exposes them. - 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. responses: '200': description: Successful Response content: application/json: schema: $ref: '#/components/schemas/RacingEventDetail' '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 the current racing event record for a canonical race ID. x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops. 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. RacingEventsStreamEvent: type: object required: - resume - changes properties: resume: type: string changes: type: array items: $ref: '#/components/schemas/RacingEventsStreamChange' RacingEventDetail: properties: event_id: type: string title: Event Id data: additionalProperties: true type: object title: Data type: object required: - event_id - data title: RacingEventDetail RacingEventSummary: properties: event_id: type: string title: Event Id description: Canonical racing event identifier (raceID). race_start_time: anyOf: - type: integer - type: 'null' title: Race Start Time description: Unix seconds. race_type: anyOf: - type: string - type: 'null' title: Race Type race_venue: anyOf: - type: string - type: 'null' title: Race Venue race_number: anyOf: - type: string - type: 'null' title: Race Number race_state: anyOf: - type: string - type: 'null' title: Race State race_country: anyOf: - type: string - type: 'null' title: Race Country race_distance: anyOf: - type: string - type: 'null' title: Race Distance active_runners: anyOf: - type: integer - type: 'null' title: Active Runners description: Best available active-runner count after known scratchings. total_runners: anyOf: - type: integer - type: 'null' title: Total Runners description: Declared field size before known scratchings, when available. scratched_runners: anyOf: - type: integer - type: 'null' title: Scratched Runners description: Known scratched/removed runner count. runner_count_source: anyOf: - type: string - type: 'null' title: Runner Count Source description: Bookmaker or schedule source used for the runner count. runner_count_updated_at_ts: anyOf: - type: integer - type: 'null' title: Runner Count Updated At Ts description: Runner-count source time in Unix seconds. runner_count_complete: anyOf: - type: boolean - type: 'null' title: Runner Count Complete description: True only when the source declares state for the complete field. status: anyOf: - type: string - type: 'null' title: Status description: Event lifecycle status. An open event can already have early bookmaker odds; fetching denotes active near-jump collection. Distinct from bookmaker snapshot statuses such as ok. Omit the status filter when discovering early races. bookmaker_links: additionalProperties: true type: object title: Bookmaker Links scrapping_links: additionalProperties: true type: object title: Scrapping Links last_capture: anyOf: - type: integer - type: 'null' title: Last Capture description: Unix seconds. type: object required: - event_id title: RacingEventSummary 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. RacingEventListResponse: properties: items: items: $ref: '#/components/schemas/RacingEventSummary' type: array title: Items next_cursor: anyOf: - type: string - type: 'null' title: Next Cursor count: type: integer title: Count type: object required: - count title: RacingEventListResponse 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 RacingEventsStreamChange: type: object required: - op - event_id properties: op: type: string enum: - upsert - remove event_id: type: string event: type: object nullable: true additionalProperties: true securitySchemes: ApiKeyAuth: type: apiKey in: header name: X-API-Key description: Send your API key in this header on every request.