Most teams new to building betting products treat live in-play odds as "pre-game odds plus a clock." They wire up ?live=true on their odds endpoint, expect roughly the same data shape, and assume the same integration patterns work. They do, until they don't, and by then there's a partially-built product on top of a wrong mental model.
In-play markets are structurally different from pre-game markets in five ways that materially change how you build against them. This post covers each.
| Pre-game | In-play | |
|---|---|---|
| Update cadence | Typically 5-60s between line moves on flagship markets, longer on niche. | 200ms-5s between line moves during active play; sub-100ms during high-leverage moments (penalty, late-game possession). |
| Market suspensions | Rare; markets stay open from posting through commence_time. | Constant. Books "freeze" markets during plays, between possessions, on injuries, and on review. A market can be suspended 30-60% of the time during an NFL game. |
| Partial-market state | All markets for an event move together (h2h, spreads, totals all suspended or all live). | Individual markets transition independently. h2h might be live while alt-spread is frozen, or vice versa. |
| Latency to bet acceptance | Tens of seconds to minutes; books accept at displayed price. | Sub-second matters. By the time your bet hits the book, the line has often moved or the market has frozen, and the book asks you to confirm at the new price. |
| API shape | Stable per-event response with full market list. | Many books expose in-play through different endpoints (/v1/inplay/*) with diff-update WebSocket pushes instead of full snapshots. |
Pre-game lines on the NBA might move 5 to 30 times between posting and tip-off. In-play, that same game gets 1,500 to 4,000 line moves over 48 minutes of play. If your scanner pulls a snapshot every 10 seconds, you're missing 98% of the in-play price discovery.
This is why pre-game polling architectures translate poorly to in-play: the polling interval is roughly the same order of magnitude as the data's coherence window. You can't just "poll faster." Books rate-limit you, your bandwidth bill explodes, and you still miss most updates because they happen between your polls.
/v1/ws ships per-market diffs at sub-second cadence on the scale tier and above. SSE alternative at /v1/sse/odds. See /docs/websocket for the subscribe / unsubscribe shape.
When a book's risk engine sees something it can't immediately model (an injury, a stoppage in play, a goal under review), it suspends the market. Customers see "this market is currently unavailable" on the book's website. The price is gone, not zeroed.
For an API consumer, this means your in-play feed has three states per market, not two:
If your code conflates "suspended" with "no value," you'll silently filter out half your potential opportunities. If your code conflates "suspended" with "active at last-known price," you'll try to place bets at prices the book isn't currently offering.
# Bad: assumes any non-null price means active
def is_bettable(market):
return market.get("price") is not None
# Better: respect explicit status flag
def is_bettable(market):
return market.get("status") == "active" and market.get("price") is not None
ParlayAPI's in-play endpoints carry an explicit status field per market: active, suspended, closed, or settled. Trust it; don't infer from price presence.
Books transition markets independently. On an NBA game in the 4th quarter:
If your scanner pulls a full event snapshot and treats "no spread price" as "spread market is dead," you'll wait minutes for a snapshot that includes both h2h and spread together. That snapshot may never come on a fast-paced game.
This is the killer. In-play markets move so fast that the price you scanned 800ms ago is often not the price you can actually bet at. Books model this and apply an extra "in-play review" step on bet submission:
End result: your "+EV bet at -110" might land at -115, which is no-EV or slightly -EV. Or it doesn't land at all.
Mitigation:
Many sportsbook APIs expose in-play through dedicated endpoints with different response shapes:
If you're consuming through ParlayAPI:
# Pre-game (full snapshot)
GET /v1/sports/basketball_nba/odds?regions=us&markets=h2h,spreads,totals
# In-play (live filter on same endpoint)
GET /v1/sports/basketball_nba/odds?regions=us&markets=h2h&live=true
# In-play dedicated (different shape: diffs vs snapshots)
GET /v1/inplay/odds?sport=basketball_nba
GET /v1/inplay/arbs?sport=basketball_nba
# In-play WebSocket (preferred for production)
wss://parlay-api.com/v1/ws?key=$PARLAY_API_KEY&sport=basketball_nba&market=h2h&mode=live
If you're integrating in-play for the first time, run this checklist before going live:
status field. Active ≠ suspended ≠ closed ≠ settled. Code accordingly.Free no-auth previews:
/v1/try/{sport}/odds: pre-game snapshot, top 5 events./v1/inplay/odds?sport=baseball_mlb: in-play snapshot (auth required for full feed)./v1/inplay/arbs?sport=baseball_mlb: live in-play arbitrages.WebSocket reference: examples/ws_reference_client.py. The reference client subscribes per-market, respects status transitions, and times every observed price for staleness filtering. ~250 LOC, single file, no deps beyond websockets.
Sign up free for an API key at /signup if you want to go past the no-auth try endpoints.