Skip to content

BlogFundamentals

The anatomy of an odds API response

OddsRelay · · Updated · 6 min read

A matched-board reply from OddsRelay is one JSON envelope: a meta object that dates the board, and a data array of events that nest markets, outcomes, then back and lay offers. Each-way and dutching reshape the offers, and raw swaps the pairing for one entry per venue.

The whole envelope, once

Here is a standard board reply cut down to one football event, one market and one outcome. Every sport on a matched board nests the same way.

GET /v2/odds/standard · example

{
  "meta": {
    "feed_type": "standard",
    "region": "uk",
    "odds_format": "decimal",
    "processed_at": "2026-09-20T12:20:57.271Z",
    "last_seen": {
      "william_hill": { "soccer": "2026-09-20T12:20:55Z" },
      "bet365": { "soccer": "2026-09-20T12:20:54Z" },
      "betfair_exchange": { "soccer": "2026-09-20T12:20:56Z" }
    },
    "count": 1,
    "unreliable_links": [],
    "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": "Arsenal",
    "away_team": "Chelsea",
    "markets": [{
      "key": "h2h",
      "outcomes": [{
        "name": "Arsenal",
        "back": [
          { "bookmaker": "william_hill", "price": 2.9, "link": null },
          { "bookmaker": "bet365", "price": 2.87, "link": null }
        ],
        "lay": [
          { "exchange": "betfair_exchange", "price": 3.0, "available": 175, "link": null }
        ]
      }]
    }]
  }]
}

What the meta object dates

meta describes the board the body was cut from. It never repeats per event.

  • feed_type is the board you asked for: standard, each-way, extra-place, bog, dutching, 2up. bog and 2up share the standard shape.
  • region is uk, the board for the UK & Ireland.
  • odds_format is decimal by default: the total return per unit staked, stake included. Standard and dutching serve decimal only, and some other boards accept oddsFormat=american, as the American odds guide explains.
  • processed_at is the board time in ISO-8601 UTC, repeated in the X-Processed-At header.
  • last_seen maps each venue and sport to the last instant that venue was read. Its sport keys are key bases such as soccer, one level above a sport_key like soccer_epl.
  • count is the number of events in data.
  • unreliable_links lists venue and sport pairs whose link values are known not to open the event page.
  • version is v2, and next_cursor is null because a matched board arrives whole in one reply.

Events hold markets, markets hold outcomes

Odds data is a hierarchy, and every level answers one question. The event says what is being bet on and when. The market asks a question with a fixed set of answers. The outcome is one answer, and the offers under it carry the prices. Read any path as a sentence: for this event, in this market, this outcome is priced here.

  • event_id names the fixture, and one match keeps one id on every board. Treat it as opaque, and do not store it as a key you expect to last for months.
  • sport_key is the machine key (soccer_epl) and sport_title the display name. commence_time is kick-off in UTC.
  • Team sports carry home_team and away_team. Racing carries venue, the course. A fixture that does not split into sides carries event_name.
  • Each market has a key such as h2h, and the matched boards use one key per market across every book. An outcome on a line market adds point, the line itself.

An event carries identity and timing and no prices. Keeping prices at the leaves is what lets one event hold every book's offer without a special case per bookmaker.

Back offers and lay offers

Each outcome holds a back array and a lay array. A back offer is a bookmaker's price: bookmaker is the venue key from GET /v2/bookmakers, price is decimal odds, and link is the venue's page for the event, or null. The array is sorted best price first.

A lay offer is an exchange's price, sorted lowest first. exchange names the venue, and the offer adds available, the money on offer at that price. The lay side comes from Betfair, Smarkets, Matchbook and BETDAQ. Offers are paired against exchange lay and liquidity-gated. The rating and qualifying loss are simple arithmetic on the pair, and the oddsmatcher widget shows them.

Your arithmetic on one pair · example

// c is your own exchange commission, e.g. 0.02 for 2%
const layStake = (back.price * stake) / (lay.price - c);
const net = layStake * (1 - c) - stake; // the same whichever side wins
const rating = (back.price / lay.price) * 100;
const fits = lay.available >= layStake;

