# The anatomy of an odds API response

> Every field in an OddsRelay /v2 odds response, walked once: the meta envelope, events, markets, outcomes, back and lay offers, each-way, dutching and raw.

Published 1 July 2026 · updated 5 November 2026

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

GET /v2/odds/standard · example:

```json
{
  "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_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](https://oddsrelay.io/docs/guides/american-odds) 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_premier_league`.
- `count` is the number of events in `data`.
- `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. It never changes once served, so you can store it. [How to store and version odds data](https://oddsrelay.io/blog/how-to-store-and-version-odds-data) covers the rare id merged into another.
- `sport_key` (`soccer_premier_league` 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. `sport_title` is its display name, and `commence_time` is kick-off in UTC.
- `competition_key` names the competition: one key per competition, the same on every live board, never changed once served, and `null` on racing, when no venue's league name identifies one, and on every past board in [historical odds](https://oddsrelay.io/products/historical-odds). `sports=` with that key returns the whole competition. To pick out one competition, filter on `competition_key`, not `sport_key`.
- Team sports carry `home_team` and `away_team`. Racing carries `venue`, the course. A fixture that does not split into sides carries `event_name`.
- `in_play` appears only when you ask for in-play events with `inPlay` (`include` or `only` on raw, `only` on the standard and dutching boards), and it is `true` while the event's venues show it in play. By default every board leaves in-play events out.
- 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. When a market is suspended or voided, [how odds feeds handle suspended and void markets](https://oddsrelay.io/blog/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 /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 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](https://oddsrelay.io/widgets/oddsmatcher) runs the same sums on screen.

Your arithmetic on one pair · example:

```ts
// 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](https://oddsrelay.io/blog/odds-api-in-python) rates every back and lay pair on a whole board, and [the CSV export tutorial](https://oddsrelay.io/blog/export-odds-to-csv) writes each offer to its own spreadsheet row. For past prices, [historical odds in Python](https://oddsrelay.io/blog/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_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:

```json
{
  "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](https://oddsrelay.io/blog/horse-racing-and-each-way-odds-data) 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:

```json
"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 /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:

```json
{
  "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, price }`. An exchange quoting a lay side is `{ name, back_price, lay_price, available }`, with no commission applied.
- Raw lists every venue's markets under one shared set of market keys, the `markets.raw` list in keyless `/v2/coverage`, 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](https://oddsrelay.io/blog/how-to-normalise-odds-across-bookmakers) covers that work. [An odds screen in Python](https://oddsrelay.io/blog/build-an-odds-screen) shows what unreconciled names do to a best-price table.
- 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.
- A winner market sold each-way, `outright_winner` in golf, horse racing and greyhound racing, adds the venue's terms: `places`, `place_fraction` and `extra_place`, which is `true` when 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_winner_ew_<places>`, and an exchange's own each-way market is `outright_each_way`.
- Raw is paged. Its `meta` adds `total`, `board_version` and a real `next_cursor`, and the [walking raw guide](https://oddsrelay.io/docs/guides/walking-raw) 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](https://oddsrelay.io/docs/guides/conditional-requests) explains. [Polling an odds feed efficiently](https://oddsrelay.io/blog/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_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 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](https://oddsrelay.io/docs/guides/freshness) has the lookup code. [Displaying odds data in your app](https://oddsrelay.io/blog/how-to-display-odds-data-in-your-app) puts that age beside each price, and [monitoring a feed in production](https://oddsrelay.io/blog/how-to-monitor-an-odds-feed-in-production) watches it over time.

## Read a live reply next

The [API reference](https://oddsrelay.io/docs/reference) lists every field and parameter, with the contract's own trimmed samples, and [live coverage](https://oddsrelay.io/coverage) shows what is on the feed today. [An AI coding tool](https://oddsrelay.io/blog/integrating-an-odds-feed-with-ai-tools) can read the same contract, if you check what it writes.

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

Source: https://oddsrelay.io/blog/anatomy-of-an-odds-api-response
