RapidOddsAPI bookmaker odds API logoRapidOddsAPI

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:

credits = market_types × ⌈bookmakers / 5⌉

Every 5 bookmakers counts as 1 group (rounded up).

Examples

Market TypesBookmakersCredits
111
151
162
2104
3159

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

PyPIGitHub

The official Python client. Handles auth and retries for you, and returns typed responses. Requires Python 3.9 or newer.

pip install rapidoddsapi

Quickstart

from rapidoddsapi import RapidOddsAPI client = RapidOddsAPI(api_key="oa_your_api_key_here") odds = client.get_odds("AFL", ["head_to_head"], ["Sportsbet", "TAB", "Ladbrokes"]) for entry in odds["games"]: game = entry["game"] print(game["away_team"], "at", game["home_team"]) for book in entry["bookmakers"]: for market in book["markets"]: for outcome in market["outcomes"]: print(" ", book["name"], outcome["name"], outcome["price"])

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

npmGitHub

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.

npm install rapidoddsapi

Quickstart

import { RapidOddsAPI } from "rapidoddsapi"; const client = new RapidOddsAPI({ apiKey: "oa_your_api_key_here" }); const odds = await client.getOdds("AFL", ["head_to_head"], ["Sportsbet", "TAB", "Ladbrokes"]); for (const entry of odds.games) { console.log(entry.game.away_team, "at", entry.game.home_team); for (const book of entry.bookmakers) { for (const market of book.markets) { for (const outcome of market.outcomes) { console.log(" ", book.name, outcome.name, outcome.price); } } } }

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

PyPIGitHub

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:

{ "mcpServers": { "rapidoddsapi": { "command": "uvx", "args": ["rapidoddsapi-mcp"], "env": { "RAPIDODDSAPI_API_KEY": "oa_your_api_key_here" } } } }

Tools

ToolCredits
list_sports0
get_oddsmarket_types × ⌈bookmakers / 5⌉
get_results1
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.

GET https://api.rapidoddsapi.com/sports/{sport_id}/markets?api_key={your_api_key}&market_type={market_type}&bookmaker={bookmaker}

Parameters

ParameterTypeRequiredDescription
sport_idpathYesSport identifier — see coverage
api_keyqueryYesYour API key
market_typequery (multi)YesMarket type. Repeat for multiple — see coverage
bookmakerquery (multi)YesBookmaker name. Repeat for multiple — see coverage

Example

GET https://api.rapidoddsapi.com/sports/NBA/markets?api_key=your_api_key&market_type=head_to_head&bookmaker=Sportsbet&bookmaker=DraftKings&bookmaker=Pinnacle
{ "sport": "NBA", "games": [ { "game": { "commence_time": "2026-02-14T10:00:00", "home_team": "Charlotte Hornets", "away_team": "Detroit Pistons" }, "bookmakers": [ { "name": "Sportsbet", "last_update": "2026-02-14T05:34:27", "markets": [ { "key": "head_to_head", "outcomes": [ { "name": "Detroit Pistons", "price": 1.65 }, { "name": "Charlotte Hornets", "price": 2.29 } ] } ] }, { "name": "DraftKings", "last_update": "2026-02-14T05:33:12", "markets": [ { "key": "head_to_head", "outcomes": [ { "name": "Detroit Pistons", "price": 1.62 }, { "name": "Charlotte Hornets", "price": 2.35 } ] } ] }, { "name": "Pinnacle", "last_update": "2026-02-14T05:34:01", "markets": [ { "key": "head_to_head", "outcomes": [ { "name": "Detroit Pistons", "price": 1.67 }, { "name": "Charlotte Hornets", "price": 2.31 } ] } ] } ] } ] }

Response

FieldTypeDescription
sportstringSport display name
games[]arrayList of games with odds
gameobjectGame details
commence_timestringGame start time (ISO 8601)
home_teamstringHome team name
away_teamstringAway team name
game_urlstringDeep link to this event on the bookmaker's site
bookmakers[]arrayRequested bookmakers with data for this game
namestringBookmaker name
last_updatestringLast time odds were updated (ISO 8601)
markets[]arrayMarkets for this bookmaker
keystringMarket type identifier
outcomes[]arrayOdds outcomes for this market
namestringOutcome name (team or Over/Under)
pricenumberDecimal odds
pointnumberLine value — spreads, totals, and player props
player_namestringPlayer name — player props only
team_namestringTeam 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