Doing that sum yourself is the right design. Only you know your commission rate, and a number computed on someone else's assumption is wrong for every customer on a different one.

Each-way and extra-place split the lay

On these boards each back offer adds places, the places the bookmaker pays, and place_fraction, its place terms (0.2 is 1/5 odds). The lay key becomes an object with a win array and a place array, and a place lay offer carries the exchange's own places.

One each-way outcome · example

{
  "name": "Albert Palais",
  "back": [
    { "bookmaker": "sky_bet", "price": 6.0, "places": 4, "place_fraction": 0.2, "link": null }
  ],
  "lay": {
    "win":   [{ "exchange": "betfair_exchange", "price": 6.4, "available": 21.71, "link": null }],
    "place": [{ "exchange": "betfair_exchange", "price": 2.16, "available": 39.42, "places": 3, "link": null }]
  }
}

The extra-place board uses the same shape. What it surfaces is a back offer paying more places than the exchange's place market, as in the sample above: four places against three. The each-way and extra-place post covers the terms.

Dutching groups back legs by dutch_id

The dutching board has no lay key. Every back offer carries dutch_id, eight hex characters naming the dutch it belongs to. A dutch is two or three legs, one per outcome, each at a different venue. Group one market's offers by dutch_id and compute 100 ÷ Σ(1/price) for each group.

A three-leg dutch in one market · example

"outcomes": [
  { "name": "Arsenal", "back": [{ "bookmaker": "william_hill", "price": 2.5, "link": null, "dutch_id": "b8d7916c" }] },
  { "name": "Draw", "back": [{ "bookmaker": "bet365", "price": 3.4, "link": null, "dutch_id": "b8d7916c" }] },
  { "name": "Chelsea", "back": [{ "bookmaker": "betfair_exchange", "price": 3.1, "link": null, "dutch_id": "b8d7916c" }] }
]

Raw keeps every venue apart

GET /v2/odds/raw answers a different question: what is each venue quoting, as that venue labels it. The event key is id, and prices sit under a bookmakers array with one entry per venue.

GET /v2/odds/raw, one event · example

{
  "id": "or_evt_da71d5873e85",
  "sport_key": "soccer_epl",
  "sport_title": "Premier League",
  "commence_time": "2026-09-20T14:00:00Z",
  "home_team": "Arsenal",
  "away_team": "Chelsea",
  "bookmakers": [{
    "key": "william_hill",
    "title": "William Hill",
    "region": "uk",
    "last_update": "2026-09-20T12:18:09Z",
    "markets": [{
      "key": "h2h",
      "last_update": "2026-09-20T12:17:02Z",
      "outcomes": [{ "name": "Arsenal", "price": 2.9 }, { "name": "Chelsea", "price": 2.5 }]
    }]
  }]
}
  • A bookmaker outcome is { name, price }. An exchange quoting a lay side is { name, back_price, lay_price, available }, with no commission applied.
  • Raw keeps each venue's own market and outcome names: a market's key is that venue's own market type, lowercased. Only the matched boards share one set of market keys, and reconciling them across books is the work the matched boards have already done.
  • A market's last_update is when that venue's market last changed. It is not a heartbeat: a quiet market's time ages while the venue is still read. The venue's last_update is the newest of its markets.
  • Raw is paged. Its meta adds total, board_version and a real next_cursor, and the walking raw guide covers paging and updatedSince.

The bare format moves meta into headers

Add ?format=bare and the reply is the data array alone. The board time stays in X-Processed-At and the event count moves to X-Count. last_seen is never sent on a bare reply, so keep the envelope if you compute price ages. Either way, each data reply carries an ETag: send it back as If-None-Match and an unchanged board is a free 304, as the conditional requests guide explains.

How old is a price in the feed?

Matched offers carry no timestamp of their own. Take meta.last_seen for the offer's venue and sport, and subtract it from now. That gives an upper bound on the price's age, and processed_at dates the board as a whole. How old is too old to show is your call. Every product on sale meets a maximum update time of 10–20 s. The freshness guide has the lookup code.

Read a live board next

The API reference lists every field and parameter, with the contract's own trimmed samples. To read a board of your own, request access, or see live coverage for what is on it today.