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.
{
"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_typeis the board you asked for:standard,each-way,extra-place,bog,dutching,2up.bogand2upshare the standard shape.regionisuk, the board for the UK & Ireland.odds_formatisdecimalby default: the total return per unit staked, stake included. Standard and dutching serve decimal only, and some other boards acceptoddsFormat=american, as the American odds guide explains.processed_atis the board time in ISO-8601 UTC, repeated in theX-Processed-Atheader.last_seenmaps each venue and sport to the last instant that venue was read. Its sport keys are key bases such assoccer, one level above asport_keylikesoccer_epl.countis the number of events indata.unreliable_linkslists venue and sport pairs whoselinkvalues are known not to open the event page.versionisv2, andnext_cursorisnullbecause 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_idnames 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_keyis the machine key (soccer_epl) andsport_titlethe display name.commence_timeis kick-off in UTC.- Team sports carry
home_teamandaway_team. Racing carriesvenue, the course. A fixture that does not split into sides carriesevent_name. - Each market has a
keysuch ash2h, and the matched boards use one key per market across every book. An outcome on a line market addspoint, 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.
// 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.
{
"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.
"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.
{
"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_updateis 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'slast_updateis the newest of its markets. - Raw is paged. Its
metaaddstotal,board_versionand a realnext_cursor, and the walking raw guide covers paging andupdatedSince.
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.