API reference
Odds Aggregation API
v1.0.0 · OpenAPI 3.0 · 1311 operations
Overview, auth, rate limits, response envelope
A canonical odds-aggregation API providing real-time and historical sports
betting odds across books, markets, and sports.
## Authentication
All endpoints require a Bearer token in the `Authorization` header:
```
Authorization: Bearer your_api_key_here
```
API keys are issued per plan (free / starter / pro / enterprise).
## Odds formats
Pass `?odds_format=decimal` (default), `?odds_format=american`, or
`?odds_format=fractional` on any endpoint that returns odds.
## Rate limits
Rate limits are enforced per API key per minute:
| Plan | Req / min |
|------------|-----------|
| free | 30 |
| starter | 120 |
| pro | 600 |
| enterprise | 6000 |
Exceeded limits return `429` with a `Retry-After` header.
## Response envelope
All successful responses are wrapped in:
```json
{ "data": ..., "meta": { "updated_at": "..." } }
```
Paginated responses additionally include `links` and pagination fields in `meta`.
Alerts
GET
/v1/alerts
List your active alerts
Responses
- 200 Paginated list of your alerts
- 401 Unauthorized (shared)
- 429 RateLimited (shared)
POST
/v1/alerts
Create a price or movement alert
Request body
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AlertRequest'
Responses
- 201 Alert created
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
- 429 RateLimited (shared)
DELETE
/v1/alerts/{alert}
Delete an alert
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| alert | path | string | yes | Alert ULID |
Responses
- 200 Alert deleted
- 401 Unauthorized (shared)
- 404 NotFound (shared)
- 429 RateLimited (shared)
Events
GET
/v1/sports/{sport}/events
List events for a sport
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sport | path | string | yes | Sport slug, e.g. `americanfootball_nfl` |
| status | query | string (scheduled|live|completed|cancelled) | Filter by event status | |
| date_from | query | string | ISO 8601 date — include events on or after this date | |
| date_to | query | string | ISO 8601 date — include events on or before this date | |
| per_page | query | integer | Results per page (1–200, default 50) | |
| page | query | integer | Page number (1-indexed) |
Responses
- 200 Paginated list of events
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
- 429 RateLimited (shared)
GET
/v1/events/{event}
Get a single event
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| event | path | string | yes | Event ULID |
Responses
- 200 Single event
- 401 Unauthorized (shared)
- 404 NotFound (shared)
- 429 RateLimited (shared)
Line Movement
GET
/v1/events/{event}/odds/movement
Line movement history for an event
Returns all recorded price changes for the event's markets, grouped by market → book → outcome.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| event | path | string | yes | Event ULID |
| odds_format | query | string (decimal|american|fractional) | Output format for odds values. Default: `decimal`. | |
| market | query | string | Filter to a specific market key | |
| book | query | string | Filter to a specific book key | |
| outcome | query | string | Filter to a specific outcome name | |
| hours | query | integer | How many hours back to look (1–168, default 48) |
Responses
- 200 Line movement grouped by market → book → outcome
- 401 Unauthorized (shared)
- 404 NotFound (shared)
- 429 RateLimited (shared)
GET
/v1/line-movement/{marketId}
Historical price series for one market (WO-E2 §2)
Starter+ only. Returns one series per (outcome, point, book) found in the requested window. Free tier → 403. Window clamped by PlanGate::historyDays — starter 7d, pro 30d, enterprise unbounded. Requested `?from` further back than the tier ceiling is silently clamped; meta.window_from reports the effective window.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| marketId | path | string | yes | |
| selection | query | string | Outcome|point key — e.g. 'Lakers|-3.5' or bare 'Lakers' | |
| book | query | string | Book key (stable identifier) | |
| from | query | string |
Responses
- 200 Series envelope
- 403 Plan required — free cannot access line movement
- 404 Market not found
Odds
GET
/v1/events/{event}/odds
Get odds for an event
Returns all markets for the event with full book comparison per outcome. Use `markets` to filter which markets are returned. Use `books` to filter which sportsbooks are included. Use `odds_format` to control the output format.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| event | path | string | yes | Event ULID |
| odds_format | query | string (decimal|american|fractional) | Output format for odds values. Default: `decimal`. | |
| markets | query | string | Comma-separated market keys, e.g. `h2h,spreads,totals` | |
| books | query | string | Comma-separated book keys to include | |
| include_movement | query | boolean | Include last 5 price history rows per outcome inline |
Responses
- 200 Event odds with market and book comparison
- 401 Unauthorized (shared)
- 404 NotFound (shared)
- 429 RateLimited (shared)
GET
/v1/odds/best
Cross-event best-line shopping
Returns the best available price for each outcome across all books, for a given sport and market.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sport | query | string | yes | Sport slug |
| market | query | string | yes | Market key |
| outcome | query | string | Filter to a specific outcome name | |
| books | query | string | Comma-separated book keys to limit comparison | |
| odds_format | query | string (decimal|american|fractional) | Output format for odds values. Default: `decimal`. | |
| per_page | query | integer | Results per page (1–200, default 50) |
Responses
- 200 Best prices per outcome across all events
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
- 429 RateLimited (shared)
GET
/v1/odds/arb
Arbitrage opportunity feed
Scans scheduled and live events for arbitrage opportunities on moneyline markets. Returns opportunities where the implied probability sum < 1.0.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sport | query | string | Filter to a single sport slug |
Responses
- 200 List of current arb opportunities
- 401 Unauthorized (shared)
- 429 RateLimited (shared)
GET
/v1/events/{event}/props
Player props for one event (pro+)
Gated to the player_props feature (pro/enterprise) — a lower tier gets 403 plan_required, NOT a silent empty array. Returns per-(player, market) rows: each side (over/under or yes/no) carries a `books` map of present books at the active point plus derived best/avg/deviation. Every book cell has american + decimal odds, point, stale flag, age_min, and a stable prop_line_id. `meta.freshness` makes an empty/all-stale response self-describing (covered + coverage_reason).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| event | path | string | yes | Event ULID |
| market | query | string | Narrow to a single prop market_key (e.g. batter_hits). |
Responses
- 200 Props envelope
- 403 Requires the pro plan or higher
- 404 Event not found
GET
/v1/events/{event}/analytics
Average + best odds per selection (WO-E2 §1)
Pro+ only. Returns OddsCalculationService::forMarket output per market: book_count, average (decimal/american/implied), best (decimal/american/book_id). Main lines only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| event | path | string | yes | Event ULID |
| market | query | string (h2h|spreads|totals) |
Responses
- 200 Analytics envelope
- 403 Plan required — free/starter cannot access analytics
- 404 Event not found
GET
/v1/arbs
Arbitrage opportunities — h2h, next 48h (WO-E2 §3)
Starter+ only. Cross-book arbitrage on h2h for upcoming events. Algorithm: best decimal per side; arb when sum(1/best) < 1. Margin = 1 - sum(1/best). Sorted by margin desc.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| league | query | string | League slug (basketball_nba) OR shortcode (NBA, EPL) | |
| min_margin | query | number | Minimum margin in [0, 1]. Default 0.5%. |
Responses
- 200 Arb feed
- 403 Plan required — free cannot access the arb feed
Sports & Leagues
GET
/v1/sports
List all sports
Responses
- 200 List of active sports
- 401 Unauthorized (shared)
- 429 RateLimited (shared)
GET
/v1/sports/{sport}/leagues
List leagues for a sport
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| sport | path | string | yes | Sport slug, e.g. `americanfootball_nfl` |
Responses
- 200 List of leagues for the sport
- 401 Unauthorized (shared)
- 404 NotFound (shared)
- 429 RateLimited (shared)
GET
/v1/books
List supported sportsbooks
Responses
- 200 All active books
- 401 Unauthorized (shared)
- 429 RateLimited (shared)
GET
/v1/ping
Smoke endpoint
Confirms auth + throttle + usage logging end-to-end. Echoes the resolved plan.
Responses
- 200 Pong
GET
/v1/health
Feed freshness + per-league coverage (PUBLIC, no ApiKey)
UNAUTHENTICATED (throttle-only). Lets a consumer/uptime monitor tell a FROZEN pipeline apart from an off-season/quiet slate — the two otherwise look identical (an empty payload). HTTP is always 200; read data.status. Alert on a stale data.as_of, not on an empty props array.
Responses
- 200 Feed freshness report
Tools
POST
/v1/tools/convert
Convert odds between formats
Request body
required: true
content:
application/json:
schema:
type: object
required:
- value
- from
- to
properties:
value:
type: string
description: "Odds value in the input format. E.g. '+150', '2.50', '3/2'"
from:
type: string
enum:
- american
- decimal
- fractional
to:
type: string
enum:
- american
- decimal
- fractional
Responses
- 200 Converted odds in all three formats
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
POST
/v1/tools/hold
Calculate market hold / vig
Request body
required: true
content:
application/json:
schema:
type: object
required:
- legs
properties:
legs:
type: array
minItems: 2
items:
type: object
required:
- outcome
- decimal
properties:
outcome: { type: string }
decimal: { type: number, minimum: 1.001 }
book: { type: string, nullable: true }
Responses
- 200 Hold percentage and no-vig fair values
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
POST
/v1/tools/arb
Calculate arbitrage opportunity
Request body
required: true
content:
application/json:
schema:
type: object
required:
- legs
properties:
legs:
type: array
minItems: 2
items:
type: object
required:
- outcome
- decimal
properties:
outcome: { type: string }
decimal: { type: number, minimum: 1.001 }
book: { type: string, nullable: true }
total_stake:
type: number
minimum: 1
default: 100
Responses
- 200 Arb calculation result
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
POST
/v1/tools/hedge
Calculate hedge bet sizing
Request body
required: true
content:
application/json:
schema:
type: object
required:
- original_stake
- leg1_decimal
- leg2_decimal
properties:
original_stake:
type: number
minimum: 0.01
leg1_decimal:
type: number
minimum: 1.001
leg2_decimal:
type: number
minimum: 1.001
leg1_is_freebet:
type: boolean
default: false
Responses
- 200 Hedge stake and profit scenarios
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
POST
/v1/tools/parlay
Calculate parlay payout
Request body
required: true
content:
application/json:
schema:
type: object
required:
- legs
- stake
properties:
legs:
type: array
minItems: 2
items:
type: object
required:
- decimal
properties:
label: { type: string }
decimal: { type: number, minimum: 1.001 }
stake:
type: number
minimum: 0.01
Responses
- 200 Parlay payout breakdown
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
POST
/v1/tools/line-shopping
Compare lines across books for one outcome
Request body
required: true
content:
application/json:
schema:
type: object
required:
- books
properties:
outcome:
type: string
nullable: true
books:
type: array
minItems: 2
items:
type: object
required:
- book
- decimal
properties:
book: { type: string }
decimal: { type: number, minimum: 1.001 }
point: { type: number, nullable: true }
Responses
- 200 Sorted book comparison for the outcome
- 401 Unauthorized (shared)
- 422 ValidationError (shared)
Schemas
21 reusable component schemas referenced by the operations above.
SuccessEnvelope object
type: object
required:
- data
- meta
properties:
data: {}
meta:
type: object
required:
- updated_at
properties:
updated_at:
type: string
format: date-time
PaginatedEnvelope object
type: object
required:
- data
- meta
- links
properties:
data:
type: array
items: {}
meta:
type: object
required:
- total
- page
- per_page
- last_page
- updated_at
properties:
total:
type: integer
page:
type: integer
per_page:
type: integer
last_page:
type: integer
updated_at:
type: string
format: date-time
links:
type: object
properties:
first:
type: string
format: uri
nullable: true
prev:
type: string
format: uri
nullable: true
next:
type: string
format: uri
nullable: true
last:
type: string
format: uri
nullable: true
Sport object
type: object
required:
- id
- slug
- name
- active
properties:
id:
type: string
description: ULID
slug:
type: string
name:
type: string
active:
type: boolean
League object
type: object
required:
- id
- slug
- name
- active
properties:
id:
type: string
slug:
type: string
name:
type: string
region:
type: string
nullable: true
sport:
$ref: '#/components/schemas/Sport'
active:
type: boolean
Book object
type: object
required:
- key
- name
- active
properties:
key:
type: string
name:
type: string
region:
type: string
nullable: true
active:
type: boolean
Event object
type: object
required:
- id
- home_team
- away_team
- commence_time
- status
properties:
id:
type: string
description: ULID
provider_event_id:
type: string
home_team:
type: string
away_team:
type: string
commence_time:
type: string
format: date-time
status:
type: string
enum:
- scheduled
- live
- completed
- cancelled
league:
$ref: '#/components/schemas/League'
Price object
type: object
required:
- book_key
- book_name
- price
- price_decimal
- is_main
- is_opening
- last_update
properties:
book_key:
type: string
book_name:
type: string
price:
description: 'Odds in requested format'
oneOf:
-
type: string
-
type: number
price_decimal:
type: number
point:
type: number
nullable: true
is_main:
type: boolean
is_opening:
type: boolean
last_update:
type: string
format: date-time
Outcome object
type: object
required:
- name
- best_price
- best_price_decimal
- best_book
- books
properties:
name:
type: string
description:
type: string
nullable: true
best_price:
oneOf:
-
type: string
-
type: number
best_price_decimal:
type: number
best_book:
type: string
opening_price:
oneOf:
-
type: string
-
type: number
books:
type: array
items:
$ref: '#/components/schemas/Price'
Market object
type: object
required:
- key
- label
- outcomes
properties:
key:
type: string
label:
type: string
description:
type: string
nullable: true
outcomes:
type: array
items:
$ref: '#/components/schemas/Outcome'
EventOdds object
type: object
required:
- event_id
- home_team
- away_team
- commence_time
- status
- markets
properties:
event_id:
type: string
home_team:
type: string
away_team:
type: string
commence_time:
type: string
format: date-time
status:
type: string
markets:
type: array
items:
$ref: '#/components/schemas/Market'
ArbOpportunity object
type: object
properties:
event_id:
type: string
home_team:
type: string
away_team:
type: string
market_key:
type: string
profit_percent:
type: number
implied_prob_sum:
type: number
stakes:
type: array
items:
type: number
legs:
type: array
items:
type: object
properties:
outcome:
type: string
book:
type: string
price:
type: number
PriceHistoryPoint object
type: object
properties:
price: {}
price_decimal:
type: number
point:
type: number
nullable: true
recorded_at:
type: string
format: date-time
OutcomeMovement object
type: object
properties:
outcome_name:
type: string
history:
type: array
items:
$ref: '#/components/schemas/PriceHistoryPoint'
BookMovement object
type: object
properties:
book_key:
type: string
outcomes:
type: array
items:
$ref: '#/components/schemas/OutcomeMovement'
MarketMovement object
type: object
properties:
market_key:
type: string
books:
type: array
items:
$ref: '#/components/schemas/BookMovement'
AlertRequest object
type: object
required:
- event_id
- market_key
- outcome_name
- threshold_type
- threshold_value
- webhook_url
properties:
event_id:
type: string
market_key:
type: string
outcome_name:
type: string
threshold_type:
type: string
enum:
- above
- below
- movement
threshold_value:
type: number
webhook_url:
type: string
format: uri
HoldResult object
type: object
properties:
hold_percent:
type: number
implied_prob_sum:
type: string
outcomes:
type: array
items:
type: object
properties:
outcome:
type: string
book:
type: string
nullable: true
decimal:
type: number
implied_prob:
type: number
fair_value_prob:
type: number
fair_decimal:
type: number
nullable: true
fair_american:
type: string
nullable: true
ArbCalcResult object
type: object
properties:
arb_exists:
type: boolean
profit_percent:
type: number
nullable: true
guaranteed_profit:
type: number
nullable: true
total_stake:
type: number
implied_prob_sum:
type: number
message:
type: string
nullable: true
legs:
type: array
items:
type: object
properties:
outcome:
type: string
decimal:
type: number
book:
type: string
nullable: true
stake:
type: number
nullable: true
gross_return:
type: number
nullable: true
HedgeCalcResult object
type: object
properties:
hedge_stake:
type: number
profit_if_leg1_wins:
type: number
profit_if_leg2_wins:
type: number
guaranteed_profit:
type: number
nullable: true
is_free_money_hedge:
type: boolean
inputs:
type: object
properties:
original_stake:
type: number
leg1_decimal:
type: number
leg1_american:
type: string
leg2_decimal:
type: number
leg2_american:
type: string
is_freebet:
type: boolean
ParlayCalcResult object
type: object
properties:
combined_decimal:
type: number
combined_american:
type: string
stake:
type: number
payout:
type: number
profit:
type: number
implied_probability:
type: string
leg_count:
type: integer
legs:
type: array
items:
type: object
properties:
label:
type: string
decimal:
type: number
american:
type: string
implied_prob:
type: number
LineShoppingResult object
type: object
properties:
outcome:
type: string
best_book:
type: string
best_decimal:
type: number
best_american:
type: string
worst_decimal:
type: number
difference_units:
type: number
comparison:
type: array
items:
type: object
properties:
book:
type: string
decimal:
type: number
american:
type: string
point:
type: number
nullable: true
implied_prob:
type: number