UK Odds API: Shop 15 UK Books for a Pinnacle-Level EPL Close

UK Odds API
How To Guides November 3, 2026

A UK odds API should return the prices from the books UK bettors actually use: Paddy Power, Betfred, Coral, Ladbrokes, bet365, Betway, the Betfair Exchange and the rest. OddsPapi carries all of those on one key, next to Pinnacle and 300+ other bookmakers. On 30 October 2026 every Premier League fixture for the weekend held 278 to 281 books, and 16 to 19 of them were UK books with a live 1X2 price.

That depth changes the question a UK model can ask. One UK book rarely prices a Premier League close as tight as Pinnacle. The best UK price on each leg, taken across 15 UK sportsbooks, closed at a median 2.68% margin on last weekend’s ten fixtures, against Pinnacle’s 3.04%. This guide shows you how to pull the UK board, collapse the books that share one price, and rebuild that combined close from free price history.

Why UK odds are hard to collect

UK sportsbooks build their sites for bettors, not for code. So most UK projects scrape: one scraper per site, one HTML layout per scraper, and a fix every time a site redesigns its event page. The Betfair Exchange has its own API, with its own account and app-key setup (the Betfair API guide walks through it), and it returns the exchange only.

Scraping also hides a second problem. Several UK brands run on one trading platform, so five scrapers can return the same three prices. You pay the maintenance cost five times and get one opinion.

Scraping UK sites OddsPapi
Access One scraper per site, blocked or rate-limited at the site’s discretion One key, the apiKey query parameter
Books per Premier League fixture The sites you maintain scrapers for 278 to 281 books, 16 to 19 UK books on the 1X2 (30 Oct 2026)
Fixture IDs Each site names the participants its own way One fixtureId across every book
Price history You record it yourself, from the day you start /v4/historical-odds, free on every plan
Live updates Poll each site Poll /v4/odds, or take the WebSocket feed

Step 1: Check the UK slugs

Every request carries the key as a query parameter. The snippets import this helper, which waits out a 429 (the body carries retryMs) and retries a 5xx:

import os
import time
import requests

API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"

def get(path, **params):
    """GET with the key on the query string; waits out a 429 or a 5xx and retries."""
    params["apiKey"] = API_KEY
    for _ in range(5):
        r = requests.get(f"{BASE}{path}", params=params, timeout=30)
        if r.status_code == 429:
            time.sleep(r.json()["error"].get("retryMs", 1000) / 1000 + 0.2)
        elif r.status_code >= 500:
            time.sleep(2)
        else:
            return r
    return r

Start from the catalogue, because a wrong slug fails quietly in a filter. The list below is the UK set we use in the rest of the guide:

from common import get

UK_BOOKS = ["bet365", "paddypower", "betfair-ex", "williamhill", "ladbrokes", "coral",
            "betfred", "betway", "unibet", "888sport", "betmgm.co.uk", "betuk", "virginbet",
            "livescorebet", "grosvenor", "midnite", "netbet.co.uk", "7bet.co.uk", "casumo",
            "mrgreen", "boylesports"]

if __name__ == "__main__":
    catalogue = {b["slug"]: b for b in get("/bookmakers").json()}
    print(f"{len(catalogue)} bookmakers in the catalogue")
    for slug in UK_BOOKS:
        b = catalogue.get(slug)
        if b is None:
            print(f"{slug:14} not in catalogue")
            continue
        print(f"{slug:14} {b['bookmakerName']:16} liveOdds={b['liveOdds']!s:5} cloneOf={b['cloneOf']}")
