# Odds-API.io Complete Documentation # Full API documentation for AI tools with large context windows # Last updated: September 2026 (odds/updated delta headers, gzip on every response, WebSocket compression required) ================================================================================ OVERVIEW ================================================================================ Odds-API.io is a sports betting odds comparison API providing real-time data from 365 bookmakers across 34 sports with near-zero latency. Key Features: - 365+ bookmakers supported - 34 sports coverage - Real-time odds with close to zero latency - Pre-match & In-play odds coverage - Period scores for all periods (half-time, 90min, overtime) - enabling complete market settlement - Value bet detection - Arbitrage opportunity detection - WebSocket real-time updates - Direct bet links to bookmaker sites Base URL: https://api.odds-api.io/v3 ================================================================================ AUTHENTICATION ================================================================================ Odds-API.io uses API key authentication. Include your API key as a query parameter: https://api.odds-api.io/v3/events?apiKey=YOUR_API_KEY&sport=football Getting Your API Key: 1. Sign up for free at https://odds-api.io 2. Check your email for the API key 3. Store it securely in environment variables Security Best Practices: - Never expose API keys in client-side code - Use environment variables - Make API calls from backend servers only Rate Limits: - Paid plans: 5,000 requests/hour (Solo £49/mo or £490/yr for 2 bookmakers, Starter £99/£990 for 5, Growth £179/£1,790 for 10, Pro £229/£2,290 for 15; annual is 10x monthly with 2 months free, card only; see https://odds-api.io/pricing) - Existing free keys: 100 requests/hour, capped at 500/day (new free keys are paused) - Additional packages available: +10K/20K/30K requests/hour - Rate limit headers returned with every response: - x-ratelimit-limit: Total requests allowed - x-ratelimit-remaining: Requests remaining - x-ratelimit-reset: ISO 8601 timestamp when limit resets Compression: - Every REST response is gzip compressed (Content-Encoding: gzip), regardless of the Accept-Encoding request header - Most HTTP clients decompress automatically; otherwise enable it (curl --compressed, PHP CURLOPT_ENCODING, .NET AutomaticDecompression) HTTP Status Codes: - 200: Success - 400: Bad Request - Invalid parameters - 401: Unauthorized - Invalid or missing API key - 403: Forbidden - API key doesn't have access - 404: Not Found - Resource doesn't exist - 429: Too Many Requests - Rate limit exceeded - 500: Internal Server Error ================================================================================ API ENDPOINTS ================================================================================ ------------------------------------------------------------------------------ GET /sports ------------------------------------------------------------------------------ Returns list of available sports. No authentication required. Response: [ {"name": "Football", "slug": "football"}, {"name": "Basketball", "slug": "basketball"}, ... ] ------------------------------------------------------------------------------ GET /markets ------------------------------------------------------------------------------ Returns the market catalogue: every market currently served, per sport, with how to parse it. No authentication required. Parameters: - sport (optional): Sport name or slug, e.g. Football or football. Omit for all sports. - A sport listed by /sports that is not priced yet returns an empty markets array. Only a sport outside that list is a 404. Response, one entry per market: name, shape (moneyline, line, outcomes, yesno, or labelled), period (match, first-half, second-half, quarter, set, game, or inning), prematch (bool, served on pending events) and live (bool, served on live events). Use this to look up exact market names instead of guessing; matching against the `markets` filter on /odds and related endpoints is case-insensitive but must be the full name. Example: https://api.odds-api.io/v3/markets?sport=football ------------------------------------------------------------------------------ GET /bookmakers ------------------------------------------------------------------------------ Returns list of available bookmakers. No authentication required. createdAt is the UTC time the bookmaker was added to the API (RFC3339). Response: [ {"name": "Bet365", "active": true, "createdAt": "2023-09-03T09:51:52Z"}, {"name": "SingBet", "active": true, "createdAt": "2025-04-11T12:00:00Z"}, ... ] ------------------------------------------------------------------------------ GET /leagues ------------------------------------------------------------------------------ Returns leagues for a specified sport. Parameters: - apiKey (required): Your API key - sport (required): Sport name or slug (e.g., "Football" or "football") Example: /leagues?apiKey=YOUR_KEY&sport=football Response: [ {"name": "England - Premier League", "slug": "england-premier-league", "eventsCount": 32}, {"name": "Spain - La Liga", "slug": "spain-la-liga", "eventsCount": 28}, ... ] ------------------------------------------------------------------------------ GET /events ------------------------------------------------------------------------------ Returns events with filtering options. Parameters: - apiKey (required): Your API key - sport (required): Sport name or slug - league (optional): League slug - status (optional): One status or a comma-separated list (pending, live, settled, cancelled). An unknown value inside a list returns 400. - from (optional): Start date/time in RFC3339 format - to (optional): End date/time in RFC3339 format. If omitted, defaults to 14 days from now (see "Default window" below). - limit (optional): Max events to return. Capped at 5000 per response. - skip (optional): Offset for pagination (use with limit). - bookmaker (optional): Filter events that have odds from this bookmaker - participantId (optional): Filter by team/participant ID Default window: Events are listed across a wide future horizon, so when no `to` is given the response defaults to events within the next 14 days. Pass an explicit `to` (and optionally `from`) to widen or shift the window. Every response is hard-capped at 5000 events - paginate with `limit` + `skip` for larger ranges. Example: /events?apiKey=YOUR_KEY&sport=football&status=pending,live Example: /events?apiKey=YOUR_KEY&sport=football&to=2026-09-01T00:00:00Z (full horizon) Response: [ { "id": 123456, "home": "Manchester United", "away": "Liverpool", "homeId": 38, "awayId": 42, "date": "2025-10-15T15:00:00Z", "status": "pending", "sport": {"name": "Football", "slug": "football"}, "league": {"name": "England - Premier League", "slug": "england-premier-league"}, "scores": { "home": 2, "away": 1, "periods": { "p1": {"home": 1, "away": 0}, "ft": {"home": 2, "away": 1} } } } ] Esports events: Esports events add an "esports" object (absent for every other sport): "esports": { "game": {"name": "Valorant", "slug": "valorant"}, "league": {"name": "VCT", "slug": "valorant-vct"}, "region": "Asia", "format": {"type": "bestOf", "games": 5} } - game: the game being played. - league: the parent league (VCT, LCK, LEC). Its slug stays the same across the league's splits and stages, while the top-level league names the stage. - region: Western Europe, Eastern Europe, North America, South America, Asia, Oceania or Middle East. Omitted when unknown. - format: type is bestOf or firstTo, games is the number (3 = best of 3). Omitted when unknown. The same object is returned by /events/live, /events/search and /events/{id}. Historical events do not carry it. Note on Scores: The scores object contains the final result (home/away) plus a "periods" map with the score for each period. The top-level home/away is the OT/penalty-inclusive result. Period keys that may appear: - "p1", "p2", ... "pN" - period/set/quarter scores (halves, sets, quarters, etc.) - "ft" - full-time / regulation result - "ot" - overtime / extra time / extra innings (when played) - "ap" - penalty shootout tally (when the match is decided on penalties) - "currentgame" - current game points, live tennis - "map1", "map2", ... - map winner, esports: 1 for the side that won the map, 0 for the other - "map1_score", "map2_score", ... - score within the map, esports: rounds for Counter-Strike, kills for League of Legends and Dota 2. Optional: added after a series settles, only for maps with a verified score - "inning1", "inning2", ... - per-innings runs, cricket Which keys appear depends on sport and how the match resolved. This enables complete settlement of all market types (half-time, period-specific, OT/penalties, and more). Note: the full-time key is "ft" (the legacy feed used "fulltime"). Cricket periods carry two extra fields alongside home/away runs: "periods": { "inning1": { "home": 155, "away": 125, "wickets": {"home": 6, "away": 9}, "overs": {"home": 20, "away": 20} } } - "wickets" - wickets lost by each side in that innings (integer pair) - "overs" - overs bowled, cricket notation: 19.3 means 19 overs and 3 balls Both are cricket-only and are omitted for every other sport, so existing period scores keep exactly their {home, away} shape. Both are also omitted when the source does not publish them, so treat them as optional even on cricket. A side that has not yet batted reads 0 rather than a missing key. Cricket has no match clock, so live cricket events carry no "clock" object. ------------------------------------------------------------------------------ GET /events/live ------------------------------------------------------------------------------ Returns all currently live events. Parameters: - apiKey (required): Your API key - sport (optional): Filter by sport name or slug Example: /events/live?apiKey=YOUR_KEY&sport=football Live events include an optional "clock" object with the current match clock: { "clock": { "minute": 67, // current match minute "playedSeconds": 4023, // seconds of play elapsed "period": 2, // current period (half, quarter, set) "running": true, // whether the clock is ticking "statusDetail": "2nd half", "serve": "home", // current server, or player at the table (snooker, darts) "injuryTime": 4 // announced stoppage minutes (football) } } "clock" also appears on live events returned by GET /events?status=live and GET /events/{id}. It is omitted when no clock data is available. minute/playedSeconds are extrapolated server-side and only advance while running is true; extrapolate client-side from playedSeconds for a per-second UI ticker. Within a period, backward feed corrections freeze the served clock at its last value with running=false until the feed catches up. The hold does not expire into a backward jump. A change of period resets the floor immediately. statusDetail is the feed's phase description passed through as-is (display text, not an enum; use period + running for logic). Typical values per sport: football "1st half"/"Halftime"/"2nd half"/"Penalties"; basketball "1st quarter".."4th quarter"; tennis/volleyball "1st set"/"2nd set"/...; esports "Map 1"/"Map 2"/...; baseball "1st inning".."9th inning" (untimed, no clock seconds); snooker/darts "Started". minute is cumulative elapsed play. To render football's conventional "45+X"/"90+X" style, cap at the period boundary: period 1 with minute 48 displays as "45+3". injuryTime carries the announced stoppage minutes. ------------------------------------------------------------------------------ GET /events/search ------------------------------------------------------------------------------ Search events by team or league name. Returns up to 10 results ranked by relevance. Matching is case- and accent-insensitive. Parameters: - apiKey (required): Your API key - query (required): Search term (min 3 characters) - status (optional): upcoming (default) searches events that have not started and are not cancelled; live searches in-play events and returns current scores Example: /events/search?apiKey=YOUR_KEY&query=Liverpool Example: /events/search?apiKey=YOUR_KEY&query=Liverpool&status=live ------------------------------------------------------------------------------ GET /events/{id} ------------------------------------------------------------------------------ Get a single event by ID. Parameters: - apiKey (required): Your API key - id (required): Event ID in path Example: /events/123456?apiKey=YOUR_KEY ------------------------------------------------------------------------------ GET /odds ------------------------------------------------------------------------------ Get odds for a specific event from selected bookmakers. Parameters: - apiKey (required): Your API key - eventId (required): Event ID - bookmakers (required): Comma-separated bookmaker names (max 30) - markets (optional): Comma-separated market names, case-insensitive (max 40). If none match, the event is returned with an empty bookmakers object Example: /odds?apiKey=YOUR_KEY&eventId=123456&bookmakers=Bet365,SingBet,Unibet&markets=ML,Spread,Totals Response: { "id": 123456, "home": "Manchester United", "away": "Liverpool", "date": "2025-10-15T15:00:00Z", "status": "pending", "sport": {"name": "Football", "slug": "football"}, "league": {"name": "England - Premier League", "slug": "england-premier-league"}, "urls": { "Bet365": "https://bet365.com/...", "SingBet": "https://SingBet.com/..." }, "bookmakers": { "Bet365": [ { "name": "ML", "updatedAt": "2025-10-04T10:30:00Z", "odds": [ {"home": "2.10", "draw": "3.40", "away": "3.20"} ] }, { "name": "Spread", "updatedAt": "2025-10-04T10:30:00Z", "odds": [ {"hdp": -0.5, "home": "1.95", "away": "1.85"}, {"hdp": 0, "home": "1.75", "away": "2.05"} ] }, { "name": "Totals", "updatedAt": "2025-10-04T10:30:00Z", "odds": [ {"hdp": 2.5, "over": "1.90", "under": "1.90"} ] } ] } } Market Types (pass these exact names to the `markets` parameter; matching is case-insensitive): - ML: Match result (home, draw, away) - Spread: Handicap with hdp value, including Asian lines - Totals: Totals with hdp as the line - Both Teams To Score: yes/no markets. On exchange-style books a row can also carry layYes/layNo and depthYes/depthNo/depthLayYes/depthLayNo (liquidity at the best price); depthOver/depthUnder/depthLayOver/depthLayUnder repeat the same values on yes/no markets and are kept for compatibility. Same on historical closing lines - Correct Score: Exact score predictions - Double Chance, Draw No Bet, European Handicap, Half Time / Full Time - Team Total Home / Team Total Away: totals for one side only - Half-time and period variants use a suffix: ML HT, Totals HT, Spread HT, Totals 2H - Sport-specific: Totals (Games) and Spread (Games) for tennis, Player Props for basketball - Handball: ML is always the full-time three-way result (home, draw, away); a two-way handball winner price is listed as Draw No Bet - And many more... Common mistakes: there is no market named "Match Winner" (use ML), and no bare "Over/Under" or "Asian Handicap". Note that longer names containing those words do exist, such as "Goals Over/Under" and "Alternative Corners", so match the full name exactly. Call GET /v3/markets?sport= to see the exact names a sport serves. ------------------------------------------------------------------------------ GET /odds/multi ------------------------------------------------------------------------------ Get odds for multiple events in a single request (counts as 1 API call). Parameters: - apiKey (required): Your API key - eventIds (required): Comma-separated event IDs (max 10) - bookmakers (required): Comma-separated bookmaker names (max 30) - markets (optional): Comma-separated market names, case-insensitive (max 40) Example: /odds/multi?apiKey=YOUR_KEY&eventIds=123456,123457,123458&bookmakers=Bet365,SingBet&markets=ML,Spread,Totals Response: Array of event odds objects (same format as /odds) ------------------------------------------------------------------------------ GET /odds/updated ------------------------------------------------------------------------------ Get odds updated since a specific timestamp. Parameters: - apiKey (required): Your API key - since (required): Unix timestamp (max 90 seconds old) - bookmaker (required): Bookmaker name (any name from /bookmakers; the response uses the same name) - sport (required): Sport name or slug - markets (required from 15 October 2026): Comma-separated market names, case-insensitive (max 40). Missing, or none of the names existing, returns 400; if only some are unknown the call succeeds and the X-Unknown-Markets header lists them. Valid names: /markets?sport= - limit (optional): Max results per response (1-10000) Response headers: - X-Server-Time: the server's current unix time - X-Next-Since: pass this as `since` on your next call to continue with no gap and no re-fetch Delta recipe: first call with since = now - 20, then on every later call use since = . Example: /odds/updated?apiKey=YOUR_KEY&since=1734450000&bookmaker=SingBet&sport=football&markets=ML,Spread,Totals ------------------------------------------------------------------------------ GET /odds/summary (and GET /odds/summary/multi for up to 10 events) ------------------------------------------------------------------------------ Best, average, median, worst and margin-free fair price per market line across bookmakers, for odds comparison pages. Growth plans and above; Free, Solo and Starter get a fixed sample response with "demo": true and an X-OddsAPI-Demo: true header. Parameters: - apiKey (required): Your API key - eventId (required): event ID. /odds/summary/multi takes eventIds instead: up to 10 comma-separated, returns an array, counts as 1 request - markets (optional): comma-separated, case-insensitive. Default ML,Spread,Totals. Max 20 - lines (optional): main (default) or all (every line priced by 3+ bookmakers) - bookmakers (optional): narrow to some of your selected bookmakers - includeExchanges (optional): true adds a separate exchanges block Per line: hdp, main, books, fairBooks, payout {best, average}, and outcomes keyed home/draw/away, over/under or yes/no, each with best, bestBookmakers, average, median, worst, fair. Notes: - average = n / sum(1/price) (probability space); fair = median of per-book de-vigged probabilities, renormalized; needs 3 complete books - one vote per bookmaker (clones and latency variants collapse); only books whose prices changed within 5 min prematch / 60 s live count; a row with any outlier price is kept out of best, worst and average - exchanges and prediction markets never enter best/average/median/worst - exchanges block (includeExchanges=true): best back price per outcome with venue, liquidity (stake available at that price; USD for Polymarket, Kalshi, NoVig, ProphetX) and currency - main line = most balanced line among lines priced by at least half as many books as the most-priced line - two-way moneylines in sports that also run three-way (ice hockey, futsal, bandy) are skipped; spreads and totals are unaffected ------------------------------------------------------------------------------ GET /odds/movements ------------------------------------------------------------------------------ Get historical odds movements for a specific market. History is recorded only for a fixed set of bookmakers: 10BET, 12bet, 1xbet, 22Bet, 888Sport, BC.Game, Bet365, Betano, Betclic FR, Betfair Exchange, Betfair Sportsbook, Betinia DK, BetMGM, Betsson, BetVictor, Betway, Bovada, Bwin ES, Bwin IT, CloudBet, ComeOn, Coolbet, DanskeSpil, DraftKings, Duel, Eurobet IT, FanDuel, FB Sports, GG.bet, Goldbet IT, Interwetten, Kalshi, Kambi, Ladbrokes, M88, Marathonbet, NetBet, NordicBet, Norsk Tipping, Novibet GR, ON Sharp, Paddy Power, Pamestoixima, Polymarket, Rollbit, Roobet, Sbobet, Sharp Exchange, SingBet, Sisal IT, Snai IT, SportyBet, Stake, Stoiximan, Superbet, Tipico DE, Tipsport.cz, Unibet, Unibet FR, Vbet, Veikkaus, William Hill, Winamax FR, and selected sharp books. Requesting a bookmaker outside this set returns a 404. Parameters: - apiKey (required): Your API key - eventId (required): Event ID - bookmaker (required): Bookmaker name, case-insensitive (e.g., "Bet365", "Bet365 (no latency)") - market (required): Market name, case-insensitive (e.g., "ML", "Totals", "Both Teams To Score") - marketLine (optional): Handicap line (e.g., 2.5, -1.25). 2.50 and 2.5 are the same line. Leave it out for the main line of Spread/Totals markets (the most balanced line; each entry's "hdp" shows the line at that time) or for markets without a line (ML, Draw No Bet, Both Teams To Score) Example: /odds/movements?apiKey=YOUR_KEY&eventId=123456&bookmaker=SingBet&market=ML Response: { "eventid": "123456", "bookmaker": "SingBet", "opening": {"home": 2.0, "draw": 3.2, "away": 3.5, "timestamp": 1734400000000}, "latest": {"home": 2.1, "draw": 3.3, "away": 3.4, "timestamp": 1734450000000}, "movements": [ {"home": 2.0, "draw": 3.2, "away": 3.5, "timestamp": 1734400000000}, {"home": 2.05, "draw": 3.25, "away": 3.45, "timestamp": 1734420000000}, {"home": 2.1, "draw": 3.3, "away": 3.4, "timestamp": 1734450000000} ] } Notes: - timestamp is Unix milliseconds; movements are oldest first, the last entry is the latest price - non-ML entries also carry "hdp" (the requested marketLine); on Totals the over price is in "home" and the under price in "away" - "max" (stake limit) is included only when the bookmaker publishes one - history is prematch only and keeps the opening price plus recent changes per line; how many are kept depends on the bookmaker feed - a 404 says what is missing: the bookmaker has no odds for the event, has no history for it, or lacks the market or line; it lists the markets or lines that do have history ------------------------------------------------------------------------------ ON SHARP (consensus bookmaker) ------------------------------------------------------------------------------ ON Sharp is a synthetic bookmaker computed by odds-api.io, named exactly "ON Sharp". It is a reference price, not a book you can bet at. Method: each contributing sharp, high-limit book is de-vigged, fair probabilities are blended (weighted towards the books that lead price discovery), then re-priced at a fixed margin. A line is only published when enough of the panel quotes that exact line. The composition is not published. Fixed margin: - Two-way markets: 2% (implied probabilities sum to 1.020). fair p = (1/odds) / 1.02 - Three-way markets (football ML and ML HT only): 2.5% (sum 1.025). fair p = (1/odds) / 1.025 Coverage: prematch only, lines removed at kickoff. Sports: Football, Basketball, Tennis, American Football, Baseball, Ice Hockey, Esports Markets: ML, Spread, Totals, ML HT, Spread HT, Totals HT, Corners Spread, Corners Totals, Corners Spread HT, Corners Totals HT (football), Spread (Games), Totals (Games) (tennis), First 5 Innings ML, First 5 Innings Spread, First 5 Innings Totals (baseball) Access: paid plans only (not on the free plan). Add "ON Sharp" to the bookmaker selection. - REST: /odds?eventId={id}&bookmakers=ON%20Sharp&apiKey={key} - WebSocket: odds channel, messages with "bookie": "ON Sharp"; replace markets, do not merge - History: /odds/movements?eventId={id}&bookmaker=ON%20Sharp&market=ML&apiKey={key} - The stake limit field "max" is never present for ON Sharp Uses: closing line value, fair-price and value screens, steam detection, model calibration. Guide: https://docs.odds-api.io/guides/on-sharp ------------------------------------------------------------------------------ GET /value-bets ------------------------------------------------------------------------------ Get prices above a fair-odds estimate, calculated by comparing bookmaker odds against market consensus. Parameters: - apiKey (required): Your API key - bookmaker (required): Bookmaker name - sport (optional): Filter by sport name or slug (e.g. Baseball or baseball). Server-side filter. - league (optional): Filter by league slug (e.g. usa-mlb). Requires sport to also be set. - includeEventDetails (optional): Include event info (true/false) Validation: - league without sport -> 400 - Sport outside /v3/sports -> 400 - Unknown league for a valid sport -> 404 Examples: /value-bets?apiKey=YOUR_KEY&bookmaker=Bet365&includeEventDetails=true /value-bets?apiKey=YOUR_KEY&bookmaker=Bet365&sport=baseball /value-bets?apiKey=YOUR_KEY&bookmaker=Bet365&sport=baseball&league=usa-mlb Response: [ { "id": "vb_123456_bet365_home", "eventId": 123456, "bookmaker": "Bet365", "market": { "name": "ML", "home": "2.10", "draw": "3.40", "away": "3.20" }, "betSide": "home", "expectedValue": 0.0523, "expectedValueUpdatedAt": "2025-10-04T10:30:00Z", "firstSeenAt": "2025-10-04T08:12:00Z", "bookmakerOddsUpdatedAt": "2025-10-04T10:28:41Z", "bookmakerOdds": { "home": "2.10", "draw": "3.40", "away": "3.20", "homeDirectLink": "https://bet365.com/..." }, "event": { "home": "Manchester United", "away": "Liverpool", "date": "2025-10-15T15:00:00Z", "sport": "Football", "league": "England - Premier League" } } ] Expected Value (EV) = (Probability × Odds) - 1 Positive EV indicates a value bet. Timestamps: - expectedValueUpdatedAt: last time the expected value or the sharp limit increased. It does not move when the value stays the same or falls, so it is not a freshness signal. - firstSeenAt: when this value bet first appeared. - bookmakerOddsUpdatedAt: last time this bookmaker price was written. Some bookmakers are only written when the price changes, so a price can be old and still correct. May be absent. ------------------------------------------------------------------------------ GET /arbitrage-bets ------------------------------------------------------------------------------ Get price gaps across bookmakers where the best odds for every outcome imply a total probability under 100%. Parameters: - apiKey (required): Your API key - bookmakers (required): Comma-separated bookmaker names - limit (optional): Max results (default 50, max 500) - includeEventDetails (optional): Include event info (true/false) Example: /arbitrage-bets?apiKey=YOUR_KEY&bookmakers=Bet365,SingBet&includeEventDetails=true Response: [ { "id": "arb_123456_ml", "eventId": 123456, "market": {"name": "ML", "hdp": null}, "profitMargin": 0.0215, "impliedProbability": 0.9785, "totalStake": 100, "legs": [ {"side": "home", "bookmaker": "Bet365", "odds": "2.15", "directLink": "..."}, {"side": "away", "bookmaker": "SingBet", "odds": "2.10", "directLink": "..."} ], "optimalStakes": [ {"side": "home", "bookmaker": "Bet365", "stake": 48.84, "potentialReturn": 105.00}, {"side": "away", "bookmaker": "SingBet", "stake": 51.16, "potentialReturn": 107.44} ], "event": {...} } ] ------------------------------------------------------------------------------ GET /dropping-odds ------------------------------------------------------------------------------ Get dropping odds from sharp bookmakers. Tracks significant odds movements to identify where the market is moving before recreational books adjust. Requires any paid plan. Free plan users receive 403 Forbidden. Parameters: - apiKey (required): Your API key - sport (optional): Sport name or slug (e.g., "Football" or "football"). A sport outside /sports returns 400. - league (optional): League slug (requires sport). e.g., "england-premier-league". Mutually exclusive with leagues. - leagues (optional): Comma-separated league slugs to filter by multiple leagues. e.g., "england-premier-league,spain-la-liga". Mutually exclusive with league. - markets (optional): Comma-separated market names to filter by (case-insensitive). e.g., "Spread,Totals". Supported: ML, Spread, Totals, Spread HT, Totals HT, Totals 1Q, Spread 1Q, Team Total Home, Team Total Away, First 5 Innings ML, First 5 Innings Spread, First 5 Innings Totals, Corners Spread, Corners Totals, Corners Spread HT, Corners Totals HT, Bookings Spread, Bookings Totals, Player Props - timeWindow (optional): Time window for drop filtering/sorting: "opening", "12h", "24h", "48h" (default "opening") - sort (optional): Sort order: "drop" (highest %), "recent" (latest movement), "kickoff" (soonest event). Default "drop" - minDrop (optional): Minimum drop percentage (default 0) - limit (optional): Results per page, max 200 (default 50) - page (optional): Page number, 1-indexed (default 1) - includeEventDetails (optional): Include event info like teams, date, sport, league (true/false, default false) Example: /dropping-odds?apiKey=YOUR_KEY&sport=football&minDrop=10&includeEventDetails=true Response: [ { "eventId": 69455182, "event": { "home": "Manchester City", "away": "Arsenal", "date": "2025-10-15T15:00:00Z", "sport": {"name": "Football", "slug": "football"}, "league": {"name": "England - Premier League", "slug": "england-premier-league"} }, "market": {"name": "Spread", "hdp": -1.5}, "betSide": "home", "odds": { "opening": 2.05, "current": 1.52, "drop": { "sinceOpening": 25.9, "12h": 8.4, "24h": 12.3, "48h": null } }, "lastMovedAt": 1775851240182, "updatedAt": 1775854172851 }, { "eventId": 61301179, "market": {"name": "Player Passes", "hdp": 34.5, "label": "Carlos Baleba"}, "betSide": "over", "odds": { "opening": 3.75, "current": 1.5, "drop": {"sinceOpening": 60, "12h": null, "24h": null, "48h": null} }, "lastMovedAt": 1776281915401, "updatedAt": 1776283503652 } ] Note: The "event" field is only included when includeEventDetails=true. Note: market.label contains the player name for player prop markets (e.g. "Carlos Baleba"). Omitted for non-player-prop markets. Note: market=Player Props returns all player prop markets across all sports. For football this includes sport-specific names like "Anytime Goalscorer", "Player Passes", "Player Shots", "Player Shots on Target", etc. Pagination headers returned with every response: - X-Total-Count: Total number of matching entries - X-Page: Current page number - X-Per-Page: Results per page - X-Updated-At: Timestamp of last data update (Unix milliseconds) Drop values: - sinceOpening: Total drop since opening odds were first recorded - 12h/24h/48h: Drop in the last 12/24/48 hours (null if no data for that window) ------------------------------------------------------------------------------ GET /mappings ------------------------------------------------------------------------------ Native id mappings: links our events and selections to a bookmaker's own event, market and outcome ids, so our prices can be matched to its markets. Each row also carries our event's home, away, date, sport and league. Polymarket: marketId = conditionId, outcomeId = CLOB token id, nativeEventId = Polymarket event id (same as bookmakerIds.Polymarket). Kalshi: marketId = contract ticker, outcomeId = yes or no, nativeEventId = event ticker (the game ticker, same as bookmakerIds.Kalshi, for ML; spread and total contracts have their own event tickers). For Kalshi pass marketId with outcomeId, since yes/no alone is not unique. Paid plans only; the bookmaker must be in your selection. Modes: - eventId, or eventIds (comma-separated, up to 10) - marketId, or marketIds (comma-separated, up to 10), optionally with outcomeId - outcomeId alone (400 if it matches outcomes in more than one market; add marketId) - bulk sync: bookmaker with none of the above. Returns every mapping of upcoming and live linked events as {"data": [...], "nextCursor": "..."|null} Every mode returns {"data": [...], "nextCursor": ...}; nextCursor is null for lookups and set in bulk sync while more pages remain. A lookup matching more than 5000 mappings returns 400 (fewer ids, a market filter, or bulk sync). A batch or bulk call counts as one request. Parameters: - apiKey (required): Your API key - bookmaker (required): Bookmaker name, e.g. Polymarket or Kalshi - eventId / eventIds: Our event id(s) - marketId / marketIds: The bookmaker's market id(s) - outcomeId: The bookmaker's outcome id - market (optional, every mode): Exact market name as in /odds, case-sensitive - active (optional, every mode): true or false; default both - sport (bulk only): Sport slug or name - league (bulk only): League slug, requires sport - limit (bulk only): Rows per page, 1-5000, default 1000 - cursor (bulk only): nextCursor of the previous page eventId(s) cannot be combined with marketId(s)/outcomeId. Examples: /mappings?apiKey=YOUR_KEY&bookmaker=Polymarket&eventId=71515842 /mappings?apiKey=YOUR_KEY&bookmaker=Polymarket&eventIds=71515842,71515900&market=Spread&active=true /mappings?apiKey=YOUR_KEY&bookmaker=Kalshi&marketId=KXNFLSPREAD-26OCT04NEBUF-BUF3&outcomeId=no /mappings?apiKey=YOUR_KEY&bookmaker=Kalshi&sport=american-football&limit=5000 /mappings?apiKey=YOUR_KEY&bookmaker=Kalshi&sport=american-football&limit=5000&cursor=NEXT_CURSOR Response (lookup): { "data": [ { "eventId": 71515842, "home": "Pittsburgh Steelers", "away": "Cleveland Browns", "date": "2026-10-04T17:00:00Z", "sport": {"name": "American Football", "slug": "american-football"}, "league": {"name": "USA - NFL", "slug": "usa-nfl"}, "bookmaker": "Polymarket", "market": "Spread", "hdp": 2.5, "side": "home", "nativeEventId": "885112", "marketId": "0x9d41f6a2...", "outcomeId": "10457830011928374650...", "outcomeLabel": "Browns", "extra": {"question": "Spread: Browns (-2.5)"}, "active": true, "firstSeenAt": "2026-09-28T09:15:02Z", "lastCheckedAt": "2026-09-30T11:55:10Z" } ], "nextCursor": null } Response (bulk sync): {"data": [ ...rows as above... ], "nextCursor": "WyI3MTUx..."} Loop until nextCursor is null. Pages are cached about 30 seconds. Bulk covers events starting in the future or live (started up to 6 hours ago); look up older events by eventId. Notes: - market, hdp, side, home and away follow our event's home/away, the same as /odds. When the bookmaker lists the teams the other way round, sides are swapped and the hdp sign is flipped for you. On a Spread, hdp is the home handicap on both rows. - Join to /odds on (eventId, market, hdp, side). Key a local copy on (marketId, outcomeId). - side is home, away, draw, over, under, yes or no. hdp is omitted on markets without a line. - Kalshi sides are mapped only when Kalshi's own ticker identifies the team. - Mappings are refreshed about every five minutes until kickoff, independently of prices, so a mapping stays available while a price is missing. - In-play: rows are still served but not refreshed (lastCheckedAt stops at kickoff) and new in-play contracts are not added. - active=false means the bookmaker stopped listing the outcome; the row is kept. - Polymarket lists many spreads from each team's side; both contracts are returned as two lines, each with its own marketId. - Football ML on Polymarket is three Yes/No markets; the Yes token of each is mapped. - Tennis: Totals = total sets, Totals (Games) = total games. ------------------------------------------------------------------------------ GET /participants ------------------------------------------------------------------------------ Get teams/participants for a sport. Parameters: - apiKey (required): Your API key - sport (required): Sport name or slug - search (optional): Filter by name Example: /participants?apiKey=YOUR_KEY&sport=football&search=Chelsea Response: [ {"id": 45, "name": "Chelsea", "sport": "football"}, {"id": 78, "name": "Chelsea Women", "sport": "football"} ] Esports participants also carry "game" (slug of the game the team plays) and, when known, "shortCode": {"id": 9002805226862, "name": "G2 Esports", "sport": "esports", "game": "valorant", "shortCode": "G2"} An organisation that fields teams in several games has one participant per game, so the same name can appear more than once. Use "game" to tell them apart. ------------------------------------------------------------------------------ GET /participants/{id} ------------------------------------------------------------------------------ Get participant by ID. Example: /participants/45?apiKey=YOUR_KEY ------------------------------------------------------------------------------ GET /participants/{id}/logo ------------------------------------------------------------------------------ Returns the team/participant crest as a PNG image, served from the Odds-API domain (no third-party URLs exposed). Responses carry Cache-Control: public, max-age=86400, so the URL can be hotlinked in tags. Returns 404 when no logo exists for the participant. Use homeId/awayId from event responses as the participant id. Esports teams are covered too. Example: /participants/45/logo?apiKey=YOUR_KEY ------------------------------------------------------------------------------ GET /bookmakers/selected ------------------------------------------------------------------------------ Get bookmakers selected by the user. Example: /bookmakers/selected?apiKey=YOUR_KEY ------------------------------------------------------------------------------ PUT /bookmakers/selected/select ------------------------------------------------------------------------------ Add bookmakers to user's selection. Parameters: - apiKey (required): Your API key - bookmakers (required): Comma-separated bookmaker names Example: /bookmakers/selected/select?apiKey=YOUR_KEY&bookmakers=Bet365,SingBet ------------------------------------------------------------------------------ PUT /bookmakers/selected/clear ------------------------------------------------------------------------------ Clear all selected bookmakers. Rate limited to once every 12 hours; the cooldown may be shorter for newly created accounts. Example: /bookmakers/selected/clear?apiKey=YOUR_KEY ================================================================================ WEBSOCKET API ================================================================================ Real-time odds updates via WebSocket connection. Endpoint: wss://api.odds-api.io/v3/ws?apiKey=YOUR_API_KEY&markets=ML,Spread,Totals Parameters: - apiKey (required): Your API key - markets (required for the odds channel): Comma-separated market names (max 40). Example: ML,Spread,Totals - channels (optional): Comma-separated subscription channels. Default is "odds" (existing behavior). Options: odds, scores, status. Example: channels=odds,scores,status - sport (optional): Comma-separated sport names or slugs (max 10). Example: football,basketball - leagues (optional): Comma-separated league slugs (max 20). Example: england-premier-league - eventIds (optional): Comma-separated event IDs (max 50). Example: 12345,67890 - status (optional): "live" or "prematch" - lastSeq (optional): Resume from this sequence number. The server replays missed messages before resuming the live stream. IMPORTANT: - permessage-deflate compression is required on every connection; uncompressed connections will stop being accepted soon. Node ws, browsers and Python `websockets` negotiate it by default. Python `websocket-client` and PHP `textalk/websocket` cannot compress, use `websockets` / `amphp/websocket-client` instead - The markets parameter is required for the odds channel - leagues and eventIds cannot be used together - Using leagues or eventIds is recommended to reduce bandwidth - channels defaults to "odds": omit it to keep the existing odds-only stream unchanged Channels: - odds (default): live odds/markets. Message types: created, updated, deleted, no_markets. - scores: live score changes. Message type: score. - status: match status transitions (pending, live, settled, cancelled). Message type: status. Subscribe to any combination, e.g. channels=scores,status for a scoreboard without odds traffic. IMPORTANT: channels is an allowlist - you receive ONLY the channels you list. To keep receiving odds you must include odds (e.g. channels=odds,scores,status); channels=scores,status stops odds delivery. Omitting channels entirely keeps the default odds-only stream. sport, leagues, eventIds and status filters apply to score/status messages too. Example URLs: wss://api.odds-api.io/v3/ws?apiKey=xxx&markets=ML,Spread,Totals&sport=football&status=live wss://api.odds-api.io/v3/ws?apiKey=xxx&markets=ML&leagues=england-premier-league,spain-la-liga wss://api.odds-api.io/v3/ws?apiKey=xxx&markets=ML,Totals&eventIds=12345,67890 Message Types: - welcome: Connection confirmed (includes active filters) - created: New match added (odds channel) - updated: Match or market changed (odds channel) - deleted: Match removed (odds channel) - no_markets: Match exists but no markets available, or none in your markets= list (odds channel) - deleted and no_markets carry an empty markets array and are always delivered, even with a markets= filter. - deleted and no_markets DO respect status=, sport= and leagues= filters and carry the event date, so a pre-match removal is not delivered on a status=live connection. - no_markets is also sent when the bookmaker still prices the event but none of your markets= remain, carrying the seq of the update it replaces. Clear what you hold for that event and bookmaker. It is sent once per disappearance (repeats are suppressed until your markets return) and can arrive for an event not yet seen on the connection. - score: Live score changed (scores channel) - status: Match status changed to pending (new match added)/live/settled/cancelled (status channel) - resync_required: Replay cannot be served safely. Client must rebuild state from a REST snapshot and reconnect. Replay & Resync: - Each update message includes a globally increasing seq. Persist it client-side and reconnect with lastSeq. Rarely an odds update arrives without seq (brief server-side interruption): apply it but keep lastSeq unchanged; a reconnect from before it gets current state instead of a replay. - Replay is compacted latest-state replay per eventId:bookie, not every intermediate tick. - deleted and no_markets states are replayed as the latest known state within retention. - If replay is too large or one or more replay payloads have expired, the server pushes current state as `created` updates and continues live on the same connection; resync_required is sent when complete state cannot be guaranteed (nothing cached, reason snapshot_truncated, or reason replay_window_dirty when the server dropped or skipped seqs before your lastSeq), in which case refetch from REST and discard your stored last_seq; it is also sent with reason snapshot_cooldown when you keep reconnecting with the same last_seq you were already served a snapshot for; nothing is replayed then, so rebuild from REST like any other resync_required instead of adopting current_seq. - Recovery path: fetch /odds or /odds/multi with includeSeq=true, read the X-OddsAPI-Seq response header, then reconnect with lastSeq set to that value. Welcome Message (sent on connection): { "type": "welcome", "message": "Connected to OddsAPI WebSocket", "user_id": "user123", "bookmakers": ["Bet365", "Unibet"], "sport_filter": ["Football"], "leagues_filter": ["england-premier-league"], "status_filter": "live", "market_filter": ["ML", "SPREAD", "TOTALS"], "channels": ["odds", "scores", "status"], "connected_at": "2026-01-15T21:00:00Z" } Update Message Format: { "type": "updated", "seq": 482917, "timestamp": 1723992773, "id": "63017989", "bookie": "SingBet", "url": "https://www.singbet.com/sports/football/match/63017989", "markets": [ { "name": "ML", "updatedAt": "2024-01-15T10:30:00Z", "odds": [{"home": "1.85", "draw": "3.25", "away": "2.10", "max": 500}] } ] } Score Message Format (scores channel) - scores only, no status field: { "type": "score", "timestamp": 1723992773, "id": "63017989", "scores": {"home": 2, "away": 1, "periods": {"p1": {"home": 1, "away": 0}}}, "clock": {"minute": 67, "playedSeconds": 4023, "period": 2, "running": true, "statusDetail": "2nd half"} } The optional "clock" snapshot (same shape as the clock field on GET /events/live) rides on live score and live-status messages when clock data is available, so a client can re-anchor its local ticker on every real event. Omitted on pending/settled/cancelled messages and on sports without a clock. The clock arrives only with an event, so a 0-0 match or one watched mid-half sends nothing until the next event: seed the initial clock with one GET /events/live on connect, then let the WebSocket re-anchor it. Status Message Format (status channel): { "type": "status", "timestamp": 1723992773, "id": "63017989", "status": "settled", "scores": {"home": 2, "away": 1, "periods": {"p1": {"home": 1, "away": 0}}} } Note on score/status: these carry no bookie and no markets - they describe the event itself. The status message includes the current scores, so a status-only subscriber gets the final result on settle. score/status messages are best-effort live state and carry no seq; they are not replayed on reconnect - fetch the current value from GET /events after reconnecting. Error Responses (400 Bad Request): - Too many sports. Maximum 10 allowed. - Too many leagues. Maximum 20 allowed. - Too many event IDs. Maximum 50 allowed. - Too many markets. Maximum 40 allowed. - Cannot use both 'leagues' and 'eventIds' filters together. - Invalid status filter. Use 'prematch' or 'live' - Invalid channel: X. Allowed: odds, scores, status Pricing: 2x the REST API price (add-on feature, subscribe at https://odds-api.io) Bookmakers: The WebSocket automatically sends updates for all bookmakers you have selected in your account. Manage bookmakers via PUT /bookmakers/selected/select endpoint. Replace, do not merge: every `updated` message carries the complete current market set for that event and bookmaker, not only the markets that changed. Overwrite your stored markets for that event with what arrives. Suspended markets are removed from the payload rather than flagged, so their absence is the signal. Merging incoming markets into existing state leaves suspended markets in your data permanently. ================================================================================ SUPPORTED SPORTS (34 total) ================================================================================ Football (football) Basketball (basketball) Tennis (tennis) Baseball (baseball) American Football (american-football) Ice Hockey (ice-hockey) Esports (esports) Darts (darts) MMA (mixed-martial-arts) Boxing (boxing) Handball (handball) Volleyball (volleyball) Snooker (snooker) Table Tennis (table-tennis) Rugby (rugby) Cricket (cricket) Waterpolo (water-polo) Futsal (futsal) Beach Volley (beach-volleyball) Aussie Rules (aussie-rules) Floorball (floorball) Squash (squash) Beach Soccer (beach-soccer) Lacrosse (lacrosse) Curling (curling) Padel (padel) Bandy (bandy) Gaelic Football (gaelic-football) Beach Handball (beach-handball) Athletics (athletics) Badminton (badminton) Cross-Country (cross-country) Golf (golf) Cycling (cycling) ================================================================================ CODE EXAMPLES ================================================================================ ------------------------------------------------------------------------------ JavaScript/Node.js - Fetch Events and Odds ------------------------------------------------------------------------------ const apiKey = process.env.ODDS_API_KEY; // Get football events const eventsResponse = await fetch( `https://api.odds-api.io/v3/events?apiKey=${apiKey}&sport=football` ); const events = await eventsResponse.json(); // Get odds for first event const eventId = events[0].id; const oddsResponse = await fetch( `https://api.odds-api.io/v3/odds?apiKey=${apiKey}&eventId=${eventId}&bookmakers=Bet365,SingBet` ); const odds = await oddsResponse.json(); // Find best home odds let bestHomeOdds = { bookmaker: null, odds: 0 }; for (const [bookmaker, markets] of Object.entries(odds.bookmakers)) { const ml = markets.find(m => m.name === 'ML'); if (ml && parseFloat(ml.odds[0].home) > bestHomeOdds.odds) { bestHomeOdds = { bookmaker, odds: parseFloat(ml.odds[0].home) }; } } console.log(`Best home odds: ${bestHomeOdds.odds} at ${bestHomeOdds.bookmaker}`); ------------------------------------------------------------------------------ Python - Fetch Value Bets ------------------------------------------------------------------------------ import requests import os api_key = os.environ['ODDS_API_KEY'] response = requests.get( 'https://api.odds-api.io/v3/value-bets', params={ 'apiKey': api_key, 'bookmaker': 'Bet365', 'includeEventDetails': True } ) value_bets = response.json() # Filter bets with > 3% expected value top_bets = [bet for bet in value_bets if bet['expectedValue'] >= 0.03] top_bets.sort(key=lambda x: x['expectedValue'], reverse=True) for bet in top_bets[:5]: print(f"{bet['event']['home']} vs {bet['event']['away']}") print(f"Bet on: {bet['betSide']}") print(f"EV: {bet['expectedValue'] * 100:.2f}%") print("---") ------------------------------------------------------------------------------ JavaScript - WebSocket Connection ------------------------------------------------------------------------------ const WebSocket = require('ws'); const apiKey = process.env.ODDS_API_KEY; const params = new URLSearchParams({ apiKey, markets: 'ML,Spread,Totals', sport: 'football', status: 'live' }); const ws = new WebSocket(`wss://api.odds-api.io/v3/ws?${params}`); let lastSeq = 0; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'resync_required') { console.log(`Resync required: ${data.reason}`); console.log('Fetch a REST snapshot with includeSeq=true, rebuild local state, then reconnect with that seq.'); return; } if (data.seq) { lastSeq = data.seq; } switch (data.type) { case 'welcome': console.log(`Connected! Filters: ${data.sport_filter}, ${data.status_filter}`); break; case 'created': console.log(`New match: ${data.id}`); break; case 'updated': console.log(`Updated: ${data.id} - ${data.markets.length} markets`); break; case 'deleted': console.log(`Deleted: ${data.id}`); break; case 'no_markets': console.log(`No markets: ${data.id}`); break; } }; ws.onerror = (err) => console.error("Error:", err); ws.onclose = () => console.log("Disconnected"); ------------------------------------------------------------------------------ cURL Examples ------------------------------------------------------------------------------ # Get sports (no auth required) curl "https://api.odds-api.io/v3/sports" # Get bookmakers (no auth required) curl "https://api.odds-api.io/v3/bookmakers" # Get football events curl "https://api.odds-api.io/v3/events?apiKey=YOUR_KEY&sport=football" # Get odds for an event curl "https://api.odds-api.io/v3/odds?apiKey=YOUR_KEY&eventId=123456&bookmakers=Bet365,SingBet" # Get value bets curl "https://api.odds-api.io/v3/value-bets?apiKey=YOUR_KEY&bookmaker=Bet365&includeEventDetails=true" # Get arbitrage opportunities curl "https://api.odds-api.io/v3/arbitrage-bets?apiKey=YOUR_KEY&bookmakers=Bet365,SingBet" # Get dropping odds (paid plans only) curl "https://api.odds-api.io/v3/dropping-odds?apiKey=YOUR_KEY&sport=football&minDrop=10&includeEventDetails=true" ================================================================================ BEST PRACTICES ================================================================================ 1. Use Multi-Odds Endpoint - Fetch up to 10 events in one request (counts as 1 API call) - Significantly reduces rate limit usage 2. Cache Responses Appropriately - Event lists: 5-10 minutes - Pre-match odds: 30-60 seconds - Live odds: stream via WebSocket instead of caching, a socket is cheaper than any poll interval 3. Select Only Needed Bookmakers - Request only bookmakers your users need - Reduces response size and processing time 4. Filter Events by Bookmaker - Use ?bookmaker=Bet365 on /events endpoint to only get events with odds from that bookmaker - Much more efficient than fetching all events and filtering client-side - Example: /events?sport=football&bookmaker=Bet365&apiKey={key} 5. Handle Errors Gracefully - Implement exponential backoff for rate limits (429) - Not all bookmakers offer all markets - Handle resync_required by rebuilding from a REST snapshot with includeSeq=true 6. Monitor Rate Limits - Check response headers for remaining requests - Implement queuing for high-volume applications 7. Use lastSeq for Reconnects - Persist seq from every WebSocket update - Reconnect with lastSeq to recover missed updates without rebuilding state when replay is still available ================================================================================ CONTACT & SUPPORT ================================================================================ Email: hello@odds-api.io Status: https://status.odds-api.io (current API status, incident history, email updates) Website: https://odds-api.io Documentation: https://docs.odds-api.io Bookmakers List: https://odds-api.io/sportsbooks ## Historical Data ### GET /historical/events Past events for a league and date range. Required: apiKey, sport (slug), league (slug), from (RFC3339), to (RFC3339). Response shape matches /events. homeId/awayId cover ~90% of events from March 2026 onward (partial for February 2026, absent earlier); rows without a resolved id omit the fields. Example: https://api.odds-api.io/v3/historical/events?sport=football&league=england-premier-league&from=2026-06-01T00:00:00Z&to=2026-07-01T00:00:00Z&apiKey=KEY ### GET /historical/odds Historical odds for a past event. Required: apiKey, eventId (from /historical/events), bookmakers (comma-separated, max 30). Optional: markets (comma-separated market names; returns all markets when omitted). Response shape matches /odds. The bookmakers parameter accepts any name listed by /v3/bookmakers, and the response reports odds under those same names. ### GET /historical/closing-lines Closing odds for every settled event across up to 10 leagues in one request. Paid plans only (free plans get 403). Required: apiKey, sport (slug), leagues (comma-separated slugs, max 10), from (RFC3339), to (RFC3339, max 366 days after from, so a full season fits in one sweep), markets (comma-separated), bookmakers (comma-separated, max 30). Optional: limit (capped at 100), skip. Returns an array of events, each shaped like the /historical/odds response. Example: https://api.odds-api.io/v3/historical/closing-lines?sport=football&leagues=england-premier-league,spain-la-liga&from=2026-01-01T00:00:00Z&to=2026-01-31T23:59:59Z&markets=ML&bookmakers=Bet365,Unibet&apiKey=KEY ## Bet Settlement Two endpoints for settling bets from final results. Paid plans only (free plans get 403). Each call counts as one request. Guide: https://docs.odds-api.io/guides/settlement ### GET /settlements Settlement record for up to 50 events. Required: apiKey, eventIds (comma-separated, 1 to 50, counted before duplicates are removed). Response: { "settlements": [ { eventId, sport, status, final, settledAt?, scores? } ], "notFound": ["id"] } - status: pending (not started), live, awaiting_result (over, result not confirmed yet), settled, cancelled (usually also postponed and abandoned matches). - final: true when you can stop polling. Set once the result has been checked against the official score, or 48 hours after the match ended. Cancelled events become final 48 hours after kickoff. - scores: only for settled events. Same shape as scores on /events (home, away, periods). The top-level score can include extra time; periods.ft is the regulation score. - notFound: ids we have never seen. Always present. Example: https://api.odds-api.io/v3/settlements?eventIds=74919888,73101436&apiKey=KEY ### POST /settlements/grade Grades up to 50 bet selections. apiKey stays in the query string. JSON body, at most 64 KB: { "selections": [ { "ref": "bet-1", "eventId": 74919888, "market": "Spread", "hdp": -0.75, "side": "home" } ] } Selection fields: - eventId (required): number or numeric string. - market (required): the market name exactly as served on /odds (case-insensitive). No fuzzy matching. - side (required): home, away, draw, over, under, yes, no, odd, even, 1x, 12, x2, none, or an HT/FT pair such as draw/home. - hdp: the line as served on /odds. Handicaps are stated for the home side (for the away side of a -1.5 row send side away with hdp -1.5). Required on Spread, Totals, team totals, European Handicap, corners, bookings and Player Props. Must be a multiple of 0.25. - label: required for Player Props, as served, for example "Arike Ogunbowale (Assists)". - tieRule: optional, lose or void. Only used when a moneyline score is level in a sport where bookmakers differ (for example rugby, Aussie rules). Ignored for MMA and boxing. - ref: optional string up to 64 characters, echoed back. Response: { "grades": [ { ref, eventId, result, reason, final, basis? } ] }, in the order sent. One bad selection never fails the batch. - result: won, lost, void, half_won, half_lost, pending, ungradeable. - reason: null for won, lost, half_won, half_lost. Otherwise one of: push, event_cancelled (void); event_not_started, event_live, awaiting_result, awaiting_stats (pending); event_not_found, unsupported_market, invalid_side, invalid_line, invalid_label, incomplete_score, ambiguous_tie, player_not_matched, stats_unavailable, result_unavailable (ungradeable). - final: true when you can stop polling this selection. Re-send selections that were not final, at most once a minute. - basis: { scope, home, away }, the score the verdict was computed from. Only on graded score markets. scope is regulation, full_game, match_result, first_half, second_half, first_period, period_2, period_3, games, or map_1 to map_5. Settlement rules: - Football, futsal, handball, rugby: every market settles on regulation time (90 minutes in football). Extra time and penalties never count. - Basketball, American football, baseball, ice hockey: ML, Spread, Totals, Team Total Home/Away, Odd/Even and Both Teams To Score include overtime. 3-Way Result, Double Chance, European Handicap and Draw No Bet settle on regulation. A hockey shootout adds one goal to the winner. Second-half markets in basketball and American football include overtime. - Tennis and other set sports settle on sets; esports on maps. A tennis match that was not played to the end (retirement, walkover) is ungradeable with incomplete_score. A plain tennis Spread handicap with an absolute value greater than 3 (for example -4.5) or a Totals line above 5 cannot be a sets line, so it is graded on total games (basis scope games). In volleyball, table tennis, badminton and esports a plain Spread or Totals line outside the sets or maps range is ungradeable with incomplete_score. Map Handicap and Total Maps are always graded on maps. - Quarter lines split the stake between the two neighbouring lines and can return half_won or half_lost. A whole line landed exactly is void with reason push. - A level score on ML: football, futsal and handball lose (draw is its own side); American football and baseball are void; other sports need tieRule or return ambiguous_tie. 3-Way Result always treats a level score as a draw. - Cancelled events: every selection is void with reason event_cancelled. Postponed and abandoned matches are usually reported as cancelled. An event that never gets a result and is never marked cancelled stays pending with awaiting_result, then becomes ungradeable with result_unavailable 72 hours after kickoff. - MMA and boxing: a result with no winner (draw or no contest) is ungradeable with incomplete_score for ML and 3-Way Result. - Corners, bookings and First Team To Score are football only, graded from post-match statistics about 30 minutes after full time. Booking points: yellow 1, red 2, second yellow leading to red 2 more. No basis is returned for these. - Player Props are graded for basketball, baseball, ice hockey and American football from the box score. An ambiguous or absent player is ungradeable with player_not_matched. - The grader never guesses: when the stored score or statistic cannot prove the answer, the result is ungradeable. Supported markets: ML, Moneyline, 3-Way Result, Spread, Totals, Goals Over/Under, Team Total Home, Team Total Away, European Handicap, Double Chance, Draw No Bet, Both Teams To Score, Odd/Even, Half Time / Full Time, the HT versions (ML HT, Spread HT, Totals HT, Team Total Home HT, Team Total Away HT, European Handicap HT, Double Chance HT, Draw No Bet HT, Both Teams To Score HT), the 2H versions (ML 2H, Spread 2H, Totals 2H, European Handicap 2H, Both Teams To Score 2H), Spread 1Q, Totals 1Q, First 5 Innings ML/Spread/Totals, 1st Inning ML, 1st Inning Totals, Spread (Games), Totals (Games), ML 1st Set, Totals 1st Set, ML 2nd Set, Totals 2nd Set, ML 3rd Set, Map Handicap, Total Maps, 1st Map ML, Corners Totals, Corners Spread, Corners Totals Home, Corners Totals Away, Alternative Corners and their HT versions, Bookings Totals, Bookings Spread, Bookings Totals Home, Bookings Totals Away and their HT versions, First Team To Score, Player Props. Anything else returns unsupported_market (for example Correct Score, goalscorer markets, football player props, cricket). Example: curl -X POST "https://api.odds-api.io/v3/settlements/grade?apiKey=KEY" -H "Content-Type: application/json" -d '{"selections":[{"ref":"bet-1","eventId":74919888,"market":"ML","side":"home"}]}' ## Billing and Payments Plans are billed monthly in the visitor's local currency (GBP, USD, EUR, BRL, CNY, JPY, KRW). Cards are charged automatically each cycle. ### Paying with crypto Customers can pay with cryptocurrency instead of a card, on any plan. Payments are processed by CoinGate (licensed as a crypto-asset service provider by the Bank of Lithuania). Supported coins include BTC, ETH, USDC, TRX, SOL and LTC, 10+ coins in total; VAT is added by country where Odds-API.io is registered to collect it, and VAT numbers (reverse charge) are only supported on card payments; prices are quoted in fiat and converted at the rate shown on the payment page. Key difference from cards: crypto cannot be charged automatically, because funds cannot be pulled from a wallet. Each renewal is therefore issued as an invoice with a payment link, sent by email and shown in the dashboard billing page. API access continues while the invoice is open. Reminders follow if it goes unpaid, and an expired payment link can be regenerated from the dashboard. Upgrades, extra request packages and the WebSocket add-on also work on crypto: the prorated amount due is shown before confirming, then paid via a payment link, and the change activates once the payment confirms. Refunds on crypto payments are arranged manually via hello@odds-api.io. Switching between crypto and card billing is handled by support and keeps the same plan and API key. Guide: https://docs.odds-api.io/guides/paying-with-crypto ================================================================================ END OF DOCUMENTATION ================================================================================