Documentation Index
Fetch the complete documentation index at: https://docs.oddspapi.io/llms.txt
Use this file to discover all available pages before exploring further.
What “resume” means
After a successful login, the server returns a resume block in login_ok:
{
"resume": {
"serverEpoch": "0804ab61513c4681a3afd8afc1fb2f75",
"resumeWindowMs": 60000,
"replayChannels": ["fixtures", "scores"],
"serverEntryIds": {
"fixtures": "1766414833582-2542",
"scores": "1766418736962-198"
}
}
}
This tells you:
serverEpoch — identifies the current gateway session (changes on restart)
resumeWindowMs — how long replayable data is buffered
replayChannels — channels eligible for replay
serverEntryIds — server’s latest cursor per channel
What clients must persist
To resume safely, persist:
serverEpoch
- The most recent
entryId you processed per channel
{
"scores": "1766418736962-198",
"fixtures": "1766414833582-2542"
}
entryId format
Each streamed message includes an entryId:
ts_ms — server timestamp (UTC, milliseconds)
seq — monotonic per-channel sequence
Important:
entryId is a cursor, not a delivery guarantee
- Gaps are normal (upstream behavior, coalescing, backpressure)
Resume login example
On reconnect, send the same serverEpoch and your stored cursors:
{
"type": "login",
"apiKey": "YOUR_API_KEY",
"channels": ["fixtures", "scores", "odds"],
"serverEpoch": "0804ab61513c4681a3afd8afc1fb2f75",
"lastSeenId": {
"scores": "1766418736962-198"
}
}
If replay succeeds, the server sends:
{
"type": "resume_complete",
"serverEpoch": "0804ab61513c4681a3afd8afc1fb2f75"
}
snapshot_required (when replay is not possible)
Sometimes replay cannot be done safely. In that case the server sends:
{
"type": "snapshot_required",
"reason": "resume_window_exceeded",
"channels": ["scores"],
"serverEpoch": "0804ab61513c4681a3afd8afc1fb2f75",
"resumeWindowMs": 60000,
"serverEntryIds": {
"scores": "1766418738000-220"
}
}
Possible reasons
server_restarted — gateway restarted, cursors invalid
resume_window_exceeded — your cursor is older than the replay buffer
client_backpressure — your client couldn’t consume replay fast enough
Important nuance
Replay eligibility depends on cursor age, not disconnect duration:
(now_ms - last_seen_entry_ts_ms) > resumeWindowMs
⇒ snapshot_required is likely
A very short disconnect can still exceed the window if your last processed message is already old.
How clients should handle snapshot_required
When you receive snapshot_required:
- Fetch a fresh snapshot via REST for the listed channels
(e.g.
/fixtures, /fixtures/odds, /futures)
- Clear
lastSeenId for those channels
- Continue processing live updates
The gateway continues streaming after snapshot_required.
This message is your signal that local state must be rebuilt.
Python reconnect example (FULL TEMPLATE)
This example:
- Persists
serverEpoch and per-channel lastSeenId
- Sends cursors only for replayable channels
- Handles
snapshot_required
- Automatically reconnects on failure
import asyncio
import json
import time
import websockets
WS_URL = "wss://v5.oddspapi.io/ws"
API_KEY = "YOUR_API_KEY"
server_epoch = None
replay_channels = None
last_seen = {}
async def run_once():
global server_epoch, replay_channels, last_seen
async with websockets.connect(
WS_URL,
ping_interval=20,
ping_timeout=20,
max_size=4194304
) as ws:
login = {
"type": "login",
"apiKey": API_KEY,
"channels": ["fixtures", "scores", "odds"],
"receiveType": "json",
}
# Resume mode
if server_epoch:
login["serverEpoch"] = server_epoch
# Only send cursors for replayable channels
if replay_channels:
cursors = {
ch: eid
for ch, eid in last_seen.items()
if ch in replay_channels
}
else:
cursors = dict(last_seen)
if cursors:
login["lastSeenId"] = cursors
await ws.send(json.dumps(login))
async for raw in ws:
if isinstance(raw, (bytes, bytearray)):
raw = raw.decode("utf-8", errors="replace")
msg = json.loads(raw)
msg_type = msg.get("type")
if msg_type == "login_ok":
resume = msg.get("resume") or {}
server_epoch = resume.get("serverEpoch") or server_epoch
rc = resume.get("replayChannels")
if isinstance(rc, list):
replay_channels = set(map(str, rc))
continue
if msg_type == "snapshot_required":
channels = msg.get("channels") or []
for ch in channels:
last_seen.pop(ch, None)
# Trigger REST snapshot refresh here
print("Snapshot required for:", channels)
continue
if msg_type == "resume_complete":
print("Resume complete")
continue
# Data message
channel = msg.get("channel")
entry_id = msg.get("entryId")
if isinstance(channel, str) and isinstance(entry_id, str):
last_seen[channel] = entry_id
async def main():
while True:
try:
await run_once()
except Exception as e:
print("Disconnected:", e)
await asyncio.sleep(1)
asyncio.run(main())