Skip to content

BlogBuying odds data

How to test an odds feed before you buy

OddsRelay · · Updated · 5 min read

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.

Can you try the feed before you pay?

Partly. Two parts of the feed answer without a key, and for the rest you request access. We review each request and reply by email. Plans differ only in their monthly tokens, and 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.

Two keyless calls

# 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"

Check coverage against your own list

Write down the bookmakers, exchanges and sports your product needs before you look at anyone's coverage page. A feed with a large book count is no use if it misses the three you need.

OddsRelay covers 140+ bookmakers, 60+ of them live in the UK & Ireland, bet365 included, with lay prices from Betfair, Smarkets, Matchbook and BETDAQ. The keyless coverage call shows each venue and sport, per product, as live, or interrupted (an interruption in the last 24 hours, or no rows now). The coverage page reads the same data. With a key, the same route adds event counts and each venue's last read time, which tells you whether a book that is listed actually has prices for your sports today. Evaluating coverage has the fuller method.

What should your first call look like?

Small, and priced before it runs. Add quote=true to any data request and it returns the call's token price instead of the board, free. 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 has the whole flow in three languages.

Quote, then call

# 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, keeping the headers
curl --compressed -s -D headers.txt -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 · example

{
  "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",
    "sport_key": "soccer_epl",
    "sport_title": "Premier League",
    "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": "betfair_exchange", "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 is paired against exchange lay and liquidity-gated, and available is the money on offer at that lay price. The rating and qualifying loss are simple arithmetic on the pair, and the oddsmatcher widget shows them. Do that arithmetic yourself, at your own commission rate, on a handful of rows.

Your arithmetic on one pair · example

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

How do you check freshness?

With 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. 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.

Every product on sale meets a maximum update time of 10–20 s. Run the comparison on fixtures that have not kicked off. The feed has no in-play board, so there is nothing to check once play starts. The freshness guide covers both fields in detail.

Prove the 304 before you set a polling rate

The feed 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 toward the rate limit, so poll at the pace your product needs rather than as fast as you can. Conditional requests has the details.

Conditional request · example

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" | 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.

What good looks like, and the red flags

SignalGoodRed flag
SchemaPublic contract with example responsesField list only after a sales call
CoverageLive status per venue and sportA book count in a PDF
Matched dataBack paired with exchange lay and liquidityBack prices only, matching "coming soon"
FreshnessTimestamps in every replySpeed promises with nothing to check them against
CostFree quotes and free 304sEvery repeat call billed in full

Where to start

Open the coverage page and hold it against your list of books and sports. If it covers them, request access and run your first quoted call with the quickstart.