CodeMeaning
401Invalid API key
403Subscription is not active
404Sport not found
422Missing required parameter (api_key, market_type, or bookmaker)
429Insufficient credits or rate limit exceeded
500Internal server error

Code Examples

curl "https://api.rapidoddsapi.com/sports/NBA/markets?\ api_key=your_api_key&\ market_type=head_to_head&\ bookmaker=Sportsbet&\ bookmaker=DraftKings"

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:

WSS wss://api.rapidoddsapi.com/ws?api_key=your_api_key

Once connected, the server sends a confirmation message:

{ "event": "connected", "tier": "pro", "message": "Send subscribe messages to start receiving odds data." }

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.

{ "action": "subscribe", "sport": "NBA", "bookmakers": ["Sportsbet", "DraftKings"], "market_types": ["head_to_head"] }

Sending — Unsubscribe

{ "action": "unsubscribe", "sport": "NBA" }

Receiving — Unsubscribed

{ "event": "unsubscribed", "sport": "NBA" }

Receiving — Subscribed

Sent immediately after a successful subscribe. Includes the credit cost per push for your subscription.

{ "event": "subscribed", "sport": "NBA", "bookmakers": ["Sportsbet", "DraftKings"], "market_types": ["head_to_head"], "credits_per_push": 1 }

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.

{ "event": "odds_update", "sport": "NBA", "timestamp": "2026-02-25T10:00:00.000000", "credits_charged": 1, "data": { "sport": "NBA", "games": [ ... ] } }

Receiving — Error

{ "event": "error", "message": "Insufficient credits. Need 2, have 0." }

Credits

Credits are charged per push, not per connection. The same formula as the REST endpoint applies:

credits = market_types × ⌈bookmakers / 5⌉

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).

TypeCode / EventMeaning
Connection4001Close code sent before the connection opens: invalid or missing API key, inactive subscription, or plan does not include WebSocket access
In sessionerrorInsufficient credits, invalid sport, missing bookmakers or market types, or unknown action

Code Examples

import asyncio import websockets import json async def main(): url = "wss://api.rapidoddsapi.com/ws?api_key=your_api_key" async with websockets.connect(url) as ws: # Connected confirmation connected = json.loads(await ws.recv()) print(connected["message"]) # Subscribe to a sport await ws.send(json.dumps({ "action": "subscribe", "sport": "NBA", "bookmakers": ["Sportsbet", "DraftKings"], "market_types": ["head_to_head"] })) sub = json.loads(await ws.recv()) print(f"Subscribed — credits per push: {sub['credits_per_push']}") # Receive pushes async for message in ws: data = json.loads(message) if data["event"] == "odds_update": games = data["data"]["games"] print(f"{len(games)} games — credits charged: {data['credits_charged']}") asyncio.run(main())

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.

GET https://api.rapidoddsapi.com/results/{sport}?api_key={your_api_key}

Parameters

ParameterTypeRequiredDescription
sportpathYesSport identifier, for example AFL or MLB. Not case sensitive
api_keyqueryYesYour API key, the same one used for odds
statusqueryNoall (default), live, concluded, or upcoming
includequeryNoComma separated: scores, players. Defaults to both
game_idqueryNoReturn a single game.
roundqueryNoFilter to one round number. Only for sports played in rounds. Sports without them return nothing for any value, and carry a null round_number
daysqueryNoOnly 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

ValueMeaningIncludes score and players?
SCHEDULEDFixtured, not yet startedNo
LIVEIn progressYes, partial and updating
CONCLUDEDFinishedYes, 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

