# betfair-odds.com — Betfair Odds API, Full Reference > Real-time Betfair exchange odds — back and lay prices with the sizes behind them — for live and > prematch markets over REST and WebSocket. Independent service; not affiliated with Betfair or > Flutter Entertainment. Paid API service. Sign up at https://betfair-odds.com/ — your API key is your login. One API key authenticates both REST and WebSocket. ## Authentication Base URL: `https://betfair-odds.com/betfair/v1/`. Auth on every endpoint: `x-api-key: YOUR_KEY` header, `x-portal-apikey: YOUR_KEY`, or `?key=YOUR_KEY`. ### Tiers | Plan | Price | REST rate | WebSocket | |---|---|---|---| | Betfair Pro | $99/30d, $249/90d | 10 req/sec | — | | Betfair Pro + WS | $149/30d, $379/90d | 10 req/sec | included | | Betfair Scale | $229/30d, $599/90d | 30 req/sec | included | Every plan allows **one** concurrent WebSocket connection. No 180-day SKUs. Live prices: `GET /panel/api/betfair/plans`. **WebSocket is an entitlement, not a tier capability.** `ws_live` and `ws_prematch` are absent from every tier's caps; they come from `users.betfair_ws_until`. Pro + WS and Scale include that entitlement in the price; a plain Pro customer can add it later for $99/mo. Buying the add-on on a plan that already bundles it is never charged twice. Demo access is granted **manually by us** — it is not self-serve. Ask on Telegram (https://t.me/ArbitrageXpro) with the email on your account and we'll enable it. ### Feeds `?feed=` selects the data set and accepts two values: | Feed | Contents | Capability | |---|---|---| | `live` | our normalised in-play livemap | `rest_live` | | `prematch` | our normalised pre-game livemap | `rest_prematch` | **`feed` is optional and defaults to `live`.** Omitting it is not an error — you silently get the live feed. Only a value that is neither of the two returns 400 `bad_feed`. Matching is case-insensitive. Always send `feed` explicitly; a variable that evaluates to empty string will quietly serve live data. ### GET /betfair/v1/livemap Full current snapshot. Body is `{ meta, liveMap }`. | Parameter | Required | Notes | |---|---|---| | `feed` | no (defaults `live`) | `live` or `prematch` | | `sports` | no | comma-separated sport **names**, case-insensitive, matched on each event's `sport` field. NOT numeric sport ids. Carried sports: `soccer`, `tennis`, `basketball`, `cricket`, `ice hockey`, `american football`, `baseball` — URL-encode the two-word names (`sports=ice%20hockey`). | ```bash curl -H "x-portal-apikey: YOUR_KEY" \ "https://betfair-odds.com/betfair/v1/livemap?feed=live&sports=soccer,tennis" ``` Response headers `X-Betfair-Seq` and `X-Betfair-Stale` are sent **on this endpoint only** — no other route emits them. Read `meta.stale` everywhere else. gzip is pre-computed and served only when `Accept-Encoding` includes gzip **and** no `sports` filter is set (a filtered response is built per-request and cannot use the cached compressed copy). Returns 503 `no_data_yet` before the first ingest lands. ### GET /betfair/v1/events A cheap index — poll this instead of pulling the whole snapshot. Body is `{ meta, events }` (an **object**, not a bare array). Each row is exactly: ```json { "eventId": "...", "team1": "...", "team2": "...", "sport": "...", "tournament": "...", "openTimestamp": 0, "marketCount": 0 } ``` Takes the same `feed` and `sports` parameters as `/livemap`. ### GET /betfair/v1/events/{eventId} One event, byte-identical to what we ingested. Body is `{ meta, event }`. Unknown id returns 404 `unknown_event`. Event ids are scoped per feed, so always pass `?feed=` explicitly. It is not enforced: omitting it defaults to `live`, so a prematch event id comes back as 404 `unknown_event` rather than telling you the feed was wrong. ### GET /betfair/v1/me What this key is entitled to. ```json { "tier": "scale", "active": true, "expires_at": 1789000000000, "ws_active": true, "ws_until": 1789000000000, "is_admin": false, "capabilities": ["rest_live", "rest_prematch", "ws_live", "ws_prematch"], "limits": { "perSec": 30 }, "max_connections": 1 } ``` Authenticated but **not** plan-gated: it returns 200 even when the subscription has lapsed (`active: false`, `tier: null`, `expires_at` in the past). It is the correct call after any 403, since every other endpoint refuses a lapsed key. ### GET /betfair/v1/health Unauthenticated ops probe. Also answers at `/ping`. ```json { "ok": true, "service": "betfair", "feeds": { "live": { "...meta..." }, "prematch": { } }, "senders": { }, "clients": 0, "ingest": { }, "ws": { } } ``` **Always HTTP 200, even when a feed is stale** — `ok` flips to `false` and `feeds..stale` says which. Never use the status code as a freshness check. `GET /betfair/v1/status` returns a human-readable HTML ops page. ### The `meta` object Present on every REST response and every WebSocket data frame. ```json { "feed": "live", "receivedAt": "2026-07-28T12:00:00.000Z", "seq": 1234, "stale": false, "age_ms": 850, "event_count": 512 } ``` ### Staleness Each feed has its own threshold: | Feed | Stale after | Env override | |---|---|---| | `live` | 15 s | `BETFAIR_STALE_LIVE_MS` | | `prematch` | 120 s | `BETFAIR_STALE_PREMATCH_MS` | The flag appears in every REST response's `meta`, in `/betfair/v1/health`, and as a WebSocket `stale` frame. The `X-Betfair-Stale` header is sent **only** by `/betfair/v1/livemap`. Transitions are detected by a 5-second polling loop, so the `stale` frame can lag the threshold by up to 5 s. Treat `meta.age_ms` as the precise measure. ### WS /betfair/v1/stream `wss://betfair-odds.com/betfair/v1/stream`. Requires the WebSocket add-on. #### 1. Authenticate — within 10 seconds ```json { "action": "authenticate", "apiKey": "YOUR_KEY" } ``` Reply: ```json { "type": "authenticated", "tier": "scale", "is_admin": false, "ws_addon": true, "capabilities": ["rest_live","rest_prematch","raw","ws_live","ws_prematch"] } ``` Miss the 10-second deadline and you get `{"type":"error","code":"auth_timeout"}` followed by close 1008. **Authenticating proves only that the key is valid — not that you may read anything.** Any valid betfair-odds.com key authenticates successfully; `tier` is `null` and `capabilities` is `[]` when there is no active subscription. Authorization happens at subscribe time. #### 2. Subscribe ```json { "action": "subscribe", "feeds": ["live"], "sports": ["soccer"] } ``` The key is `action` (not `type`) and `feeds` (not `streams`). Omitting `feeds` defaults to `["live"]`. Acknowledgement, then the snapshot: ```json { "type": "subscribed", "feed": "live", "sports": [] } ``` A feed you are not entitled to does **not** close the socket — you get `{"type":"error","code":"...","message":"...","feed":"...","required_capability":"..."}` and the other feeds in the same request still subscribe. Unsubscribe with `{"action":"unsubscribe","feeds":["live"]}` → `{"type":"unsubscribed","feeds":[...]}`. #### 3. Data frames ```json { "type": "snapshot", "feed": "live", "meta": {...}, "liveMap": {...} } { "type": "update", "feed": "live", "meta": {...}, "changed": {...}, "removed": [...] } { "type": "stale", "feed": "live", "stale": true } ``` The snapshot payload lives under **`liveMap`**. **Snapshots are chunked at 512 KB.** When the payload exceeds one chunk the server sends *multiple* frames all typed `snapshot`, each carrying `seq` and `final`. When it fits in one chunk there is **no `seq` and no `final` at all** — so a client that waits unconditionally for `final: true` hangs forever on small feeds. Treat "no `final` field" as a complete snapshot. #### 4. Heartbeat The server sends `{"type":"ping","ts":...,"buffered_max_bytes":...}` every 30 s. Reply: ```json { "action": "pong" } ``` Note `action`, **not** `{"type":"pong"}` — a type-keyed pong is ignored here. No pong for 75 s closes the socket with 1001 `stale`. A protocol-level WebSocket ping is also sent, so libraries that auto-pong at the protocol layer keep the connection alive regardless. #### 5. Connection cap Per API key: **1 concurrent connection on every plan.** Exceeding it rejects the **new** socket with `{"type":"error","code":"too_many_connections"}` and close 1008; existing connections are untouched. The new socket is the one rejected (not the oldest evicted), so a reconnect on a flaky link can never kill your own healthy feed — but it also means a half-dead socket holds its slot until the 75 s heartbeat reaps it. #### 6. Close codes | Code | Reason | Meaning | |---|---|---| | 1008 | `no authenticate` | 10-second auth deadline missed | | 1008 | `too_many_connections` | tier connection cap reached | | 1008 | auth error code | `missing_key` / `invalid_key` | | 1001 | `stale` | no pong for 75 s | | 1001 | `server shutdown` | deploy or restart — reconnect | | 1011 | `deregistered: ` | dropped for backpressure. Causes: `slow_consumer`, `send_err`, `send_threw`, `not_open`, `socket_error` | Backpressure eviction: a sustained backlog above 1 MB for 60 s, or above 128 MB instantly. The `buffered_max_bytes` field on each ping tells you how close you are. Compression: permessage-deflate is always enabled server-side above 1 KB. There is no client-side tradeoff to configure. ### Errors REST bodies use **`error`**; WebSocket error frames use **`code`**. A client that reads the wrong field sees `undefined` on one of the two transports. ```json REST : { "error": "no_betfair_plan", "message": "...", "required_capability": "...", "current_tier": null } WS : { "type": "error", "code": "unknown_feed", "message": "..." } ``` | Status | Error | When | |---|---|---| | 400 | `bad_feed` | `?feed=` set to something other than `live` or `prematch`. A *missing* feed is not an error. | | 401 | `missing_key` | no key supplied | | 401 | `invalid_key` | key not recognised | | 403 | `no_betfair_plan` | valid key, no active Betfair subscription | | 403 | `tier_lacks_capability` | your tier does not include this capability | | 403 | `ws_addon_inactive` | tier active, WebSocket add-on is not | | 404 | `unknown_event` | no such event on that feed | | 429 | `rate_limited` | body `{error, limit, retry_after_ms}` plus a `Retry-After` header. | | 503 | `no_data_yet` | `/livemap` before the first ingest. Body carries `meta`, no `message`. | | 503 | `auth_unavailable` | entitlement store temporarily unreadable | Every 403 body also carries `required_capability` and `current_tier`. Call `/betfair/v1/me` to see what you actually hold. WebSocket-only codes, none of which close the socket — handle an error frame on a still-open connection: `invalid_json`, `invalid_message`, `not_authenticated`, `unknown_action`, `unknown_feed` (the WS equivalent of REST's `bad_feed`). ## Affiliate Program Every account ships with a unique referral link visible in the dashboard. Anyone who lands on betfair-odds.com with `?ref=`, signs up, verifies email in the same session, and pays for a plan earns the referrer **30% of every payment** they make — lifetime, including renewals and upgrades. The referee pays the normal price; there is no signup discount on their side. | Topic | Detail | |---|---| | Link format | `https://betfair-odds.com/?ref=<8-char-code>` | | Commission | 30% of every USD payment from referred users, recurring for the lifetime of the subscription | | Attribution | Session-based: `?ref=` must be present when the visitor lands on betfair-odds.com AND they must complete signup + email verification before clearing sessionStorage | | Tracking | `GET /panel/api/me` returns a `referral` object containing the code, link, masked referral list, and commission stats (paying / unpaid / paid_out) | | Self-referral | Blocked at the verify-confirm step | | Payouts | Manual. We contact the referrer when unpaid balance reaches $50+. Crypto / bank / PayPal — referrer's choice | | Privacy | Referee emails are masked in the panel (`j***@gmail.com`); admins see unmasked | | Refunds | A refunded payment voids its commission. We don't claw back paid-out commissions for routine churn | --- ## Support - Email: info@betfair-odds.com - Technical questions / integration help: Telegram @ArbitrageXpro - API status: https://betfair-odds.com/betfair/v1/health (public, always HTTP 200 — read `ok` and `feeds..stale`)