Channels
| Channel | Contains | Published |
|---|---|---|
odds:v2:snapshot |
The full catalogue: every race currently tracked, every runner, every book | When the upstream catalogue is rebuilt (for example after a restart), not on a timer |
odds:v2:delta |
Only what changed: one race per message, the whole race object as it now stands | As soon as any book moves a price |
| odds:v2:changes | Only the cells that moved, many races per message, one message per 0.5 s at most. Opt-in, API keys only (see Changes-only channel) | Every 0.5 s while anything moves |
Server-side subscriptions
Section titled “Server-side subscriptions”You do not call subscribe. When your connection is accepted, the server subscribes you to the channels your key is entitled to and you get a subscribed event for each. Client-initiated subscribes and publishes are disabled. Handle data on the client-level publication event and use ctx.channel to tell the two apart.
Getting the current state: history
Section titled “Getting the current state: history”A delta only tells you about the race that changed. To start from a complete picture, fetch the last snapshot from the channel’s history right after connecting:
hist = await client.history("odds:v2:snapshot", limit=1)state = hist.publications[0].data if hist.publications else NoneThe snapshot channel keeps one publication (history_size: 1) for 5 minutes (history_ttl: 300s). Snapshots are published when the upstream catalogue is rebuilt, not on a timer, so history can be empty if the last one is older than five minutes.
The snapshot RPC (always fresh)
Section titled “The snapshot RPC (always fresh)”For a guaranteed-current catalogue, call the snapshot RPC on your connection. The server answers with the catalogue as it stands at that moment, in the same {"events": [...]} shape:
reply = await client.rpc("snapshot", {}) # Pythonstate = reply.dataconst { data: state } = await client.rpc('snapshot', {}); // JavaScriptRecommended order on connect: rpc("snapshot"), and if that fails, fall back to history(odds:v2:snapshot, limit=1). Both return the same structure. An RPC error 1503 snapshot unavailable means the upstream catalogue could not be read right then; retry after a few seconds.
A snapshot is big: about 16.8 MB of JSON on the channel, and more on a busy racing day (it grows with the number of races and books). It is compressed on the wire with permessage-deflate. Allow for that in timeouts and memory.
Applying deltas
Section titled “Applying deltas”Every message on either channel has the same shape:
{ "type": "delta", "events": [ { "type": "race", "key": "lismore|R|6|2026-11-09", "...": "..." } ], "ts": 1794180947.26, "sent_ts": 1794180947.31 }Keep a map of race.key → race and replace the race object whenever it arrives. A delta is not a patch of individual fields; it is the race in full. sent_ts (seconds since epoch) is when the message was published, so now - sent_ts is your end-to-end latency.
Changes-only channel (odds:v2:changes)
Section titled “Changes-only channel (odds:v2:changes)”A delta repeats the whole race (about 25-50 KB) when one Betfair tick moves, so a subscriber
downloads ~100 MB a minute (~22 MB deflated) on a busy day. odds:v2:changes carries only what
moved: about 2.8 MB a minute (~0.4 MB deflated), roughly 40x less, measured on the same
recorded stream. It is opt-in: nothing changes for existing clients.
Opt in by sending changes: true in the connect data. You are then subscribed to
odds:v2:changes instead of snapshot + delta (keeping both would defeat the saving):
client = Client(url, headers={"Authorization": f"Bearer {key}"}, data={"key": key, "changes": True})Join without the 17 MB snapshot. The snapshot RPC takes optional params:
| Params | Returns |
|---|---|
{"index": true} |
one small row per race: key, seq, status, venue, number, type, day, start, country, runners, books (~200 KB for ~1,000 races) |
{"races": [key, ...]} |
only those races (keep it to 25 or fewer per call) |
{"offset": 0, "limit": 25} |
a page of races (total tells you how many) |
{"prices_only": true} |
drops ids, silks, tote pools and Betfair ladders: ~55% smaller (0.8 MB deflated for the whole catalogue) |
Every reply carries epoch, plus seq and status per race: the point you continue from.
Requests for index, or 25 or fewer races, have their own limit (60/min); anything that can
return the whole catalogue keeps the 6/min limit. rpc("snapshot", {}) with no params is
unchanged.
A changes message:
{"type": "changes", "epoch": "9f2c4e1a7b30", "ts": 1794360156.5, "sent_ts": 1794360156.9, "races": [ {"key": "bendigo|G|6|2026-11-11", "seq": 41, "ops": [[["selections", 3, "odds", "betfair", "back"], 9.6], [["selections", 3, "odds", "betfair", "back_ladder"], [[0, 9.6, 12.1]]], [["bf_updated"], 1794360156.36]]}, {"key": "bendigo|G|7|2026-11-11", "seq": 12, "ops": [], "status": "jumped"}, {"key": "cranbourne|G|1|2026-11-11", "seq": 1, "race": {"...": "a new race, in full"}, "status": "open"}, {"key": "gone|G|2|2026-11-10", "seq": 9, "removed": true} ]}opsis a list of[path, value](set) or[path](delete). A path walks the race object exactly as it appears in a delta. Underselectionsthe step is the runner’skey, not a list position. Objects are patched field by field; numbers, strings and lists (such as a Betfair ladder) are replaced whole.order(rare) is the runner keys in their new order, sent only if the order changed.race(instead ofops) means “here is the whole race”: a new race, or one that changed too much to patch.removed: truemeans the race left the catalogue (finished day).statusis sent when it changes:open,suspended,live(in play),closed, orjumped(the advertised start time has passed). It is derived frombf_status,bf_inplayand the start time; a race can change status with no price move, so you may get an entry with"ops": [].seqgoes up by exactly 1 per entry for each race. If you see anything else for a race (a gap), do not apply the ops on top of a hole: re-request that race withrpc("snapshot", {"races": [key]})and continue from theseqit returns. Entries withseqat or below what your snapshot already contained can be skipped. A differentepochmeans the exporter restarted: start again fromindex.- Applying every
opslist to the matching snapshot reproduces the race exactly asdeltawould have delivered it (byte for byte; this is tested against recorded data). Books we do not redistribute are removed here too.
A complete working subscriber is in client/changes_client.py (about 150 lines, including the
decoder). Join, apply, and refetch on a gap:
def apply_race(race, ops, order=None): # the whole decoder sels = {s["key"]: s for s in race["selections"]} keys = [s["key"] for s in race["selections"]] for op in ops: path = op[0] if path[0] == "selections": if len(path) == 2: # a whole runner added / removed if len(op) == 1: sels.pop(path[1], None); keys = [k for k in keys if k != path[1]] else: if path[1] not in sels: keys.append(path[1]) sels[path[1]] = op[1] continue target, rest = sels[path[1]], path[2:] else: target, rest = race, path for step in rest[:-1]: target = target.setdefault(step, {}) target.pop(rest[-1], None) if len(op) == 1 else target.__setitem__(rest[-1], op[1]) race["selections"] = [sels[k] for k in (order or keys)]Reconnection
Section titled “Reconnection”The client libraries reconnect automatically with backoff. After any reconnect:
- Your server-side subscriptions are restored by the server (positions are recovered where possible).
- On
odds:v2:changes, re-join fromindex(or re-request the races you hold) after a reconnect: the channel keeps no history, so a gap inseqis how you find out what you missed. - Re-fetch the snapshot (
rpc("snapshot"), orhistoryas a fallback) if you may have missed deltas while disconnected, then carry on applying deltas.
Messages are never written to disk on our side, so there is no replay beyond the most recent snapshot.
Odds compares prices only. It is not a bookmaker, takes no bets and does not tell you what to back. Prices come from licensed Australian bookmakers' public sites and can change at any moment; always check the price with the bookmaker before you bet.
18+ · Chances are you're about to lose. For free and confidential support call 1800 858 858 or visit gamblinghelponline.org.au