# How to test an odds feed before you buy

> Test an odds feed against your own books and markets: read the public contract, check live coverage without a key, price calls for free, and prove the 304s.

Published 1 July 2026 · updated 5 November 2026

**Test an odds feed by running the calls your product will make: coverage against your own list, a priced first call, a freshness check, a 304 and a deliberate error.** Treat everything in a vendor's deck as a claim until you have.

## What you can check before paying

The contract, live coverage and a status per product answer without a key, and for the rest you [sign up free](https://app.oddsrelay.io/signup). Plans differ only in their tokens, and [pricing](https://oddsrelay.io/pricing) lists them.

## Read the contract before the key arrives

The OpenAPI 3.1 contract is public at `/v2/openapi.json`. It carries every route, parameter and error code, with example responses. Read it for the field names you will depend on and for how refusals are shaped. An AI coding assistant can draft a client from the same file, as [integrating an odds feed with AI tools](https://oddsrelay.io/blog/integrating-an-odds-feed-with-ai-tools) shows.

Three keyless calls · example:

```bash
# The contract
curl --compressed -s https://api.oddsrelay.io/v2/openapi.json -o openapi.json

# Live status per product, venue and sport in the UK & Ireland
curl --compressed -s "https://api.oddsrelay.io/v2/coverage?region=uk"

# One word per product: ok, slow or stale
curl --compressed -s "https://api.oddsrelay.io/v2/status?region=uk"
```

## How to evaluate odds-data coverage

Judge coverage per product, bookmaker, sport and market, because a headline book count answers none of those. Write down the bookmakers, exchanges and sports your product needs before you look at anyone's coverage page. When bet365 is the book your product cannot do without, [choosing a bet365 odds API](https://oddsrelay.io/compare/bet365-odds-apis) narrows these checks to that one book.

OddsRelay covers 140+ bookmakers, 60+ of them live in the UK & Ireland, with bet365 for featured football and UK and Irish racing, and lay prices from Betfair, Smarkets and Matchbook. With a key, `GET /v2/bookmakers` returns the named list: every bookmaker and exchange key the API serves, with its display name, its regions and whether it is an exchange. It costs no tokens, so you can diff it against your own list in a script.

### A big bookmaker count misleads

A count says how many books are on a list and nothing about what each one carries. A vendor can print a large number and still fail you in three ordinary ways:

- **Thin coverage**: a book is on the list for your sport but carries a handful of its fixtures, so it lifts the total while missing most of the card.
- **Headline markets only**: football is covered, but only the 1X2 price. Over/under, both teams to score and handicaps are missing, and those are often the markets your users want.
- **Listed but not current**: the book is named, but nothing has been read from it for your sport today. A price nobody has read for hours looks usable and is not.

### Coverage is per product, not per feed

A book that is live for football on the standard board may carry no each-way racing at all, so one yes or no for a whole feed is the wrong unit. The keyless coverage call answers per product: each venue and sport is `live`, or `interrupted_24h` (not serving now, though it served within the last 24 hours). It carries no counts and no per-venue times. The [coverage page](https://oddsrelay.io/coverage) shows only what is covered, never these statuses, so run the call itself.

Keyless GET /v2/coverage, trimmed · example:

```json
{
  "region": "uk",
  "generated_at": "2026-09-17T02:20:29Z",
  "products": {
    "each-way": [
      { "bookmaker": "7bet", "sport": "horse_racing", "status": "live" },
      { "bookmaker": "bet600", "sport": "horse_racing", "status": "live" }
    ],
    "standard": [
      { "bookmaker": "7bet", "sport": "american_football", "status": "live" }
    ]
  }
}
```

Filter it to your list and you have a first answer in one call. A book that is `live` on the product you need, for the sports you need, passes this stage. Anything else is a question to ask before you sign.

### Count the markets per bookmaker

Depth is the market count per book on a real fixture, not the markets a vendor claims across its whole dataset. `GET /v2/sports` needs a key but costs no tokens, and lists every competition on the matched boards with the market keys seen under it, so you can see whether handicaps or totals exist for your competitions at all.

With a key, the coverage route adds, for each venue and sport, the number of events it has rows for now and its `last_seen` time. Compare a bookmaker's event count with the exchanges' on the same product and sport: a wide gap on a product you need is worth raising before you sign. Then, priced first with `quote=true` (shown below), pull the raw board for a few of your fixtures and count the markets each bookmaker returns. [Walking raw](https://oddsrelay.io/docs/guides/walking-raw) shows how to page through it.

Keyed GET /v2/coverage, one product, trimmed · example:

```json
{
  "region": "uk",
  "generated_at": "2026-09-17T02:27:31Z",
  "products": {
    "raw": {
      "bookmakers": [
        { "key": "7bet", "name": "7bet",
          "sports": { "american_football": { "events": 71, "last_seen": "2026-09-17T02:27:19Z" } } }
      ],
      "exchanges": [
        { "key": "matchbook", "name": "Matchbook",
          "sports": { "american_football": { "events": 72, "last_seen": "2026-09-17T02:27:17Z" } } }
      ]
    }
  }
}
```

### Check coverage holds over time

Coverage that is whole one week can thin out by the next, because bookmakers change what they offer, and a single demo cannot show that. Read the keyless coverage document daily for a week or two and note every venue and sport on your list that goes to `interrupted_24h` or drops off the list. A venue and sport stay on the list only while they have published within the last 24 hours. What you are buying is the upkeep, the case [buying vs building an odds data layer](https://oddsrelay.io/blog/buy-vs-build-odds-data) makes.

Completeness inside a market needs its own check. A correct-score market missing two selections still renders and still looks fine, so compare a few deep markets on your fixtures with the bookmaker's own page, selection by selection.

## Keep the first call small

Price it before it runs. Add `quote=true` to any data request and it returns the call's token price instead of the board, free. On the live feed, a price comes from the board and the filters you send, never from the size of the reply, so on the standard board a narrow call with named sports and bookmakers is the cheap way to look. The [quickstart](https://oddsrelay.io/docs/guides/quickstart) runs the same quote, call and ETag steps in cURL, Python and JavaScript.

Quote, then call · example:

```bash
# 1. Price it (free)
curl --compressed -s -H "Authorization: Bearer $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/odds/standard?region=uk&sports=soccer&bookmakers=bet365&quote=true"

# 2. Make it
curl --compressed -s -H "Authorization: Bearer $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/odds/standard?region=uk&sports=soccer&bookmakers=bet365"
```

Read the first reply by hand before you write integration code. Check that your books appear under `back`, that your markets appear under `markets`, and that the event names and kick-off times match what the bookmaker shows.

One event on the standard board (trimmed) · example:

```json
{
  "meta": {
    "feed_type": "standard", "region": "uk", "odds_format": "decimal",
    "processed_at": "2026-09-17T14:20:57.271Z",
    "last_seen": { "bet365": { "soccer": "2026-09-17T14:20:55Z" } },
    "count": 1,
    "version": "v2",
    "next_cursor": null
  },
  "data": [{
    "event_id": "or_evt_917dd44bce05",
    "commence_time": "2026-09-20T14:00:00Z",
    "home_team": "Brentford",
    "away_team": "Everton",
    "markets": [{
      "key": "h2h",
      "outcomes": [{
        "name": "Brentford",
        "back": [{ "bookmaker": "bet365", "price": 3.4, "link": null }],
        "lay":  [{ "exchange": "smarkets", "price": 3.5, "available": 1620, "link": null }]
      }]
    }]
  }]
}
```

## Read the pair, then do the arithmetic

A matched board earns its name in the `lay` array. Each back offer sits beside an exchange lay offer, and `available` says how much liquidity waits at that lay price, so coverage for a matched-betting tool means the lay side too. [Lay and exchange coverage](https://oddsrelay.io/guides/lay-and-exchange-coverage-explained) goes further. Nothing on the board is derived. Work the rating and qualifying loss out yourself on a handful of rows, at your own commission rate, and check them against the [oddsmatcher widget](https://oddsrelay.io/widgets/oddsmatcher).

Your arithmetic on one pair · example:

```js
const back = 3.4, lay = 3.5, commission = 0.02;
const rating = (back / lay) * 100;                  // 97.14
const layStake = (back * 10) / (lay - commission);  // for a 10-unit back stake
```

## Check freshness

You need the timestamps in the reply and a browser. Every response carries the board's time in `meta.processed_at` and the `X-Processed-At` header. On a matched board, `meta.last_seen` gives each venue and sport's last read, so now minus that time is an upper bound on a price's age. On raw, each market's `last_update` is when that market last changed, which is not a heartbeat: a quiet market's time ages while the venue is still read. Take a few rows and compare them with the bookmaker and exchange sites at the same moment. They will not match to the penny on a moving market, but they should be close.

Run the comparison on fixtures that have not kicked off. `inPlay=only` on the standard board, served since 2 November 2026, returns only the events in play, and bet365, the book in the calls above, has no in-play prices. The [freshness guide](https://oddsrelay.io/docs/guides/freshness) covers both fields in detail.

## Prove the 304 before you set a polling rate

A self-serve key is pull only, so your running cost depends on how it answers a repeat call. Every data reply carries an `ETag`. Send it back as `If-None-Match` and an unchanged board comes back as `304 Not Modified` with an empty body, which costs no tokens. It still counts towards the rate limit, so poll at the pace your product needs rather than as fast as you can. [Conditional requests](https://oddsrelay.io/docs/guides/conditional-requests) has the details.

Ask for `format=bare` for this test. The default envelope carries `meta.processed_at` in its body, so its `ETag` changes with every new board version, and an envelope can come back `200` even when none of your prices moved. Send the second call straight after the first. A busy board can change even in that gap, and a `200` then means something in your events changed, so run the pair again.

Conditional request · example:

```bash
# 1. The same call as bare, keeping the headers
curl --compressed -s -o /dev/null -D headers.txt -H "Authorization: Bearer $ODDSRELAY_KEY" \
  "https://api.oddsrelay.io/v2/odds/standard?region=uk&sports=soccer&bookmakers=bet365&format=bare"

# 2. Straight after, send its ETag back
ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2- | tr -d '\r')
curl --compressed -s -i -H "Authorization: Bearer $ODDSRELAY_KEY" \
  -H "If-None-Match: $ETAG" \
  "https://api.oddsrelay.io/v2/odds/standard?region=uk&sports=soccer&bookmakers=bet365&format=bare" | head -n 1
# HTTP/2 304   (nothing changed: keep the rows you have)
```

## Break it on purpose

How a feed fails tells you more than the happy path. Send a bookmaker key that does not exist: the reply should be a structured error that lists the valid values. Here it is a `400` with the code `unknown_bookmaker`. The API's errors are JSON with a `code` and a `request_id` you can quote to support. A `429` carries `Retry-After`, sometimes with an empty body, so handle both. A call your balance cannot cover is a `402` that serves and spends nothing.

Then run the exact call your product will make, at roughly the rate it will make it, for a few minutes. You are checking that your client handles the retry signals and the 304s, not trying to knock the service over. If you are replacing a provider, [switching odds data providers](https://oddsrelay.io/blog/how-to-switch-odds-data-providers) runs this test beside your current feed.

## What good looks like, and the red flags

| Signal | Good | Red flag |
| --- | --- | --- |
| Schema | Public contract with example responses | Field list only after a sales call |
| Coverage | Live status per venue and sport | A book count in a PDF |
| Matched data | Back paired with exchange lay and liquidity | Back prices only, matching "coming soon" |
| Freshness | Timestamps in every reply | Speed promises with nothing to check them against |
| Cost | Free quotes and free 304s | Every repeat call billed in full |

## Run the first test

Open [the coverage list](https://oddsrelay.io/coverage) and hold it against your list of bookmakers and sports. If it covers them, run your first quoted call with [the example client](https://oddsrelay.io/docs/guides/quickstart).

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

Source: https://oddsrelay.io/blog/how-to-test-an-odds-feed-before-you-buy
