MoviOdds Docs
API docs Markets Bet Builder moviodds.com

bet365 odds in Python

Three short scripts: live match prices, every market for one match, and a live stream of changes.

Everything here uses the free MoviOdds API. The odds are exclusively bet365's, there's no sign-up and no key, and each script below was run against the live API before it went on this page.

Setup

pip install requests websockets

Python 3.9 or newer. The base URL for everything is https://moviodds.com.

1. Live match prices

Ask /odds for in-play matches and just the Full Time Result market. That keeps the response to a few kilobytes instead of the whole board.

import requests

# Full Time Result for every match in play (group 40 pre-match, 1777 in play).
r = requests.get(
    "https://moviodds.com/odds",
    params={"in_play": "1", "groups": "40,1777"},
    timeout=30,
)
r.raise_for_status()

for m in r.json()["matches"]:
    prices = {q["label"]: q["odds_dec"] for q in m["quotes"] if not q.get("suspended")}
    if prices:
        fmt = lambda x: f"{x:.2f}" if x else "-"
        print(f'{m["league"]}: {m["home"]} {fmt(prices.get(m["home"]))} | '
              f'Draw {fmt(prices.get("Draw"))} | {m["away"]} {fmt(prices.get(m["away"]))}')

Output looks like:

UEFA Nations League D: Azerbaijan 1.57 | Draw 2.60 | Liechtenstein 15.00
Ghana Premier League: Asante Kotoko 1.12 | Draw 6.50 | Karela United FC 17.00

Full Time Result is market 40 before kickoff and 1777 once a match is in play, so ask for both. Every market's id is stable across matches; see the market list.

2. Every market for a match

Find a match in /catalog, then get all of its prices from /snapshot. Big matches often have 40 or more markets.

from collections import defaultdict
from datetime import datetime, timezone

import requests

BASE = "https://moviodds.com"

# Pick the next match that hasn't kicked off yet.
catalog = requests.get(f"{BASE}/catalog", timeout=30).json()["fixtures"]
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
upcoming = sorted((f for f in catalog if not f["in_play"] and f["kickoff_utc"] > now), key=lambda f: f["kickoff_utc"])
match = upcoming[0]

# Every market bet365 prices for it.
snap = requests.get(f"{BASE}/snapshot", params={"fi": match["id"]}, timeout=30).json()
markets = defaultdict(list)
for q in snap["quotes"] or []:
    markets[q["group_name"]].append(q)

print(f'{match["home"]} v {match["away"]} ({match["league"]}): {len(markets)} markets')
for name, quotes in sorted(markets.items()):
    first = quotes[0]
    pick = " ".join(dict.fromkeys(x for x in (first.get("label"), first.get("side"), first.get("line")) if x))
    print(f"  {name}: {len(quotes)} prices, e.g. {pick} @ {first['odds_dec']:.2f}")

Output looks like:

UD Santa Marta v Salamanca CF UDS (Spain Copa Federacion): 11 markets
  Correct Score: 24 prices, e.g. 1-0 UD Santa Marta @ 11.00
  Double Chance: 3 prices, e.g. Draw or Salamanca CF UDS @ 1.22
  Goals Over/Under: 2 prices, e.g. Over 2.5 @ 2.15

3. Stream price changes

Load the board once with /odds, then keep it current over one WebSocket. You get a snapshot per match, then a delta each time prices move. In-play prices arrive the moment bet365 changes them.

import asyncio
import json

import requests
import websockets  # pip install websockets

BASE = "https://moviodds.com"
board = {}  # match id -> {"match": {...}, "quotes": {(group_id, market_id, sel_id): quote}}


def key(q):
    return (q["group_id"], q.get("market_id", ""), q["sel_id"])


def load_board():
    data = requests.get(f"{BASE}/odds", timeout=60).json()
    for m in data["matches"]:
        board[m["id"]] = {"match": m, "quotes": {key(q): q for q in m["quotes"]}}


def apply(msg):
    entry = board.setdefault(msg["fixture"], {"match": None, "quotes": {}})
    if msg["type"] == "snapshot":
        entry["quotes"] = {}  # a snapshot replaces the match's odds
    for q in msg["quotes"]:
        entry["quotes"][key(q)] = q  # a delta only carries what changed
    return entry


async def stream():
    delay = 1
    while True:
        try:
            load_board()
            async with websockets.connect(BASE.replace("https", "wss") + "/ws", max_size=None) as ws:
                await ws.send(json.dumps({"op": "subscribe"}))
                delay = 1
                async for raw in ws:
                    msg = json.loads(raw)
                    if msg.get("type") == "lagged":
                        break  # we fell behind: reload the board and reconnect
                    if msg.get("type") not in ("snapshot", "delta"):
                        continue
                    entry = apply(msg)
                    m = entry["match"]
                    if msg["type"] == "delta" and m:
                        for q in msg["quotes"]:
                            pick = " ".join(dict.fromkeys(x for x in (q.get("label"), q.get("side"), q.get("line")) if x))
                            price = "suspended" if q.get("suspended") else f'{q["odds_dec"]:.2f}'
                            print(f'{m["home"]} v {m["away"]} | {q.get("group_name")} | {pick} -> {price}')
        except (websockets.ConnectionClosed, OSError, requests.RequestException) as e:
            print("connection lost, retrying:", e)
        await asyncio.sleep(delay)
        delay = min(delay * 2, 30)


asyncio.run(stream())

Output looks like:

Hungary U21 v Lithuania U21 | 1st Half Goal Line (0-1) | Under 1.5 -> 2.05
Liverpool (llulle) v Real Madrid (Cira) | Asian Handicap (0-0) | Liverpool (llulle) -1 -> 1.80

To follow only some markets or matches, subscribe with lists: {"op": "subscribe", "groups": ["40", "1777"]} or {"op": "subscribe", "fixtures": ["<match id>"]}.

Tips

Next

Questions? Message @Movisu on Discord.