GET https://api.rapidoddsapi.com/results/AFL?api_key=your_api_key&status=concluded&include=scores,players
{ "sport": "AFL", "games": [ { "game": { "game_id": 8216, "commence_time": "2026-07-23T09:30:00", "last_update": "2026-07-25T07:53:41", "home_team": "Adelaide Crows", "away_team": "Collingwood", "status": "CONCLUDED", "round_number": 20 }, "score": { "home": { "team": "Adelaide Crows", "goals": 11, "behinds": 8, "points": 74 }, "away": { "team": "Collingwood", "goals": 16, "behinds": 12, "points": 108 }, "home_by_period": [ { "period": 1, "goals": 5, "behinds": 3, "points": 33 }, { "period": 2, "goals": 2, "behinds": 3, "points": 15 }, { "period": 3, "goals": 2, "behinds": 0, "points": 12 }, { "period": 4, "goals": 2, "behinds": 2, "points": 14 } ], "away_by_period": [ { "period": 1, "goals": 3, "behinds": 2, "points": 20 }, { "period": 2, "goals": 3, "behinds": 2, "points": 20 }, { "period": 3, "goals": 4, "behinds": 4, "points": 28 }, { "period": 4, "goals": 6, "behinds": 4, "points": 40 } ], "period_state": [ { "period": 1, "completed": true, "period_seconds_elapsed": 1901 }, { "period": 2, "completed": true, "period_seconds_elapsed": 1846 }, { "period": 3, "completed": true, "period_seconds_elapsed": 2021 }, { "period": 4, "completed": true, "period_seconds_elapsed": 1950 } ], "totals": { "full_time": { "goals": 27, "behinds": 20, "points": 182 }, "half_time": { "goals": 13, "behinds": 10, "points": 88 }, "by_period": [ { "period": 1, "goals": 8, "behinds": 5, "points": 53 }, { "period": 2, "goals": 5, "behinds": 5, "points": 35 }, { "period": 3, "goals": 6, "behinds": 4, "points": 40 }, { "period": 4, "goals": 8, "behinds": 6, "points": 54 } ] } }, "players": [ { "name": "Nick Daicos", "team": "Collingwood", "player_disposals": 39, "player_kicks": 22, "player_handballs": 17, "player_goals": 1, "player_marks": 6, "player_tackles": 3, "player_clearances": 7, "player_fantasy_points": 116, "behinds": 0 } ] } ] }

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.

{ "sport": "MLB", "games": [ { "game": { "game_id": 401816253, "commence_time": "2026-07-25T22:10:00", "last_update": "2026-07-26T00:09:15", "home_team": "Tampa Bay Rays", "away_team": "Cleveland Guardians", "status": "LIVE", "round_number": null }, "score": { "home": { "team": "Tampa Bay Rays", "runs": 3, "hits": 9, "errors": 0 }, "away": { "team": "Cleveland Guardians", "runs": 0, "hits": 4, "errors": 0 }, "home_by_period": [ { "period": 1, "runs": 1 }, { "period": 2, "runs": 0 } ], "away_by_period": [ { "period": 1, "runs": 0 }, { "period": 2, "runs": 0 } ], "period_state": [ { "period": 1, "completed": true }, { "period": 6, "completed": true }, { "period": 7, "completed": false, "half": "Bottom" } ], "totals": { "full_time": null, "first_5_innings": { "runs": 2 }, "by_period": [ { "period": 1, "runs": 1 }, { "period": 2, "runs": 0 } ] } }, "players": [ { "name": "Steven Kwan", "team": "Cleveland Guardians", "batter_hits": 1, "batter_runs": 0, "batter_rbis": 0, "batter_home_runs": 0, "batter_walks": 0, "batter_strikeouts": 1, "batter_hits_runs_rbis": 1, "batter_singles": 1, "batter_total_bases": 1, "doubles": 0, "triples": 0 }, { "name": "Cole Sulser", "team": "Tampa Bay Rays", "pitcher_strikeouts": 1, "pitcher_earned_runs": 0, "pitcher_hits_allowed": 0, "pitcher_walks_allowed": 0, "pitcher_outs": 3 } ] } ] }

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

FieldTypeDescription
sportstringSport display name
games[]arrayList of games
gameobjectAlways present
game_idintegerStable identifier, use with the game_id parameter
commence_timestringScheduled start (UTC)
last_updatestringWhen we last refreshed this game (UTC)
home_team / away_teamstringTeam names, standardised to match the odds API
statusstringSee the status table above
round_numberinteger or nullRound this game belongs to. Null for sports not played in rounds, such as MLB
scoreobjectPresent once a game starts, if include has scores
home / awayobjectTeam name plus that sport's scoring fields. AFL goals, behinds, points. MLB runs, hits, errors
home_by_period[] / away_by_period[]arrayThat team's score in each period. Per period, not cumulative
period_state[]arrayPer 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
totalsobjectBoth teams combined, see Settling Bets below
full_timeobject or nullFinal match total. Null until the game is CONCLUDED
half_time / first_5_inningsobject or nullThe 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[]arrayCombined total for each period played so far
scoring_events[]arrayEvery 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[]arrayEvery player who has taken the field, if include has players
name / teamstringPlayer name and their team, both standardised
{stat_name}integer or booleanOne 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}.

