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.
Beta — rolling out per region. zstd is being enabled gradually. If a gateway has it
disabled, your
receiveType: "zstd" connection is gracefully downgraded to JSON — the
server tells you the negotiated mode in login_ok.receiveType. Always trust that echo to
choose your decoder; never assume the mode you requested.Why zstd
odds is the highest-volume channel. zstd with a trained dictionary shrinks the data you receive
~7× (other channels similarly) — less bandwidth on the wire, so high-volume subscriptions are
easier to keep up with and you’re less likely to hit backpressure disconnects.
The format is uniform and self-describing:
- Every data frame on a
zstdconnection is a binary zstd frame — on every channel. - Each frame embeds the id of the dictionary it was compressed with (
dictId). Your client reads that id, picks the matching dictionary (or none), and decompresses. - No per-channel branching, ever. When we add a dictionary to a new channel later, your code does not change.
json (everything is text) and binary (everything is MessagePack) already work:
one format for the whole connection.
Enabling it (login)
SetreceiveType: "zstd". Optionally tell the server which dictionaries you already cached from a
previous connection so it can skip re-sending them.
| Field | Type | Description |
|---|---|---|
receiveType | string | "json" (default) | "binary" (MessagePack) | "zstd" (compressed JSON). Mutually exclusive whole-connection modes. |
dicts | object | Optional { "<channel>": "<dictVersion>" } of dictionaries you already hold. The server skips re-sending those. Omit on a first connection. |
There is intentionally no compressed-MessagePack mode — compressing JSON beats compressing MessagePack at every level.
What you get back (login_ok)
login_ok.receiveType is the negotiated mode. If it comes back "json", the gateway has zstd
disabled and you must decode plain JSON text frames — do not try to decompress.
Dictionary delivery (server → client)
Right afterlogin_ok (and before any data frame), for each subscribed channel that has a trained
dictionary you did not already list in dicts, the server pushes one control frame:
- Base64-decode
datato get the raw dictionary bytes. - Store it keyed by
dictId(build a reusable zstd decoder from it). - Keep
channel→dictVersionin your own cache so you can send thedictsmap on the next connection and skip the re-download.
Decoding is driven by the
dictId embedded in each data frame, not by channel. The
channel/dictVersion fields on the dict frame are only for cache bookkeeping.scores, clocks) send no dict frame — their
data frames carry dictId = 0 and decode without a dictionary.
Data frames
- Every data frame is a WebSocket Binary frame containing one standalone zstd frame
(magic
28 B5 2F FD, with an embeddeddictId). - Read
dictIdfrom the frame header, look it up in your store:- found → decompress with that dictionary,
0or unknown → decompress dictless.
- Decompress, then
JSON.parse, then route by.channel— exactly as in JSON mode.
login_ok, dict, error, snapshot_required, resume_complete)
remain JSON text frames, even on a zstd connection. The rule is fixed:
Text frame → control (JSON). Binary frame → data (zstd).
Decode (pseudocode)
🐍 Python example
Requirespip install zstandard.
Caching dictionaries across reconnects
Dictionaries are ~32 KB each and rarely change. To avoid re-downloading them every reconnect:- Persist the
channel → dictVersionmap you received viadictframes. - On the next
login, send it asdicts. - The server only re-sends a dictionary whose version you do not list.
dictId (not channel) — a dictionary rotation ships a new
dictId, and a stale decoder simply won’t match (it errors rather than mis-decodes), which is the
safety net.
See also
- Auth & Filters — full
loginfield reference - Resume & Replay — reconnecting and recovering missed data
- Troubleshooting — zstd decoding issues