Skip to content

BlogFundamentals

How odds feeds handle suspended and void markets

OddsRelay · · Updated · 5 min read

A suspended price is one the bookmaker has paused, and OddsRelay's matched boards show it by leaving it out. A void market is a settlement decision about bets already placed, which is a results question rather than a pricing one.

That split decides how you build. Suspension is something you read from the prices in hand. Voiding is something you learn from the bookmaker's settlement, which a price feed does not carry.

What is a suspended market?

A suspended market is one the bookmaker has temporarily stopped taking bets on. It happens whenever news could change the true odds before a trader has repriced the market: team news, a non-runner in a race, a late market rebuild, or a trader pulling the market to look again.

The key word is temporary. Nothing has been settled and the selection still exists. It comes back, usually at a new price, or the market closes. For a matched-betting product both sides can pause independently, the bookmaker's back price and the exchange's lay price, and a pair is only usable while both are on offer.

Most suspensions people picture happen in play, after a goal or a red card. The OddsRelay feed is built for pre-match data and has no in-play product, so the suspensions it reflects are the pre-match kind.

What is a void market?

A void market is a settlement outcome: bets on it are treated as if they never happened and stakes go back. Voiding is permanent and it looks backwards. A fixture is abandoned, a player withdraws before the match starts, a market was offered in error, or one of the bookmaker's own rules triggers no-bet.

Mixing them up costs in both directions. Treating a void market as paused keeps an opportunity alive that no longer exists, and treating a short suspension as a void throws away a selection that is about to come back.

Three ways a feed can show a suspension

Across the industry, a feed tends to represent a paused price in one of three ways. Knowing which one yours uses is most of the job.

  • A null price. The selection stays in the response with an empty price. You keep the row but must never let the null reach a calculation.
  • A status field. Each row carries a flag such as suspended or open. The state is explicit, and your code branches on it.
  • Absence. A price nobody is offering is left out of the response. What is on the board is what is on offer.

Each works. The failure is a fourth pattern, where a feed keeps repeating the last price it saw after the bookmaker pulled it. That price looks perfectly usable and is not.

OddsRelay's boards use absence

Board rows on /v2 carry no status field. A back price the bookmaker no longer offers is missing from the first board built after that venue is read again. The same goes for a lay offer from any of Betfair, Smarkets, Matchbook and BETDAQ. An outcome left without a back offer or a lay offer goes too, and then any market or event with nothing left in it.

So a price pulled since a venue's last read can still be on the board you hold. The board's build time is meta.processed_at (also the X-Processed-At header), and meta.last_seen gives the last time each venue and sport was read. Now minus last_seen is an upper bound on how old a venue's price can be. The freshness guide covers both.

Here are two publishes of the standard board, some minutes apart. In the first, both teams have a pair. In the second, the bookmaker has pulled its Chelsea price, so the Chelsea outcome is gone while Arsenal's pair remains.

Two publishes of GET /v2/odds/standard (example, trimmed)

// first publish
{
  "meta": { "feed_type": "standard", "region": "uk", "processed_at": "2026-09-17T14:02:10.114Z", "version": "v2" },
  "data": [{
    "event_id": "or_evt_917dd44bce05",
    "sport_key": "soccer_epl",
    "home_team": "Arsenal",
    "away_team": "Chelsea",
    "commence_time": "2026-09-20T14:00:00Z",
    "markets": [{
      "key": "h2h",
      "outcomes": [
        { "name": "Arsenal",
          "back": [{ "bookmaker": "william_hill", "price": 2.9, "link": null }],
          "lay":  [{ "exchange": "betfair_exchange", "price": 3.0, "available": 175, "link": null }] },
        { "name": "Chelsea",
          "back": [{ "bookmaker": "william_hill", "price": 2.5, "link": null }],
          "lay":  [{ "exchange": "betfair_exchange", "price": 2.56, "available": 90, "link": null }] }
      ]
    }]
  }]
}

// a later publish: the Chelsea back price was pulled
{
  "meta": { "feed_type": "standard", "region": "uk", "processed_at": "2026-09-17T14:09:41.530Z", "version": "v2" },
  "data": [{
    "event_id": "or_evt_917dd44bce05",
    "markets": [{
      "key": "h2h",
      "outcomes": [
        { "name": "Arsenal",
          "back": [{ "bookmaker": "william_hill", "price": 2.9, "link": null }],
          "lay":  [{ "exchange": "betfair_exchange", "price": 3.0, "available": 175, "link": null }] }
      ]
    }]
  }]
}

Absence does not say why. A suspension, a closed market and a bookmaker taking the event down all look the same on the board.

One status does exist, on a different route. GET /v2/events lists a sport's events for discovery, and each carries a status of upcoming or started. It tells you whether the event has kicked off. It says nothing about whether a given price is on offer.

Diff against what you hold

Because a paused price is an absence, you detect it by comparing the new board with the one you already have. Key every offer by event, market, outcome, line and venue. Anything in your store that is missing from the latest board is no longer on offer.

Your code: find back offers that left the standard board (example)

const keyOf = (e, m, o, venue) => `${e.event_id}|${m.key}|${o.name}|${o.point ?? ""}|${venue}`;

function offerKeys(board) {
  const keys = new Set();
  for (const e of board.data)
    for (const m of e.markets)
      for (const o of m.outcomes)
        for (const b of o.back) if (b.price != null) keys.add(keyOf(e, m, o, b.bookmaker));
  return keys;
}

const before = offerKeys(previousBoard);
const now = offerKeys(latestBoard);
const pulled = [...before].filter((k) => !now.has(k)); // hide or flag these

Poll with the board's ETag in If-None-Match. An unchanged board answers 304 and costs no tokens, so you only run the diff when something moved. The conditional requests guide has the details.

What should your product do with them?

Never render a pulled price as if it could still be backed. Beyond that, the right move depends on the surface.

  1. In an oddsmatcher or ranked list: drop the pair as soon as it leaves the board, and let it reappear when a later publish carries it again. It is temporary, so do not blacklist it.
  2. In a detail or comparison view: keep the row but grey it out with a plain "unavailable" label, so the user sees it exists and cannot act on it.
  3. For void markets: take settlement from the bookmaker's own rules and results. If a user was tracking a selection that was voided, tell them rather than letting it vanish.
  4. Whatever the feed: treat a missing or null price as no price. Never substitute the last value you saw.

A stale price invites a click that ends at a bet that is not there. The display side, including empty states, is in how to display odds data in your app.

Try it on a live board

Fetch the same board twice a few minutes apart and diff it to see prices come and go. Request access for a key, or read the docs for every field on the board.