# How odds comparison sites use odds data

> How an odds comparison site turns many bookmakers' prices into one best-price table, and which part of that job a raw odds feed does for you.

Published 1 July 2026 · updated 5 November 2026

**Anyone working out how to build an odds comparison site faces one data job: read many bookmakers' prices for the same fixture, line up each selection across them, and highlight the highest number.** Reading the prices is the easy half. Proving that every column in a row is the same bet is the work.

## What is an odds comparison site?

An odds comparison site shows many bookmakers' prices for the same bet side by side and highlights the best one. Underneath, it is a best-price table. For a football match a row is one selection, Arsenal to win, and each column is one book's current price for it. The front end is thin. Whether users trust it comes down to the data behind it.

## Four things the data layer must get right

- **Breadth.** Users expect the books they bet with, bet365 among them. A UK table without bet365 looks incomplete, however good the rest is.
- **Line-up.** Every book's price in a row has to refer to the same fixture, market and selection.
- **Freshness.** Prices move. A price that has changed at the book since you read it looks usable and isn't, so your table needs to know how old each price is.
- **Completeness.** A missing price reads as a fact. If a book is absent from a row because it wasn't captured, the table implies that book is worse, which is the wrong fact.

## Lining up prices is the hard part

Books describe the same thing differently. One lists a team as Celtic, another as Celtic FC. One names a market by its own internal type, another uses a different word for the same bet. Until those are reconciled you have two lists side by side, and a best price picked across them is a guess.

