UK Odds API: Shop 15 UK Books for a Pinnacle-Level EPL Close
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.