369 bookmakers in the catalogue
bet365         bet365           liveOdds=False cloneOf=None
paddypower     Paddy Power      liveOdds=True  cloneOf=None
betfair-ex     BetFair Exchange liveOdds=True  cloneOf=None
williamhill    WilliamHill      liveOdds=True  cloneOf=None
ladbrokes      Ladbrokes        liveOdds=True  cloneOf=None
coral          Coral            liveOdds=True  cloneOf=None
betfred        Betfred          liveOdds=True  cloneOf=None
betway         Betway           liveOdds=True  cloneOf=None
unibet         Unibet           liveOdds=True  cloneOf=None
888sport       888Sport         liveOdds=True  cloneOf=None
betmgm.co.uk   BetMGM UK        liveOdds=True  cloneOf=None
betuk          BetUK            liveOdds=True  cloneOf=None
virginbet      Virginbet        liveOdds=True  cloneOf=None
livescorebet   LiveScoreBet     liveOdds=True  cloneOf=virginbet
grosvenor      Grosvenor        liveOdds=True  cloneOf=None
midnite        Midnite          liveOdds=False cloneOf=None
netbet.co.uk   NetBet UK        liveOdds=True  cloneOf=None
7bet.co.uk     7bet UK          liveOdds=True  cloneOf=lottoland
casumo         Casumo           liveOdds=True  cloneOf=None
mrgreen        MrGreen          liveOdds=False cloneOf=None
boylesports    BoyleSports      liveOdds=False cloneOf=None

Two fields matter here. liveOdds says whether the book streams in-play prices; bet365 and Midnite read False but still quote pre-match. cloneOf marks a skin of another book, but do not trust it to find duplicates: Unibet and Casumo read cloneOf: None and quoted the same 1X2 on all ten weekend fixtures. Step 2 finds them on the prices instead.

The williamhill slug carries the US feed that William Hill shares with Caesars, so it does not price the Premier League. The William Hill API guide covers that feed.

Step 2: Pull the UK board and collapse the shared prices

Ask /v4/fixtures for the Premier League (tournamentId=17), then pull the whole board for one fixture in one call. The script keeps the UK books, reads market 101 (the full-time 1X2), and groups the books by their price tuple:

import time
from collections import defaultdict
from common import get
from step1_catalogue import UK_BOOKS

fixtures = get("/fixtures", sportId=10, tournamentId=17,
               **{"from": "2026-10-31", "to": "2026-11-01"}).json()
fixture = fixtures[0]
print(fixture["participant1Name"], "v", fixture["participant2Name"], fixture["startTime"])
time.sleep(1.0)

board = get("/odds", fixtureId=fixture["fixtureId"]).json()["bookmakerOdds"]
print(f"{len(board)} books on the board")

feeds = defaultdict(list)                       # price tuple -> slugs quoting it
for slug in UK_BOOKS:
    book = board.get(slug, {})
    if book.get("suspended") or book.get("bookmakerIsActive") is False:
        continue                                # the whole book is parked pre-match
    market = book.get("markets", {}).get("101")   # 1X2, full time
    if not market:
        continue
    legs = [market["outcomes"][o]["players"]["0"] for o in ("101", "102", "103")]
    if all(leg["active"] is not False and leg["price"] for leg in legs):
        feeds[tuple(leg["price"] for leg in legs)].append(slug)

print(f"{sum(len(s) for s in feeds.values())} UK books priced the 1X2, {len(feeds)} distinct prices")
for prices, slugs in sorted(feeds.items(), key=lambda kv: sum(1 / p for p in kv[0])):
    margin = (sum(1 / p for p in prices) - 1) * 100
    print(f"{margin:5.2f}%  {prices}  {', '.join(slugs)}")
Chelsea FC v Manchester United 2026-10-31T12:30:00.000Z
280 books on the board
18 UK books priced the 1X2, 13 distinct prices
 0.34%  (1.92, 4.0, 4.3)  betfair-ex
 1.44%  (1.9, 4.0, 4.2)  paddypower
 5.10%  (1.79, 3.85, 4.3)  unibet, casumo
 6.20%  (1.8, 3.9, 4.0)  ladbrokes, coral
 6.22%  (1.833, 4.0, 3.75)  betfred
 6.62%  (1.77, 3.8, 4.2)  bet365, grosvenor
 6.87%  (1.8, 3.8, 4.0)  betway
 7.19%  (1.833, 3.8, 3.8)  midnite
 7.29%  (1.76, 3.75, 4.2)  betuk
 8.22%  (1.8, 3.7, 3.9)  888sport, mrgreen
 8.24%  (1.76, 3.7, 4.1)  virginbet, livescorebet
 8.56%  (1.75, 3.7, 4.1)  betmgm.co.uk
 8.97%  (1.786, 3.75, 3.8)  7bet.co.uk