Lining up happens at three levels: the fixture, the market and the selection. Get all three right and the best price is a plain `max()` across the row. Get one wrong and you either miss the best price or compare two different bets and publish a false best. [Normalising odds across books](https://oddsrelay.io/blog/how-to-normalise-odds-across-bookmakers) goes through the mechanics.

> **The costly bug is a false comparison** A missing price is visible. Two cells that look aligned but refer to different outcomes are not, and they put a wrong best price in front of a user who then clicks out to the book. Check your mapping against the book's own page before a new market goes live on the table.

## What the raw feed hands a comparison site

The [raw feed](https://oddsrelay.io/products/raw-feed) (`GET /v2/odds/raw`) carries every venue's prices per event in one schema, from 140+ bookmakers, 60+ of them live in the UK & Ireland, bet365 included. Each event lists the venues joined to it under `bookmakers[]`, though one fixture can still arrive as two or more events, so group them on kick-off and team names. Each venue's markets carry their own `last_update`, the time that market last changed. The sample below is trimmed, with the event's `sport_key` and `sport_title` left out.

One raw event, trimmed to a pair of bookmakers · example:

```json
{
  "id": "or_evt_ed3ca8739560",
  "cluster_id": null,
  "commence_time": "2026-09-17T19:00:00Z",
  "home_team": "Celtic FC",
  "away_team": "Ferencvaros",
  "bookmakers": [{
    "key": "7bet",
    "title": "7bet",
    "region": "uk",
    "last_update": "2026-09-17T16:31:51Z",
    "markets": [{
      "key": "h2h",
      "last_update": "2026-09-17T15:45:04Z",
      "outcomes": [
        { "name": "Celtic FC", "price": 1.71 },
        { "name": "Draw", "price": 4.0 },
        { "name": "Ferencvaros TC", "price": 4.0 }
      ]
    }]
  }, {
    "key": "betgoodwin",
    "title": "BetGoodwin",
    "region": "uk",
    "last_update": "2026-09-17T16:31:43Z",
    "markets": [{
      "key": "h2h",
      "last_update": "2026-09-17T13:39:55Z",
      "outcomes": [
        { "name": "Celtic", "price": 1.7 },
        { "name": "Draw", "price": 4.0 },
        { "name": "Ferencváros", "price": 4.5 }
      ]
    }]
  }]
}
```

Look at the outcome names: one venue sends Celtic FC and Ferencvaros TC, the other Celtic and Ferencváros. Raw puts both venues in one event here, under the same `h2h` market key, but each venue keeps its own outcome labels, as it lists them. Mapping those onto your own selections is the part your code owns. It is a small, testable table per market, and it is where a comparison site's accuracy is decided. [Our odds screen tutorial](https://oddsrelay.io/blog/build-an-odds-screen) runs a best-price table in Python over this same event, with a third venue's spelling added.

Your code: best price per selection · example:

```ts
// selectionFor is your mapping: (venue, market key, outcome name) -> your selection id, or null
const best = new Map()
for (const venue of event.bookmakers) {
  for (const market of venue.markets) {
    for (const o of market.outcomes) {
      const sel = selectionFor(venue.key, market.key, o.name)
      if (!sel || o.price == null) continue
      const top = best.get(sel)
      if (!top || o.price > top.price) {
        best.set(sel, { venue: venue.key, price: o.price, changed: market.last_update })
      }
    }
  }
}
```

The book margin behind a row is arithmetic on the same prices, if your table shows it. The [overround entry](https://oddsrelay.io/glossary/overround) has the formula, and our [study of UK bookmaker margins](https://oddsrelay.io/blog/uk-bookmaker-margins-study) shows how far that margin varies on the same matches.

## Cutting the feed to your table

A comparison page rarely needs every venue and every market. Raw filters on `sports`, `leagues`, `bookmakers` and `markets`, and on a kick-off window with `commenceTimeFrom` and `commenceTimeTo`, so a page can pull only the books you have deals with. Naming sports, bookmakers or markets also narrows what the call costs, which [filters and tokens](https://oddsrelay.io/docs/guides/filters-and-tokens) explains. Add `includeLinks=true` and each venue that has one carries a `link` to its page for the event, which is your click-out.

Raw is paged. Follow `meta.next_cursor` until it is `null`, and dedupe by event `id`, since an event whose kick-off moves during a walk can appear twice or be missed. Take a missed one on the next walk. [Walking raw](https://oddsrelay.io/docs/guides/walking-raw) covers the cursor and what happens when one expires.

Some markets need no mapping at all. Where an exchange also prices the market, the matched standard board already puts the bookmakers' prices for a selection on one outcome, best first. [Affiliates and comparison sites](https://oddsrelay.io/for/affiliates-and-comparison-sites) shows which view of a comparison site reads which board. Raw covers everything beyond that.

## How fresh the table needs to be

Fresh enough that the best price you show is still the price when the user lands on the book. Every reply carries `meta.processed_at`, the board time. The published bar for every product on sale is a maximum update time of 10–20 s for match odds, and for every market within six hours of kick-off. Further from kick-off, prices in the other markets can be older. Since 2 November 2026 raw can fill an in-play column too: `inPlay=include` adds the events in play to a walk and marks every event with `in_play`, though bet365 and William Hill have no in-play prices ([live vs pre-match odds data](https://oddsrelay.io/blog/live-vs-pre-match-odds-data) explains the split).

Read a market's `last_update` for what it is: when that price last changed. It is not a heartbeat. A quiet market can show an old `last_update` while its price is still current, so treat it as the age of the price, never as a sign the venue went away. Venue-level freshness is `last_seen` in your key's `GET /v2/coverage`.

To follow changes, walk raw again with `updatedSince` set to the start of your previous walk. You get only the venues whose markets changed since then, and a recent `updatedSince` pays a reduced share of the price. Send each reply's `ETag` back as `If-None-Match` and an unchanged page answers `304`, which costs no tokens ([conditional requests](https://oddsrelay.io/docs/guides/conditional-requests)).

## Which part to build yourself

License the collection and own the mapping. Sourcing every book and keeping it running as sites change is an open-ended job that no user of your table ever sees. The selection mapping and the way you present the row are where comparison sites differ, and they are small enough to own. [Getting bookmaker odds into your app](https://oddsrelay.io/blog/how-to-get-bookmaker-odds-data) walks the licensed route from the first call.

If odds collection is your core business, owning it can be the right call. [Buy vs build](https://oddsrelay.io/blog/buy-vs-build-odds-data) sets out the costs on each side.

## Checking the books your table needs

Start on [the coverage page](https://oddsrelay.io/coverage), which lists every bookmaker and the sports it carries on each feed. The [raw feed page](https://oddsrelay.io/products/raw-feed) and [the docs](https://oddsrelay.io/docs) carry the full shape.

18+ · OddsRelay supplies odds data only and takes no bets. Please gamble responsibly.

Source: https://oddsrelay.io/blog/how-odds-comparison-sites-use-data
