API Documentation
Everything you need to get started with the RapidOddsAPI
Getting Started
1. Create a free account to get your API key
2. Find your key in the dashboard
3. Start making requests
Authentication
Include your API key as a query parameter on every request. Your key starts with oa_ and can be found in your dashboard. Keep it secret — if compromised, regenerate it instantly.
Never expose your API key in client-side code (browser JavaScript, mobile apps, etc). Always make API calls from your backend server to keep your key secure.
Coverage
We cover 10+ leagues, 100+ bookmakers (including clones), and a wide range of market types including head-to-head, spreads, totals, and player props. Available markets vary by sport and bookmaker.
See our full coverage page for a complete list of all available leagues, bookmakers, and market types per sport.
Credits
Each request costs credits based on the number of market types and bookmakers requested:
Every 5 bookmakers counts as 1 group (rounded up).
Examples
| Market Types | Bookmakers | Credits |
|---|---|---|
| 1 | 1 | 1 |
| 1 | 5 | 1 |
| 1 | 6 | 2 |
| 2 | 10 | 4 |
| 3 | 15 | 9 |
Credits are only deducted when data is returned. If your query returns no games, you are not charged.
Client Libraries
Official clients for Python and Node.js, plus an MCP server for AI assistants. They wrap the same API documented below, so anything you can do with a raw request you can do through them.
They save you writing the same plumbing every time: your key on every request, retries when one fails, reconnecting and re-subscribing when the WebSocket drops, game matching across bookmakers and they even include an arbitrage and value bet finder.
You do not need them. The API is plain HTTP and JSON, so if you would rather not add a dependency, or you work in another language, go straight to the endpoint reference. The cURL, Python and JavaScript examples further down use nothing but a HTTP client.
Python SDK
The official Python client. Handles auth and retries for you, and returns typed responses. Requires Python 3.9 or newer.
Quickstart
It also wraps the WebSocket stream (reconnecting and re-subscribing on its own), the results API, and has arbitrage and value bet finders built in. See the README for the full reference.
Node.js SDK
The official Node client. Written in TypeScript and ships its own type definitions, so it works the same from plain JavaScript. Requires Node 18 or newer.
Quickstart
CommonJS works too with const { RapidOddsAPI } = require("rapidoddsapi"). The WebSocket stream, results API, and the arbitrage and value bet finders are all included. See the README for the full reference.
MCP Server
Connects RapidOddsAPI to an AI assistant over the Model Context Protocol, so it can pull live odds, scores and value bets on its own. Built on the Python SDK.
Claude Desktop
Add this to your claude_desktop_config.json, then restart Claude Desktop:
Tools
| Tool | Credits |
|---|---|
| list_sports | 0 |
| get_odds | market_types × ⌈bookmakers / 5⌉ |
| get_results | 1 |
| find_arbitrage | ⌈bookmakers / 5⌉ |
| find_value_bets | ⌈bookmakers / 5⌉ |
Every response ends with what the call cost and how many credits are left, so the assistant can see its own spend.
Rather do it yourself? No library is required. Everything from here down is the raw API, and it works from any language with a HTTP client.
Endpoint
One endpoint for everything. Choose your sport, pick the bookmakers and market types you want.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sport_id | path | Yes | Sport identifier — see coverage |
| api_key | query | Yes | Your API key |
| market_type | query (multi) | Yes | Market type. Repeat for multiple — see coverage |
| bookmaker | query (multi) | Yes | Bookmaker name. Repeat for multiple — see coverage |
Example
Response
| Field | Type | Description |
|---|---|---|
| sport | string | Sport display name |
| games[] | array | List of games with odds |
| game | object | Game details |
| commence_time | string | Game start time (ISO 8601) |
| home_team | string | Home team name |
| away_team | string | Away team name |
| game_url | string | Deep link to this event on the bookmaker's site |
| bookmakers[] | array | Requested bookmakers with data for this game |
| name | string | Bookmaker name |
| last_update | string | Last time odds were updated (ISO 8601) |
| markets[] | array | Markets for this bookmaker |
| key | string | Market type identifier |
| outcomes[] | array | Odds outcomes for this market |
| name | string | Outcome name (team or Over/Under) |
| price | number | Decimal odds |
| point | number | Line value — spreads, totals, and player props |
| player_name | string | Player name — player props only |
| team_name | string | Team name — team markets only |
Rate Limiting
All plans are limited to 30 requests per second per API key. Exceeding this limit returns a 429 error.
Error Codes
| Code | Meaning |
|---|---|
| 401 | Invalid API key |
| 403 | Subscription is not active |
| 404 | Sport not found |
| 422 | Missing required parameter (api_key, market_type, or bookmaker) |
| 429 | Insufficient credits or rate limit exceeded |
| 500 | Internal server error |
Code Examples
Odds WebSocket
Real time odds pushed straight to you
Overview
The WebSocket feed pushes fresh odds data directly to you the moment each scraping cycle completes — no polling required. You connect once, subscribe to the sports and bookmakers you want, and receive updates automatically.
WebSocket access is included on the Pro and Elite plans. See pricing for details.
The data pushed over WebSocket is identical in structure to the REST endpoint response — the same sports, bookmakers, market types, and credit formula apply.
Connecting
Connect using your API key as a query parameter:
Once connected, the server sends a confirmation message:
Messages
Sending — Subscribe
After connecting, send a subscribe message to start receiving pushes for a sport. You can subscribe to multiple sports by sending one message per sport.
Sending — Unsubscribe
Receiving — Unsubscribed
Receiving — Subscribed
Sent immediately after a successful subscribe. Includes the credit cost per push for your subscription.
Receiving — Odds Update
Sent each time a scraping cycle completes for your subscribed sport. The data field is identical in structure to the REST endpoint response.
Receiving — Error
Credits
Credits are charged per push, not per connection. The same formula as the REST endpoint applies:
The credit cost for your subscription is shown in the credits_per_push field of the subscribed confirmation. Credits are only deducted when a push contains data — if a scrape cycle returns no games, you are not charged.
If you have insufficient credits when a push is triggered, you will receive an error event and the push will be skipped.
Error Codes
WebSocket errors fall into two categories — connection rejections (before the connection is established) and in-session error events (sent as messages after connecting).
| Type | Code / Event | Meaning |
|---|---|---|
| Connection | 4001 | Close code sent before the connection opens: invalid or missing API key, inactive subscription, or plan does not include WebSocket access |
| In session | error | Insufficient credits, invalid sport, missing bookmakers or market types, or unknown action |
Code Examples
Results API
Live scores and player stats in real time
Overview
The Results API returns live scores, period breakdowns, and full player stat lines for every game we cover. Use it to track games in play, or to settle bets once a game finishes.
It runs on its own endpoint but uses the same API key as the odds API, so there is nothing extra to set up.
See our full coverage page for the sports and player stats available. Requesting a sport we do not cover returns a 404 listing the sports that are.
Endpoint
One endpoint. Filter by status to get only the games you care about, and use include to control how much data comes back.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| sport | path | Yes | Sport identifier, for example AFL or MLB. Not case sensitive |
| api_key | query | Yes | Your API key, the same one used for odds |
| status | query | No | all (default), live, concluded, or upcoming |
| include | query | No | Comma separated: scores, players. Defaults to both |
| game_id | query | No | Return a single game. |
| round | query | No | Filter to one round number. Only for sports played in rounds. Sports without them return nothing for any value, and carry a null round_number |
| days | query | No | Only return concluded games from the last N days. Live and upcoming games are always returned. Omit for everything we hold |
We keep concluded games for 7 days so there is a window to settle against, then they are removed. Use days to trim that down when you only care about recent games, for example days=1 on a busy slate to get today's games without a week of history behind it. It is an upper bound rather than a guarantee, so asking for more days than we hold is not an error, you just get everything there is.
Game status values
| Value | Meaning | Includes score and players? |
|---|---|---|
| SCHEDULED | Fixtured, not yet started | No |
| LIVE | In progress | Yes, partial and updating |
| CONCLUDED | Finished | Yes, final |
These three values are the same across every sport, whatever the underlying competition calls them. The upcoming filter maps to SCHEDULED. Games that have not started carry only the game block, with no score or players.
Example
The same response for another sport
Same structure, same field names. Only the scoring fields, the stat fields and the name of the mid-game total change. Here a LIVE MLB game, where a period is an inning.
Note full_time is null because the game is still live, while first_5_innings is populated because those innings have finished. Innings are trimmed here for length.
Response
| Field | Type | Description |
|---|---|---|
| sport | string | Sport display name |
| games[] | array | List of games |
| game | object | Always present |
| game_id | integer | Stable identifier, use with the game_id parameter |
| commence_time | string | Scheduled start (UTC) |
| last_update | string | When we last refreshed this game (UTC) |
| home_team / away_team | string | Team names, standardised to match the odds API |
| status | string | See the status table above |
| round_number | integer or null | Round this game belongs to. Null for sports not played in rounds, such as MLB |
| score | object | Present once a game starts, if include has scores |
| home / away | object | Team name plus that sport's scoring fields. AFL goals, behinds, points. MLB runs, hits, errors |
| home_by_period[] / away_by_period[] | array | That team's score in each period. Per period, not cumulative |
| period_state[] | array | Per period completed flag, plus whatever live detail that sport has: a clock field (see the clock table), or half on the MLB inning in progress |
| totals | object | Both teams combined, see Settling Bets below |
| full_time | object or null | Final match total. Null until the game is CONCLUDED |
| half_time / first_5_innings | object or null | The sport's mid-game total, null until every period it covers has finished. half_time covers the first half, which is however many periods that sport's first half contains. That is period 2 in a sport played in quarters, or period 1 in a sport played in halves. MLB carries first_5_innings instead, which waits for inning 5 |
| by_period[] | array | Combined total for each period played so far |
| scoring_events[] | array | Every score in the match in the order it happened, where that sport has one worth recording. Each entry carries order, type, the scoring player, their team, the period and the clock. Absent for sports we do not track this for |
| players[] | array | Every player who has taken the field, if include has players |
| name / team | string | Player name and their team, both standardised |
| {stat_name} | integer or boolean | One field per player prop market we cover for that sport, named to match the market key, for example player_disposals or batter_total_bases. Counting stats are integers. A market that asks whether a player did something rather than how often, such as scoring first, is a boolean. See coverage |
Periods
The response shape is identical for every sport. A period is whatever that sport divides a game into, whether a quarter, a half or an inning, and by_period always holds the raw per-period values you can build any period market from. Only the stat field names change between sports. Because a period means different things, treat the period numbers as opaque: period 2 is half time in one sport and quarter time in another.
The live clock
Inside period_state[], each sport carries the clock its own competition keeps. They are not interchangeable, so the field name states both things you need to know: which window it measures and which way it counts, as {scope}_seconds_{direction}.
| Field | Measures | Counts | Resets each period? |
|---|---|---|---|
| period_seconds_elapsed | Time played in the current period | Up, from 0 | Yes |
| period_seconds_remaining | Time left in the current period | Down, to 0 | Yes |
| match_seconds_elapsed | Time played since the start of the match | Up, from 0 | No |
| display_clock | The clock as that sport writes it, for example "43'" or "8:57" | String | n/a |
| half | Where an MLB inning is up to: Top, Bottom, Middle or End | String | n/a |
A sport carries whichever of these its competition actually publishes, so read the field that is present rather than assuming one. Baseball has no clock at all and carries half instead. Only the period in progress carries a clock, with one exception: AFL keeps period_seconds_elapsed on finished quarters too, because its feed records how long each one ran.
The clock is for display. Nothing settles from it, and it is not a countdown to the end of a period unless the field says remaining. AFL is the clearest case: its clock includes time-on, so a nominal 20 minute quarter runs anywhere from about 1760 to 2020 seconds and there is no fixed length to count down from. Use completed to know a period is over.
Player fields
A player who fills two roles appears once with both sets of fields. An MLB two-way player carries the batter_ and pitcher_ fields together, and fields for a role a player did not fill are simply absent rather than zero.
Markets about who scored first read the player field, not scoring_events. A market like player_1st_tryscorer is already resolved to a boolean on that player, so you never have to walk the list yourself. scoring_events is there to show the sequence live, and to check a settlement against if one is ever queried.
Timestamps
Timestamps are UTC and use the same format as the odds API. Start times for the same game can differ slightly between the two, so match games on team names plus a time window rather than an exact timestamp. Our guide on matching games across bookmakers shows the approach.
Settling Bets
If you are tracking bets taken from the odds API and want to settle them automatically, this section covers what you need. The two APIs share standardised team and player names, so a bet recorded against a market key and a selection can be resolved straight from the results data once the game is done.
Only settle on CONCLUDED games
A named total is null until the period it describes has finished. full_time stays null until the game is CONCLUDED, and a mid-game total stays null until its own periods are done. That is deliberate, so a live scoreline can never be mistaken for a final one. For a live running total, sum by_period yourself.
Do not hardcode how many periods a half is. half_time waits for period 2 in a sport played in quarters, but only period 1 in a sport played in halves, so a rule like "settle once period 2 is done" silently never fires for the second group. Read period_state[].completed and let the null tell you: if half_time is populated, that half is over and it is safe to settle.
Market to field map
A few examples of how odds API markets line up with the results data. The rest follow the same patterns.
Dots below show where a value sits in the nested response, so score.totals.half_time.points means the points value inside half_time, inside totals, inside score.
| Market | Settle from |
|---|---|
| head_to_head | Higher of score.home and score.away on that sport's scoring field, so points in most sports and runs in MLB |
| alternate_lines | Selected team's score minus the opponent's, plus that outcome's point. See below |
| alternate_total_points | score.totals.full_time.points |
| alternate_total_points_1st_half | score.totals.half_time.points |
| player_disposals | players[].player_disposals |
| alternate_total_runs | score.totals.full_time.runs |
| alternate_total_runs_1st_5_innings | score.totals.first_5_innings.runs |
| alternate_total_runs_1st_1_innings | Period 1 of score.totals.by_period |
| alternate_team_total_runs | score.home.runs or score.away.runs |
| batter_total_bases | players[].batter_total_bases |
| pitcher_strikeouts | players[].pitcher_strikeouts |
The pattern holds across the rest. Team markets read from score.home or score.away, a market covering part of a game reads that sport's named total (half_time, or first_5_innings for MLB) or the matching entries in the period arrays, single period markets read the matching entry in score.totals.by_period, and every player market maps to the stat field of the same name.
Player stat fields are named after the market key, so batter_hits settles from players[].batter_hits with no lookup table. A _milestones market settles from the same field as its base market.
Player and team names are standardised across both APIs, so they join directly. See the coverage page for the full market list.
Handicaps in detail
Handicaps are the one case where the match margin alone is not enough. Each outcome carries its own signed point, so a bet is settled from the point of view of the team that was backed, not the home team.
Using the example game above, Adelaide Crows 74 and Collingwood 108:
| Bet | Margin | Plus point | Result |
|---|---|---|---|
| Collingwood -20.5 | 108 - 74 = +34 | 34 - 20.5 = +13.5 | Win |
| Adelaide Crows +20.5 | 74 - 108 = -34 | -34 + 20.5 = -13.5 | Lose |
| Adelaide Crows +40.5 | 74 - 108 = -34 | -34 + 40.5 = +6.5 | Win |
| Adelaide Crows +34 | 74 - 108 = -34 | -34 + 34 = 0 | Push |
First half handicaps settle the same way, using the first half's entries in the two _by_period arrays in place of the final scores. That is periods 1 and 2 in a sport played in quarters, but period 1 alone in a sport played in halves, so read period_state[] rather than assuming two.
Credits
Results requests cost a flat 1 credit each, no matter how many games come back or how much detail you include.
As with odds, credits are only deducted when data is returned. Polling status=live with no games in play costs nothing.
Results and odds requests draw from the same monthly credit allowance. See pricing.
Error Codes
| Code | Meaning |
|---|---|
| 400 | Invalid status or include value |
| 401 | Invalid API key |
| 403 | Subscription is not active |
| 404 | Sport not found |
| 422 | Missing api_key |
| 429 | Insufficient credits or rate limit exceeded |
| 500 | Internal server error |
An unknown game_id is not an error. It returns 200 with an empty games array, and costs no credits.
Code Examples
Results WebSocket
Live scores pushed to you as they happen
Connecting
The results WebSocket pushes fresh scores and player stats every time a scrape cycle completes. See coverage for update frequencies. It is a separate endpoint from the odds WebSocket, and is included on the Pro and Elite plans.
Once connected, the server sends a confirmation message:
Messages
Sending — Subscribe
The status, include and days fields work exactly as they do on the REST endpoint, and all are optional. Subscribing to live is the usual choice for score tracking.
Sending — Unsubscribe
Receiving — Subscribed
Receiving — Results Update
Sent each time a scrape cycle completes. The data field is identical in structure to the REST response, and the same rule applies: full_time is null until a game is CONCLUDED.