bet365 odds WebSocket feed
Live bet365 prices pushed to you as JSON when they change, instead of polling. One connection covers every match and sport. Free, no key.
Connect and subscribe
wss://moviodds.com/ws
Once connected, send a subscribe message. Leave out both lists to get every match and market:
{"op": "subscribe"}
Or narrow it down by match ids, market groups, or both:
{"op": "subscribe", "groups": ["1777", "67"]}
{"op": "subscribe", "fixtures": ["202138001"]}
Group 1777 is the in-play soccer Full Time Result and 67 the in-play tennis match winner. The sports page lists the winner groups for every sport.
Messages
First you get a snapshot per match with all its current prices. After that, a delta arrives whenever a price changes, carrying only the changed quotes. A real delta:
{
"type": "delta",
"fixture": "202138001",
"quotes": [{
"fixture_id": "202138001",
"group_id": "1777",
"group_name": "Fulltime Result",
"sel_id": "128575935",
"label": "Draw",
"n2": "X",
"odds_dec": 2.05,
"odds_frac": "21/20",
"ts": "2026-10-05T02:39:10.528360129Z"
}],
"server_time": "2026-10-05T02:39:10.528647673Z"
}
- Identify a quote by
group_id,market_id(use""when missing) andsel_id. - On a
snapshot, replace the match's prices. On adelta, update or add the quotes it carries and keep the rest. "suspended": truemeans the price can't be bet right now. Missing means false.- A
removemessage means stop offering selections that have closed or left the board.{"type":"remove","fixture":"202138001","quotes":[{"group_id":"1777","market_id":"","sel_id":"128575935"}]}drops just those selections;{"type":"remove","fixture":"202138001"}with no quotes means the whole match is gone, so drop it. Just before eachremovewe also send those selections in adeltamarked"suspended": true, so a client that ignores theremovetype still stops treating them as bettable.
Python example
import asyncio, json, websockets
async def main():
async with websockets.connect("wss://moviodds.com/ws", max_size=None) as ws:
await ws.send(json.dumps({"op": "subscribe", "groups": ["1777"]}))
async for raw in ws:
msg = json.loads(raw)
if msg.get("type") in ("snapshot", "delta"):
for q in msg["quotes"]:
print(msg["type"], msg["fixture"], q.get("label"), q["odds_dec"],
"SUSP" if q.get("suspended") else "")
asyncio.run(main())
JavaScript example
const ws = new WebSocket("wss://moviodds.com/ws");
ws.onopen = () => ws.send(JSON.stringify({ op: "subscribe", groups: ["1777"] }));
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data);
if (msg.type === "snapshot" || msg.type === "delta") {
for (const q of msg.quotes) console.log(msg.type, msg.fixture, q.label, q.odds_dec);
} else if (msg.type === "remove") {
// No quotes: the whole match is gone. Otherwise drop just the listed selections.
console.log("remove", msg.fixture, (msg.quotes || []).length || "whole match");
}
};
It runs in the browser too: the moviodds.com live board is built on this socket.
Changing a subscription
subscribereplaces your whole subscription (matches and groups) and sends a freshsnapshotfor the new set. To add a match, send the full list again. Empty or missing lists mean everything.{"op":"unsubscribe","fixtures":["202138001"]}removes those matches from a list you subscribed to.{"op":"unsubscribe"}with no list stops all delivery.- You don't need to unsubscribe finished matches: when a match leaves the feed you get a
removewith no quotes.
Live match stats
When match stats are on, add "stats": true to your subscribe to also get the live stats of in-play matches:
{"op": "subscribe", "stats": true}
{"type": "stats", "fixture": "202138001", "stats": {"sport": "soccer", "home": {"attacks": 41, "possession": 55}, "away": {"attacks": 37, "possession": 45}}, "server_time": "2026-10-17T20:01:02Z"}
- Each match's
statsfollows itssnapshot, then comes on each change, at most one per match per second. Changes inside that second are merged and you get the latest. Each message is the match's complete stats: replace your copy. - Without
"stats": trueyou get nostatsmessages. A later subscribe without it turns them off, as does{"op":"unsubscribe"}. Thegroupsfilter doesn't apply to stats. - Over REST, stats are opt-in too: add
stats=1to/oddsor/catalogfor each in-play match'sstats, and to/resultsfor full-time stats. Without it nostatsfield appears. - Best effort: a dropped
statsmessage may not be re-sent and needs no resync; the next change brings the current values. stats.sportnames the shape: soccer (xg,attacks,dangerous_attacks,possession, shots,corners, cards,offsides,players,timeline), basketball (points, made shots,free_throw_pct,timeouts_remaining,fouls,periods), tennis (aces,double_faults, serve and break-point %,games,point,serving,sets), and where enabled ice hockey, American football, rugby union, baseball, darts, volleyball, e-soccer, e-basketball and Counter-Strike. Every field per sport is in the API docs.
Snapshots are complete
Every subscribe sends one snapshot per subscribed match we currently carry, with every current selection, suspended ones included. Replace the match's prices with it. A selection missing from the snapshot is gone. A match we no longer carry gets no snapshot, so drop it. There is no "snapshot finished" message, so don't wait for one. You don't need a REST reload after reconnecting; the snapshot is enough.
Heartbeats, closes and reconnects
- The server sends a WebSocket protocol ping every 30 seconds. Most clients answer it automatically, but in Node.js and browsers the pong is sent from the event loop, so keep long synchronous work (big gzip or disk writes) off the main thread.
{"op":"ping"}returns{"type":"pong"}. Use it as your own watchdog if you want one. It doesn't affect the server's ping.- Close
1001("server restarting, reconnect"): we restarted. Wait about 3 seconds, then retry at 3, 6, 12 and 24 seconds, then every 30. A restart takes 10 to 30 seconds, and REST may answer 502 meanwhile. - Close
1008("client not reading fast enough"): our ping wasn't answered within 10 seconds, or a message couldn't be delivered for 10 seconds because the socket wasn't being read. Reconnect and subscribe again. {"type":"polled","fixtures":{"202138001":"2026-10-14T21:40:05Z"},"groups":{"202138001":{"1777":"2026-10-14T21:40:05Z"}}}: right after each read (one message every 250 ms at most), the matches you subscribe to that we read again since your last one, with their newpolled_at, and, if you subscribe to specific groups, the ones that read covered. It always comes after the price changes of the reads it confirms. It moves even when no price changes, and stays put when a read fails or comes back empty, so a match that stops showing up here is no longer being refreshed. Eachsnapshotalso carries the match'spolled_at.{"type":"lagged","dropped":N}: your client fell behind, so we dropped your oldest N queued messages instead of disconnecting you. Fresh snapshots of the matches affected follow on their own. Don't subscribe again.{"type":"resync","server_dropped":N}: we lost updates on our side. Fresh snapshots of your matches follow on their own, no need to subscribe again.
Up to 25 connections per IP. Each connection's first 3 subscribe/unsubscribe messages apply at once, then one every 2 seconds; extra ones are merged and the latest wins. Full details are in the API docs.
FAQ
Is the bet365 odds WebSocket free?
Yes. No sign-up and no key. Up to 25 connections per IP.
How fast are the updates?
In-play changes arrive within about a second of bet365 making them. Pre-match changes arrive when we next read the match: every few seconds near kickoff, up to every 30 minutes for matches days away.
Which sports stream live?
All 24 we cover, including soccer, tennis, basketball, American football, ice hockey, baseball, cricket, table tennis and esports.
Do I need to poll as well?
No. Every subscribe sends a complete snapshot of each match you subscribe to, so you can run from the WebSocket alone. /odds is there if you prefer to load the board over REST.
Are these really bet365 odds?
Yes, only bet365's own prices. MoviOdds is an independent project and is not affiliated with bet365.
More: closing lines · historical odds · all 24 sports · Python client · API docs