Eighteen UK books gave thirteen distinct prices on this fixture. NetBet UK sat out because its whole book was parked (bookmakerIsActive: false), so check the book-level flags before the price-level ones. Across the ten weekend fixtures, four pairs repeated: Unibet and Casumo on 10 of 10, 888sport and Mr Green on 9, Virgin Bet and LiveScore Bet on 9, and Ladbrokes and Coral on 5. Count each group once before you average UK prices, or the shared feeds outvote the independent books.

Read the exchange row apart from the rest. Betfair Exchange back prices sum to 0.3% to 1.2% over the ten fixtures because the exchange charges its commission on winnings, not in the price. Paddy Power sat at 1.4% to 2.8% on six of the ten fixtures a day before kick-off, close to the exchange, and at 6.8% to 8.7% on the other four.

The Premier League odds API guide covers the rest of the menu (handicaps, totals and corners) on the same board.

Step 3: Rebuild the UK close from free history

The live board shows where the UK books sit a day out. The close shows where they sit when the market is most informed. /v4/historical-odds returns every price each book posted on a played fixture, up to three books per call. Pass outcomeId to keep the payload small. The Betfair Exchange is the one exception: request it alone, with one outcomeId, or the call returns 400.

The script below takes each book’s last price before the scheduled kick-off. It drops a price that the book stopped quoting more than 30 minutes before kick-off, then takes the best UK price on each leg:

import time
from datetime import datetime, timedelta
from common import get

FIXTURE_ID = "id1000001772221336"     # Chelsea v Tottenham, played 24 Oct 2026
UK = ["paddypower", "betfred", "netbet.co.uk", "midnite", "bet365", "unibet", "coral",
      "betway", "888sport", "ladbrokes", "grosvenor", "virginbet", "betuk", "betmgm.co.uk",
      "7bet.co.uk"]
BOOKS = ["pinnacle"] + UK
FRESH = timedelta(minutes=30)          # drop a price the book stopped quoting long before kick-off
LEGS = ("101", "102", "103")           # home, draw, away on market 101 (1X2)

def ts(s):
    return datetime.fromisoformat(s.replace("Z", "+00:00"))

fixture = get("/fixture", fixtureId=FIXTURE_ID).json()
kickoff = ts(fixture["startTime"])
print(fixture["participant1Name"], "v", fixture["participant2Name"], "kick-off", kickoff)

close = {b: {} for b in BOOKS}
for i in range(0, len(BOOKS), 3):      # /historical-odds takes up to 3 slugs per call
    group = BOOKS[i:i + 3]
    for leg in LEGS:
        time.sleep(4.6)
        data = get("/historical-odds", fixtureId=FIXTURE_ID,
                   bookmakers=",".join(group), outcomeId=leg).json()
        for book in group:
            snaps = (data.get("bookmakers", {}).get(book, {}).get("markets", {})
                     .get("101", {}).get("outcomes", {}).get(leg, {})
                     .get("players", {}).get("0", []))
            before = [s for s in snaps if ts(s["createdAt"]) < kickoff]
            last = max(before, key=lambda s: s["createdAt"]) if before else None
            if last and last["active"] and kickoff - ts(last["createdAt"]) <= FRESH:
                close[book][leg] = last["price"]

def margin(prices):
    return (sum(1 / p for p in prices) - 1) * 100

best = []
for leg in LEGS:
    quotes = {b: close[b][leg] for b in UK if leg in close[b]}
    top = max(quotes.values())
    best.append(top)
    print(f"leg {leg}: best UK {top:<5} from {', '.join(b for b, p in quotes.items() if p == top)}"
          f"   pinnacle {close['pinnacle'][leg]}")

full = [b for b in UK if len(close[b]) == 3]
print(f"{len(full)} UK books with a fresh close on all three legs")
print(f"best UK price per leg: margin {margin(best):.2f}%")
print(f"pinnacle:              margin {margin([close['pinnacle'][l] for l in LEGS]):.2f}%")
Chelsea FC v Tottenham Hotspur kick-off 2026-10-24 16:30:00+00:00
leg 101: best UK 1.91  from paddypower   pinnacle 1.869
leg 102: best UK 4.0   from paddypower   pinnacle 4.01
leg 103: best UK 4.0   from paddypower, bet365   pinnacle 4.07
10 UK books with a fresh close on all three legs
best UK price per leg: margin 2.36%
pinnacle:              margin 3.01%

