OpenOddsAPI API Get started

Platform

Migrate from The Odds API

Three changes: the base URL, the auth header, and a couple of key names. The response shape is close enough that most code is untouched.

The whole migration

If your client already talks to The Odds API v4, you are changing where requests go and how the key is sent. Paths, query parameters and the outcome shape carry over.

WhatThe Odds APIOpenOddsAPI
Base URLhttps://api.the-odds-api.com/v4https://api.openoddsapi.com/v1
Auth?apiKey=… query parameterX-API-Key: request header
Region paramregions=usregion=us
Moneyline marketh2hmoneyline
NOTESending the key as a query parameter also works (?api_key=…) so you can migrate in two steps, but the header is preferred — query strings end up in logs and proxy caches.

Before and after

JavaScript
// Before
const r = await fetch(
  `https://api.the-odds-api.com/v4/sports/${sport}/odds` +
  `?apiKey=${KEY}&regions=us&markets=h2h`
);

// After
const r = await fetch(
  `https://api.openoddsapi.com/v1/sports/${sport}/odds` +
  `?region=us&market=moneyline`,
  { headers: { "X-API-Key": KEY } }
);

Endpoint mapping

The Odds APIOpenOddsAPINotes
GET /v4/sportsGET /v1/sportsSame shape
GET /v4/sports/{sport}/oddsGET /v1/sports/{sport_key}/oddsAdds per-book last_update
GET /v4/sports/{sport}/scoresGET /v1/sports/{sport_key}/scoresPlanned
GET /v4/sports/{sport}/eventsGET /v1/sports/{sport_key}/eventsPlanned
GET /v1/statusKey status, quota and remaining

Sport keys

Our keys are shorter and stable. UFC is live today; the rest are in build order, so check status before you point production traffic at a sport that is not there yet.

The Odds API keyOpenOddsAPI keyStatus
mma_mixed_martial_artsufcLive
boxing_boxingboxingNext
tennis_atp / tennis_wtatennisPlanned
americanfootball_nflnflPlanned
golf_pga_tourgolfPlanned

Market keys

The Odds API keyOpenOddsAPI key
h2hmoneyline
spreadsspread
totalstotal_rounds / total_points
outrightsoutright

Response differences

The parts your code reads are the same. Outcomes keep name and price, and prices are American by default.

  • Per-bookmaker `last_update` on every book entry, so you can show or filter on freshness.
  • A `stale` flag per source in the top-level sources array — set when we have not had a good read from that book recently. Surface it rather than hiding it.
  • `bouts` instead of a bare array for fight sports, since a bout is not a home/away game. Each bout carries home, away, commence_time and books.
JSON
{
  "sport": "ufc",
  "generated_at": "2026-08-08T18:09:55.214Z",
  "sources": [
    { "book": "pinnacle", "last_update": "2026-08-08T18:09:48Z", "stale": false, "latency_ms": 37 }
  ],
  "bouts": [
    {
      "home": "Mackenzie Dern",
      "away": "Gillian Robertson",
      "commence_time": "2026-08-16T02:45:00.000Z",
      "books": [
        {
          "book": "pinnacle",
          "last_update": "2026-08-08T18:09:48Z",
          "outcomes": [
            { "name": "Mackenzie Dern", "price": -240 },
            { "name": "Gillian Robertson", "price": 201 }
          ]
        }
      ]
    }
  ]
}

Quota headers

We return the same headers you already handle, so your rate-limit and back-off logic needs no changes: x-requests-remaining and x-requests-used. A 429 means the monthly quota is spent.

Drop-in shim

If you would rather not touch call sites yet, this wrapper accepts The Odds API's parameter names and translates them.

shim.js
const BASE = "https://api.openoddsapi.com/v1";

const SPORTS = { mma_mixed_martial_arts: "ufc", boxing_boxing: "boxing" };
const MARKETS = { h2h: "moneyline", spreads: "spread", totals: "total_rounds" };

// Same call signature as your existing client; new API underneath.
export async function getOdds(sport, { regions = "us", markets = "h2h" } = {}) {
  const params = new URLSearchParams({
    region: regions.split(",")[0],
    market: MARKETS[markets] ?? markets,
  });

  const res = await fetch(`${BASE}/sports/${SPORTS[sport] ?? sport}/odds?${params}`, {
    headers: { "X-API-Key": process.env.OPENODDSAPI_KEY },
  });

  if (res.status === 429) throw new Error("quota exceeded");
  return res.json();
}

Let an agent do it

Point Claude Code or Cursor at your repo with this prompt and it will find every call site and rewrite it. The MCP server is the faster path if you would rather skip the client entirely — see MCP server.

prompt
Migrate this project from The Odds API to OpenOddsAPI.

- Base URL: https://api.the-odds-api.com/v4 -> https://api.openoddsapi.com/v1
- Auth: move the apiKey query parameter to an X-API-Key request header,
  read from the OPENODDSAPI_KEY environment variable
- Query params: regions -> region, markets -> market
- Market keys: h2h -> moneyline, spreads -> spread, totals -> total_rounds
- Sport keys: mma_mixed_martial_arts -> ufc, boxing_boxing -> boxing
- Responses now include a per-bookmaker last_update and a per-source stale
  flag; surface staleness in the UI instead of hiding it
- Keep the existing 429 back-off; the x-requests-remaining header is unchanged

Find every call site, update them, then make one real request and print the
result so I can confirm it works.

Checklist

  • Swap the base URL and move the key into the X-API-Key header.
  • Rename regions to region and markets to market.
  • Map sport and market keys using the tables above.
  • Read last_update per book and handle the stale flag.
  • Run both APIs side by side for a day and diff the prices before cutting over.
WARNINGRun in shadow mode first. Diff our prices against your current provider for at least a day before you switch production reads — that is the only way to catch a mapping mistake before your users do.
OpenOddsAPI API — Documentation v1Examples use illustrative data