FieldMeasuresCountsResets each period?
period_seconds_elapsedTime played in the current periodUp, from 0Yes
period_seconds_remainingTime left in the current periodDown, to 0Yes
match_seconds_elapsedTime played since the start of the matchUp, from 0No
display_clockThe clock as that sport writes it, for example "43'" or "8:57"Stringn/a
halfWhere an MLB inning is up to: Top, Bottom, Middle or EndStringn/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.

totals = entry["score"]["totals"] if totals["full_time"] is not None: settle(total_points=totals["full_time"]["points"]) # safe, game is final else: running = sum(p["points"] for p in totals["by_period"]) # display only # "points" here is the scoring field for this sport. MLB uses "runs".

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.

MarketSettle from
head_to_headHigher of score.home and score.away on that sport's scoring field, so points in most sports and runs in MLB
alternate_linesSelected team's score minus the opponent's, plus that outcome's point. See below
alternate_total_pointsscore.totals.full_time.points
alternate_total_points_1st_halfscore.totals.half_time.points
player_disposalsplayers[].player_disposals
alternate_total_runsscore.totals.full_time.runs
alternate_total_runs_1st_5_inningsscore.totals.first_5_innings.runs
alternate_total_runs_1st_1_inningsPeriod 1 of score.totals.by_period
alternate_team_total_runsscore.home.runs or score.away.runs
batter_total_basesplayers[].batter_total_bases
pitcher_strikeoutsplayers[].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.

margin = points(selected team) - points(opponent) result = margin + outcome.point > 0 win < 0 lose = 0 push (only possible on whole number lines)

Using the example game above, Adelaide Crows 74 and Collingwood 108:

BetMarginPlus pointResult
Collingwood -20.5108 - 74 = +3434 - 20.5 = +13.5Win
Adelaide Crows +20.574 - 108 = -34-34 + 20.5 = -13.5Lose
Adelaide Crows +40.574 - 108 = -34-34 + 40.5 = +6.5Win
Adelaide Crows +3474 - 108 = -34-34 + 34 = 0Push

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.

credits = 1 per request

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

CodeMeaning
400Invalid status or include value
401Invalid API key
403Subscription is not active
404Sport not found
422Missing api_key
429Insufficient credits or rate limit exceeded
500Internal server error

An unknown game_id is not an error. It returns 200 with an empty games array, and costs no credits.

Code Examples

curl "https://api.rapidoddsapi.com/results/AFL?\ api_key=your_api_key&\ status=live&\ include=scores,players"

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.

WSS wss://api.rapidoddsapi.com/results-ws?api_key=your_api_key

Once connected, the server sends a confirmation message:

{ "event": "connected", "tier": "pro", "message": "Send subscribe messages to start receiving results data." }

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.

{ "action": "subscribe", "sport": "AFL", "status": "live", "include": ["scores", "players"], "days": 2 }

Sending — Unsubscribe

{ "action": "unsubscribe", "sport": "AFL" }

Receiving — Subscribed

{ "event": "subscribed", "sport": "AFL", "status": "live", "include": ["scores", "players"], "days": 2, "credits_per_push": 1 }

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.

{ "event": "results_update", "sport": "AFL", "timestamp": "2026-07-25T08:31:49.714572", "credits_charged": 1, "data": { "sport": "AFL", "games": [ ... ] } }

Receiving — Error

{ "event": "error", "message": "Invalid include value(s): score. Use one or both of: scores, players" }

Code Examples

import asyncio import websockets import json async def main(): url = "wss://api.rapidoddsapi.com/results-ws?api_key=your_api_key" async with websockets.connect(url) as ws: connected = json.loads(await ws.recv()) print(connected["message"]) await ws.send(json.dumps({ "action": "subscribe", "sport": "AFL", "status": "live", "include": ["scores", "players"] })) sub = json.loads(await ws.recv()) print(f"Subscribed, {sub['credits_per_push']} credit per push") async for message in ws: data = json.loads(message) if data["event"] != "results_update": continue for entry in data["data"]["games"]: game, score = entry["game"], entry["score"] totals = score["totals"] # full_time is null until the game is CONCLUDED, so use # by_period for a live running total. if totals["full_time"] is None: running = sum(p["points"] for p in totals["by_period"]) print(f"{game['home_team']} live, total {running}") else: print(f"{game['home_team']} FINAL, " f"total {totals['full_time']['points']}") asyncio.run(main())