We ran the same cut on all ten Premier League fixtures played 23 to 25 October 2026. Pinnacle closed at a median 3.04% (2.70% to 3.51%). The best UK price per leg closed at a median 2.68% (1.98% to 3.47%), tighter than Pinnacle on 6 of 10 fixtures, and it matched or beat Pinnacle’s price on 17 of 30 legs. Betfred and Paddy Power supplied the best UK price on 9 legs each, NetBet UK on 6, Midnite on 5 and bet365 on 4.

No single UK book got there alone. Each UK sportsbook’s median close, with the same cut and the same freshness filter:

Book Slug Fixtures with a fresh close Median 1X2 margin at the close
Pinnacle (reference) pinnacle 10 of 10 3.04%
NetBet UK netbet.co.uk 10 of 10 5.03%
Unibet unibet 7 of 10 5.21%
Betfred betfred 8 of 10 5.62%
Coral coral 7 of 10 6.03%
bet365 bet365 10 of 10 6.22%
Ladbrokes ladbrokes 8 of 10 6.48%
Paddy Power paddypower 10 of 10 6.57%
Grosvenor grosvenor 8 of 10 6.70%
Betway betway 8 of 10 6.73%
Virgin Bet virginbet 10 of 10 7.21%
BetUK betuk 6 of 10 7.33%
Midnite midnite 8 of 10 7.60%
888sport 888sport 7 of 10 8.02%
BetMGM UK betmgm.co.uk 7 of 10 8.79%
7bet UK 7bet.co.uk 9 of 10 8.95%

NetBet UK was the tightest single UK sportsbook, at 4.85% to 5.21% on all ten. Paddy Power’s range runs from 2.03% to 7.12%, which matches what the live board showed: on some fixtures it prices near the exchange, on most it does not. So rank the books per fixture and per leg, and do not pick one UK book as your reference.

Where you cut the record moves the close. The historical odds API guide compares the scheduled kick-off, the recorded kick-off and the last active row. To turn the combined close into a fair price, the consensus odds guide de-vigs the deduplicated board, and the line shopping guide runs the same best-price logic live.

UK odds API FAQ

Is there a UK odds API?

Yes. OddsPapi returns UK bookmaker odds as JSON on /v4/odds, including Paddy Power, Betfred, Coral, Ladbrokes, bet365, Betway, Unibet, 888sport and the Betfair Exchange. On 30 October 2026 each Premier League fixture carried 16 to 19 UK books with a 1X2 price, out of 278 to 281 books on the board.

Which UK bookmakers does the API cover?

Check /v4/bookmakers for the current list. The UK set in this guide is bet365, paddypower, betfair-ex, ladbrokes, coral, betfred, betway, unibet, 888sport, betmgm.co.uk, betuk, virginbet, livescorebet, grosvenor, midnite, netbet.co.uk, 7bet.co.uk, casumo and mrgreen.

Can I get historical UK bookmaker odds?

Yes. /v4/historical-odds returns every price a book posted on a played fixture, up to three bookmakers per call, on every plan. Request the Betfair Exchange alone with one outcomeId.

Which UK bookmaker has the best Premier League odds?

It changes per fixture and per leg. At the close of 10 Premier League fixtures on 23 to 25 October 2026, NetBet UK had the tightest single-book margin (median 5.03%), and Betfred and Paddy Power supplied the best UK price on 9 of 30 legs each. Combined, the best UK price per leg closed at a median 2.68%.

Do UK bookmakers share odds feeds?

Some do. Unibet and Casumo quoted the same Premier League 1X2 on 10 of 10 fixtures, and 888sport and Mr Green, Virgin Bet and LiveScore Bet, and Ladbrokes and Coral also shared prices. Group books by their price tuple before you average them.

Pull the UK board on one key

Every call in this guide runs on the free key: up to 19 UK books on each Premier League board, 300+ bookmakers in all, free price history for the close, and a WebSocket feed when polling is too slow. Get a free API key and run Step 2 on this weekend’s fixtures.