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.
| What | The Odds API | OpenOddsAPI |
|---|---|---|
| Base URL | https://api.the-odds-api.com/v4 | https://api.openoddsapi.com/v1 |
| Auth | ?apiKey=… query parameter | X-API-Key: request header |
| Region param | regions=us | region=us |
| Moneyline market | h2h | moneyline |
?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
// Before
const r = await fetch(
`https://api.the-odds-api.com/v4/sports/${sport}/odds` +
`?apiKey=${KEY}®ions=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 } }
);# Before
r = requests.get(
f"https://api.the-odds-api.com/v4/sports/{sport}/odds",
params={"apiKey": KEY, "regions": "us", "markets": "h2h"},
)
# After
r = requests.get(
f"https://api.openoddsapi.com/v1/sports/{sport}/odds",
params={"region": "us", "market": "moneyline"},
headers={"X-API-Key": KEY},
)Endpoint mapping
| The Odds API | OpenOddsAPI | Notes |
|---|---|---|
| GET /v4/sports | GET /v1/sports | Same shape |
| GET /v4/sports/{sport}/odds | GET /v1/sports/{sport_key}/odds | Adds per-book last_update |
| GET /v4/sports/{sport}/scores | GET /v1/sports/{sport_key}/scores | Planned |
| GET /v4/sports/{sport}/events | GET /v1/sports/{sport_key}/events | Planned |
| — | GET /v1/status | Key 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 key | OpenOddsAPI key | Status |
|---|---|---|
| mma_mixed_martial_arts | ufc | Live |
| boxing_boxing | boxing | Next |
| tennis_atp / tennis_wta | tennis | Planned |
| americanfootball_nfl | nfl | Planned |
| golf_pga_tour | golf | Planned |
Market keys
| The Odds API key | OpenOddsAPI key |
|---|---|
| h2h | moneyline |
| spreads | spread |
| totals | total_rounds / total_points |
| outrights | outright |
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
sourcesarray — 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_timeandbooks.
{
"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.
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.
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-Keyheader. - Rename
regionstoregionandmarketstomarket. - Map sport and market keys using the tables above.
- Read
last_updateper book and handle thestaleflag. - Run both APIs side by side for a day and diff the prices before cutting over.