How odds feeds handle suspended and void markets
Suspended and void markets are different problems. 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, so it is a results question, not 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 has served in-play prices since 2 November 2026, with inPlay on raw and on the standard and dutching boards, and an in-play price is served only while freshly read.
Void markets settle as no bet
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
nullprice. The selection stays in the response with an empty price. You keep the row but must never let thenullreach 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 / 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 and Matchbook. 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_ (also the X-Processed-At header), and meta.last_ gives the last time each venue and sport was read. Now minus last_ 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.
// first publish { "meta": { "feed_type": "standard", "region": "uk", "processed_at": "2026-09-17T14:02:10.114Z", "version": "v2" }, "data": [{ "event_id": "or_evt_917dd44bce05", "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": "smarkets", "price": 3.0, "available": 175, "link": null }] }, { "name": "Chelsea", "back": [{ "bookmaker": "william_hill", "price": 2.5, "link": null }], "lay": [{ "exchange": "smarkets", "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": "smarkets", "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.
Two event statuses do exist. GET / lists a sport's events for discovery, and each carries a status of upcoming or started, which tells you whether the event has kicked off. On the odds routes, an event served with inPlay=, or include on raw, carries in_, true while its venues show it in play. Neither says 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. Storing and versioning odds data covers the store itself.
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 thesePoll 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 your product should do
Never render a pulled price as if it could still be backed. Beyond that, the right move depends on the surface.
- 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.
- 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.
- 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.
- Whatever the feed: treat a missing or
nullprice 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 the live feed
Fetch the same feed twice a few minutes apart and diff it to see prices come and go. The reference has every field in the reply.
Related posts
- The anatomy of an odds API responseEvery field in a /v2 odds response, from the meta envelope down to each back and lay offer.
- How to display odds data in your appTurn a nested odds response into table rows, format prices and show each price's age.
- 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.
18+ · OddsRelay supplies odds data only and takes no bets. Please gamble responsibly.