Build an odds comparison site
Match exact markets across bookmakers, reject stale prices and keep the board current with REST or streams.
What a valid comparison needs
The biggest price is not always the best price.
An odds comparison API is useful only when the event, period, market, selection, line and settlement contract match. Miss one field and a clean-looking table can recommend a different bet.
Prove equivalence first. Then rank the prices.
What the contract gives you
| Question | Fields | Why it matters |
|---|---|---|
| Is it the same bet? | market_group_id, selection_key, period, metric, line and side | The prices belong in one comparison row. |
| Does it settle the same way? | market_contract, selection_parameters | Similar source labels do not hide different rules. |
| How current is each book? | bookmaker_as_of_ts_ms, target_refresh_interval_seconds | Freshness can be shown per bookmaker, not guessed from one page timestamp. |
| Did every requested book arrive? | complete, bookmakers_included, bookmaker_counts | The interface can distinguish complete data from a partial page. |
| Can updates continue? | resume, stream delta and resync | The board can recover after a disconnect. |
The false-comparison test
These illustrative prices show why the line matters.
| Book | Line | Odds | Same bet? |
|---|---|---|---|
| Book A | -2.5 | 1.95 | Yes · best |
| Book B | -3.5 | 2.10 | No |
| Book C | -2.5 | 1.91 | Yes |
Naive resultBook B at 2.10Wrong. It ranks a different line.
Valid resultBook A at 1.95Correct for the -2.5 comparison group.
Build the comparison key first
Do not group by team name and market label. Match the fields that define the bet.
- 01Event
event_id - 02Market
market_group_id · period · metric · line - 03Selection
selection_key · side · player_name - 04Contract
market_contract · selection_parameters
Different key. Different comparison row.
Build it in six steps
Start with one event and a small bookmaker set.
-
Choose one event
Find a covered event and keep its canonical event_id.
-
Request a complete snapshot
Request the bookmakers and markets you need, then follow next_cursor until complete is true.
-
Build an exact comparison key
Match the event, market group, selection, period, metric, line, side and settlement contract.
-
Reject unusable prices
Remove unavailable prices, incomplete market groups and rows outside your freshness policy.
-
Render the best valid price
Rank prices only inside an exact comparison group and preserve ties.
-
Keep the board current
Start from the snapshot resume token, apply stream deltas and reload after resync.
Request a complete snapshot
curl --request GET \
--header "X-API-Key: $ODDS_API_KEY" \
"https://api.odds-api.net/v1/events/{event_id}/odds/snapshot?bookmakers=pinnacle,draftkings,fanduel&market_keys=moneyline,handicap,total&periods=full%20time&include_unavailable=true&limit=2000"
If next_cursor is present, keep the filters unchanged until complete is true.
View the JavaScript grouping example
function stable(value) {
if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
if (value && typeof value === "object") {
const pairs = Object.keys(value).sort()
.map(key => `${JSON.stringify(key)}:${stable(value[key])}`);
return `{${pairs.join(",")}}`;
}
return JSON.stringify(value ?? null);
}
function comparisonKey(eventId, row) {
return [
eventId,
row.market_group_id,
row.selection_key,
row.period_str ?? row.period,
row.bet_type,
row.metric,
row.line ?? "",
row.side ?? "",
row.player_name ?? "",
stable(row.market_contract),
stable(row.selection_parameters),
].join("|");
}
function bestValidPrices(snapshot) {
const groups = new Map();
for (const row of snapshot.items) {
if (!row.is_available || typeof row.odds !== "number") continue;
const key = comparisonKey(snapshot.event_id, row);
const current = groups.get(key) ?? [];
if (!current.length || row.odds > current[0].odds) groups.set(key, [row]);
else if (row.odds === current[0].odds) current.push(row);
}
return [...groups.entries()].map(([key, books]) => ({
key,
best_odds: books[0].odds,
books,
}));
}
Run this on your backend. Never expose ODDS_API_KEY in browser JavaScript.
Show freshness per bookmaker
Use bookmaker_as_of_ts_ms for each book. Compare it with the current time and target_refresh_interval_seconds.
Current
Inside your threshold. Show the price normally.
Ageing
Show its age and stop marking it as best.
Unavailable
Keep the cell visible but inactive.
An empty cell means that book has no comparable available price.
Snapshot first. Stream when it matters.
A stream is not a complete board. Load the REST snapshot, save resume, then apply deltas.
- 01SnapshotLoad complete rows
- 02Save resumeKeep the cursor
- 03StreamSSE or WebSocket
- 04Apply deltasUpdate exact keys
- 05ResyncReload after resync
Use SSE for one-way updates or WebSockets where they fit your stack. Both use the same update contract.
Lay out the comparison screen
Show the bet, the best valid price and its age without a tooltip.
| Selection | DraftKings | FanDuel | Pinnacle |
|---|---|---|---|
| Lakers -2.5 | 1.91 6s | 1.95 8s · best | 1.93 12s |
| Celtics +2.5 | 1.91 6s | 1.88 8s | 1.96 12s · best |
Use the API commercially
Paid plans support customer-facing comparison products. Pre-match odds only. Your product owns the interface and customer experience.
50,000 credits
2 million credits
20 million credits
Raw-feed resale, bulk redistribution and API-key sharing are not included. Read the API Terms and Data License before launch, then use the current pricing limits to model requests and stream hours.
Compare the bet before the price
Match the event, market, line and settlement contract. Check freshness. Then highlight the best price.
Odds comparison API questions
Can I use odds-api.net to build an odds comparison site?
Yes. Paid plans support customer-facing odds comparison sites using covered pre-match bookmaker prices. Keep the API key on your server and follow the data licence. Raw-feed resale, bulk redistribution and API-key sharing are not included.
How do I compare the same bet across bookmakers?
Compare only rows with the same event, market group, selection, period, metric, line, side and settlement contract. A point spread of -2.5 is not the same bet as -3.5, even when both rows name the same team.
How do I know whether every requested bookmaker is included?
Use complete, bookmakers_included and bookmaker_counts on the snapshot response. When next_cursor is present, keep the filters unchanged and request the next page before treating the board as complete.
How fresh are the odds for each bookmaker?
Read bookmaker_as_of_ts_ms for each bookmaker included in the response. Compare it with the current time and target_refresh_interval_seconds. as_of_ts_ms describes the assembled snapshot, not each bookmaker's individual acceptance time.
Should an odds comparison site poll or stream?
Load a complete REST snapshot first. Use SSE or WebSockets for supported pre-match changes, save the latest resume token and load a new snapshot whenever the stream sends resync.
Which plan suits an odds comparison website?
Builder is the usual starting point for a public comparison product because it includes 2 million base API credits and eight concurrent streams. A smaller prototype may fit Starter. Calculate the events, refresh rate and stream hours before choosing.
Does odds-api.net provide in-play sportsbook odds?
No. The general sportsbook product supplies pre-match odds. Do not design a live in-play promise around this feed.
Sources and method
The contract study checked the live odds-api.net OpenAPI document, coverage catalogue, pricing and public developer package on 15 September 2026. The point-spread comparison is a controlled illustrative example, not a record of live bookmaker prices.