The anatomy of an odds API response
The odds API response format on an OddsRelay matched board 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" },
"smarkets": { "soccer": "2026-09-20T12:20:56Z" }
},
"count": 1,
"version": "v2",
"next_cursor": null
},
"data": [{
"event_id": "or_evt_917dd44bce05",
"sport_key": "soccer_premier_league",
"sport_title": "Premier League",
"competition_key": "soccer_english_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": "smarkets", "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_is the board you asked for:type standard,each-way,extra-place,bog,dutching,2up.bogand2upshare the standard shape.regionisuk, the board for the UK & Ireland.odds_isformat decimalby default: the total return per unit staked, stake included. Standard and dutching serve decimal only, and some other boards acceptoddsFormat=, as the American odds guide explains.american processed_is the board time in ISO-8601 UTC, repeated in theat X-Processed-Atheader.last_maps each venue and sport to the last instant that venue was read. Its sport keys are key bases such asseen soccer, one level above asport_likekey soccer_.premier_ league countis the number of events indata.versionisv2, andnext_iscursor nullbecause 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_names the fixture, and one match keeps one id on every board. Treat it as opaque. It never changes once served, so you can store it. How to store and version odds data covers the rare id merged into another.id sport_(key soccer_in the sample) follows the league name the venues use, so several competitions can share one, and one competition's matches can carry different ones.premier_ league sport_is its display name, andtitle commence_is kick-off in UTC.time competition_names the competition: one key per competition, the same on every live board, never changed once served, andkey nullon racing, when no venue's league name identifies one, and on every past board in historical odds.sports=with that key returns the whole competition. To pick out one competition, filter oncompetition_, notkey sport_.key - Team sports carry
home_andteam away_. Racing carriesteam venue, the course. A fixture that does not split into sides carriesevent_.name in_appears only when you ask for in-play events withplay inPlay(includeoronlyon raw,onlyon the standard and dutching boards), and it istruewhile the event's venues show it in play. By default every board leaves in-play events out.- 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. When a market is suspended or voided, how odds feeds handle suspended and void markets covers what your code should do as its prices leave the tree.
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 /, 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 and Matchbook. Only lays with enough money behind them make it onto the board. Rating and qualifying loss stay out of the reply: they are sums your code runs on the two prices, as below, and the oddsmatcher widget runs the same sums on screen.
// 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. An oddsmatcher in Python rates every back and lay pair on a whole board, and the CSV export tutorial writes each offer to its own spreadsheet row. For past prices, historical odds in Python prints one event's price changes.
Each-way and extra-place split the lay
On these boards each back offer adds places, the places the bookmaker pays, and place_, 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": "matchbook", "price": 6.4, "available": 21.71, "link": null }],
"place": [{ "exchange": "matchbook", "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_, 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_ 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": "smarkets", "price": 3.1, "link": null, "dutch_id": "b8d7916c" }] }
]Raw keeps every venue apart
GET / 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_917dd44bce05",
"sport_key": "soccer_premier_league",
"sport_title": "Premier League",
"competition_key": "soccer_english_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,. An exchange quoting a lay side isprice } { name,, with no commission applied.back_ price, lay_ price, available } - Raw lists every venue's markets under one shared set of market keys, the
markets.rawlist in keyless/, while each venue keeps its own outcome names. Reconciling those names across books is the work the matched boards have already done. Normalising odds across bookmakers covers that work. An odds screen in Python shows what unreconciled names do to a best-price table.v2/ coverage - A market's
last_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'supdate last_is the newest of its markets.update - A winner market sold each-way,
outright_in golf, horse racing and greyhound racing, adds the venue's terms:winner places,place_andfraction extra_, which isplace truewhen the venue pays more places than its standard terms. Any other set of terms a book offers for the same event is a market of its own,outright_, and an exchange's own each-way market iswinner_ ew_ <places> outright_.each_ way - Raw is paged. Its
metaaddstotal,board_and a realversion next_, and the walking raw guide covers paging andcursor updatedSince.
The bare format moves meta into headers
Add ? 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_ 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. Polling an odds feed efficiently builds the request loop around it and explains why polling suits odds data.
Every price's age is in the reply
A matched offer has no time field. Take meta.last_ for the offer's venue and sport, and subtract it from now. That gives an upper bound on the price's age, and processed_ 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 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. The freshness guide has the lookup code. Displaying odds data in your app puts that age beside each price, and monitoring a feed in production watches it over time.
Read a live reply next
The API reference lists every field and parameter, with the contract's own trimmed samples, and live coverage shows what is on the feed today. An AI coding tool can read the same contract, if you check what it writes.
Related posts
- How to build an oddsmatcherAn oddsmatcher is paired price data plus the product on top. Which half deserves your engineering time.
- Efficient odds API polling with ETag and 304sSend the ETag back as If-None-Match and an unchanged reply is an empty 304 that costs no tokens.
- How to calculate qualifying loss and ratingsThe rating and qualifying loss formulas, worked on one back and lay pair at your own commission.
18+ · OddsRelay supplies odds data only and takes no bets. Please gamble responsibly.