Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Odds Betting opportunities 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=<snapshot resume>.
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=<last_resume>.
- 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=<last_resume>.
- 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: Betting opportunities
description: Positive EV, arbitrage, middle, and bonus-bet opportunity feeds.
paths:
/bets/snapshot:
get:
tags:
- Betting opportunities
summary: 'Betting opportunities: Snapshot'
operationId: snapshot_bets_snapshot_get
security:
- ApiKeyAuth: []
parameters:
- name: strategies
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Comma-separated strategies or 'all'
default: all
title: Strategies
description: Comma-separated betting strategies or `all`.
- name: limit
in: query
required: false
schema:
type: integer
maximum: 20000
minimum: 1
default: 2000
title: Limit
description: Maximum items to return. Respect the caps returned by `/limits`.
- name: event_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Event Id
description: Canonical event or race identifier from an event list response.
- name: source
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'Live bet source: active, legacy, or admin-only hybrid'
title: Source
description: 'Live bet source: active, legacy, or admin-only hybrid'
- name: price_fields
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Price Fields
description: '`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.'
- 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: include_debug_ids
in: query
required: false
schema:
anyOf:
- type: boolean
- type: 'null'
title: Include Debug Ids
description: When true, include internal/subgroup/opposing IDs useful for reconciliation.
responses:
'200':
description: Successful Response
content:
application/json:
schema:
$ref: '#/components/schemas/BetsSnapshotResponse'
examples:
default:
summary: 'Betting opportunities: Snapshot'
value:
items:
- id: example-positive-ev
strategy: pos_ev
event_id: '3704597661'
bookmaker_name: Bet365
selection_key: moneyline:home
odds: 2.1
ev: 7.7
resume: '{"pos_ev":"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 current betting opportunities by strategy. Use `strategies`, `limit`, and `event_id` to keep the payload scoped to what your product needs. Poll at a bounded interval, cache the last good response, and show execution-risk language before any user-facing bet action.
x-rate-limit-note: Handle HTTP 429 with backoff and avoid tight polling loops.
/bets/stream:
get:
tags:
- Betting opportunities
summary: 'Betting opportunities: Stream (SSE)'
operationId: stream_bets_stream_get
security:
- ApiKeyAuth: []
parameters:
- name: strategies
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
default: all
title: Strategies
description: Comma-separated betting strategies or `all`.
- name: event_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Event Id
description: Canonical event or race identifier from an event list response.
- name: source
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'Live bet source: active, legacy, or admin-only hybrid'
title: Source
description: 'Live bet source: active, legacy, or admin-only hybrid'
- name: price_fields
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Price Fields
description: '`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.'
- 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: include_debug_ids
in: query
required: false
schema:
anyOf:
- type: boolean
- type: 'null'
title: Include Debug Ids
description: When true, include internal/subgroup/opposing IDs useful for reconciliation.
- name: since
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Resume token from /snapshot (JSON mapping strategy->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.
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/BetsStreamEvent'
- $ref: '#/components/schemas/StreamHeartbeat'
- $ref: '#/components/schemas/StreamResyncEvent'
examples:
delta:
summary: Decoded delta message
value:
event: delta
data:
resume: 1760000000000-0
events: []
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 betting opportunity inserts, updates, and removals. Use this for alerting instead of high-frequency snapshot polling. Source-binding response headers identify the requested logical live-bet source and per-strategy effective sources.
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
/bets/ws:
get:
operationId: websocket_bets_ws
parameters:
- name: strategies
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
default: all
title: Strategies
description: Comma-separated betting strategies or `all`.
- name: event_id
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Event Id
description: Canonical event or race identifier from an event list response.
- name: source
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: 'Live bet source: active, legacy, or admin-only hybrid'
title: Source
description: 'Live bet source: active, legacy, or admin-only hybrid'
- name: price_fields
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
title: Price Fields
description: '`odds`, `odds,novig`, `odds,fair`, or `all`. Compact clients default to `odds`; existing clients default to current full pricing fields.'
- 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: include_debug_ids
in: query
required: false
schema:
anyOf:
- type: boolean
- type: 'null'
title: Include Debug Ids
description: When true, include internal/subgroup/opposing IDs useful for reconciliation.
- name: since
in: query
required: false
schema:
anyOf:
- type: string
- type: 'null'
description: Resume token from /snapshot (JSON mapping strategy->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.
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/BetsStreamEvent'
- $ref: '#/components/schemas/StreamHeartbeat'
- $ref: '#/components/schemas/StreamResyncEvent'
examples:
delta:
summary: Delta message
value:
event: delta
data:
resume: 1760000000000-0
events: []
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/BetsStreamEvent'
- $ref: '#/components/schemas/StreamHeartbeat'
- $ref: '#/components/schemas/StreamResyncEvent'
examples:
delta:
summary: Delta message
value:
event: delta
data:
resume: 1760000000000-0
events: []
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: /bets/stream
tags:
- Betting opportunities
summary: 'Betting opportunities: Stream (WebSocket)'
description: WebSocket feed for betting opportunity inserts, updates, and removals. The accept handshake includes source-binding headers matching the SSE stream.
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:
BetOpportunity:
type: object
description: A normalized betting opportunity. Strategy-specific fields may be present.
additionalProperties: true
properties:
id:
type: string
strategy:
type: string
enum:
- pos_ev
- arbitrage
- middles
- freebets
- pos_ev_special
event_id:
type: string
nullable: true
bookmaker_name:
type: string
nullable: true
selection_key:
type: string
nullable: true
odds:
type: number
nullable: true
fair_odds:
type: number
nullable: true
ev:
type: number
nullable: true
ev_multiplicative_method:
type: number
nullable: true
ev_additive_method:
type: number
nullable: true
ev_power_method:
type: number
nullable: true
ev_shin_method:
type: number
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
RateLimitResponse:
allOf:
- $ref: '#/components/schemas/ErrorResponse'
description: Rate-limit response. Retry after the window or reduce polling frequency.
BetsStreamDelta:
type: object
required:
- strategy
- op
- id
properties:
strategy:
type: string
op:
type: string
enum:
- upsert
- delete
id:
type: string
doc:
$ref: '#/components/schemas/BetOpportunity'
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.
BetsStreamEvent:
type: object
required:
- resume
- events
properties:
resume:
type: string
events:
type: array
items:
$ref: '#/components/schemas/BetsStreamDelta'
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
BetsSnapshotResponse:
type: object
required:
- resume
- items
properties:
resume:
type: string
description: JSON string mapping each requested strategy to its stream resume ID.
items:
type: array
items:
$ref: '#/components/schemas/BetOpportunity'
securitySchemes:
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: Send your API key in this header on every request.