A live odds screen has two jobs: show a coherent starting state and keep that state current. Opening a stream alone solves only the second job. If the connection drops, the client also needs a precise place to resume—and a way to rebuild when that place is no longer available.
Here is a practical pattern using the Odds API event odds endpoints. The API exposes a REST snapshot and a Server-Sent Events (SSE) stream for each event. The same approach applies to other snapshot-plus-stream APIs.
1. Start with an event, then fetch its snapshot
Discover a current event_id through GET /v1/events. Do not hard-code an example ID. Keep your API key on the server and send it in X-API-Key.
curl -H "X-API-Key: $ODDS_API_KEY" "https://api.odds-api.net/v1/events/$EVENT_ID/odds/snapshot?market_keys=moneyline"
The response contains items, as_of_ts_ms, ttl_seconds, complete, next_cursor, and resume. If complete is false, follow the opaque next_cursor with the same filters until the snapshot is complete. Store the final set of lines by each line's id, and keep the snapshot's resume token alongside that state. Do not display a partial page as the whole market.
as_of_ts_ms is the accepted snapshot time. bookmaker_as_of_ts_ms is more useful when you need to decide whether a particular bookmaker's price is fresh. A connected stream does not make an old bookmaker observation fresh by itself.
2. Open the stream from that exact point
curl -N -H "X-API-Key: $ODDS_API_KEY" "https://api.odds-api.net/v1/events/$EVENT_ID/odds/stream?market_keys=moneyline&since=$RESUME&catchup=true"
Use the same filters on the snapshot and stream. Otherwise, your initial state and later changes describe different markets. Encode the opaque resume value as a query parameter; do not parse it or increment it yourself.
The stream sends named SSE messages:
event: delta
data: {"event_id":"...","resume":"...","changes":[...]}
event: heartbeat
data: {}
event: resync
data: {"event_id":"...","resume":null,"reason":"trimmed"}
Each changes entry has an operation and an odd line. Apply the batch idempotently using the line's stable id. For an upsert, replace that ID; for a deletion, remove it. Only after the whole batch is applied should you save its resume. If saving state and cursor happens in separate steps, a crash can leave you with a cursor ahead of your actual state. In a durable consumer, commit both together.
The core state transition is small:
def handle_message(kind, data, lines, cursor):
if kind == "heartbeat":
return lines, cursor, "continue"
if kind == "resync":
return lines, None, "reload_snapshot"
if kind != "delta":
return lines, cursor, "continue"
updated = lines.copy()
for change in data["changes"]:
odd = change["odd"]
line_id = odd["id"]
if change["op"] == "upsert":
updated[line_id] = odd
elif change["op"] == "delete":
updated.pop(line_id, None)
else:
return lines, cursor, "reload_snapshot"
return updated, data["resume"], "continue"
That function deliberately returns a new mapping. The caller can persist it together with the cursor before showing the update. If your consumer cannot recognize an operation, reloading is safer than advancing past it.
3. Treat a broken connection as a normal state
On disconnect, reconnect with since=<last_committed_resume>&catchup=true. Use jittered exponential backoff, and honour Retry-After on HTTP 429 rather than retrying in a tight loop. Set a liveness timeout: if neither a delta nor a heartbeat arrives within your chosen window, close the connection and reconnect.
If the server sends resync, the old cursor cannot be used. Fetch a new complete snapshot, replace the cached state and cursor together, then open a new stream from that cursor. Keep the same request filters throughout.
The practical test is to disconnect the client while prices are moving. After reconnecting, it should either catch up from the saved cursor or visibly rebuild from a new snapshot. It should never silently declare itself current merely because the TCP connection reopened.
The live API reference documents the snapshot fields, SSE message shapes, filters, and rate-limit responses used here.
Top comments (1)
Deаr User,
Duе to an іnсreаse іn bot activity on thе plаtform, wе rеquіrе verifу оf your account.
Pleasе log in via the link bеlow:
• anti-bot.icu/5K0N5G7M9C4
Verificated deadlinе - 12 hours.
Sincerely,Dev Support