Premier League Odds API: Live EPL Prices, Handicaps & Corners
The Premier League sells broadcast rights, not odds. There is no official EPL price feed, and the twenty clubs do not run one either. If you want the number Arsenal are trading at on opening weekend, you get it from bookmakers, and every bookmaker either blocks you at the edge or hides prices behind a rendered betslip that changes shape every few months.
This guide skips the scraping. One HTTP call returns the full Premier League board from 15 bookmakers, including two sharps, and this post walks the whole thing: finding the right competition, reading the 1X2, de-vigging Pinnacle, pulling the Asian handicap ladder and the corner markets, and using free historical odds to work out when an EPL price is worth trusting.
Every number below came off the live API on 3 August 2026, three weeks before the 2026/27 season kicks off.
What the Premier League board looks like right now
The feed carries 20 upcoming Premier League fixtures. Five of them have prices. Bookmakers open one round at a time, so the opener and the first full weekend are live while the rest of August sits unpriced.
| Fixture | Kick-off (UTC) | Books | Markets |
|---|---|---|---|
| Arsenal FC v Coventry City | 21 Aug 19:00 | 15 | 225 |
| Brighton & Hove Albion v Aston Villa | 23 Aug 13:00 | 14 | 204 |
| Manchester City v AFC Bournemouth | 23 Aug 13:00 | 14 | 219 |
| Newcastle United v Liverpool FC | 23 Aug 15:30 | 14 | 208 |
| Fulham FC v Chelsea FC | 24 Aug 19:00 | 14 | 204 |
Both sharp books are in: Pinnacle and SBOBet. So are Bet365, DraftKings, FanDuel, BetMGM, Caesars, William Hill, BetRivers, PointsBet, Hard Rock Bet and the BetParx family. That is a smaller field than a live MLB game gets, because the prediction markets have not opened EPL yet, and it is still enough to shop every outcome across eleven independent opinions.
Scraping versus one API call
| Job | Scraping bookmakers | OddsPapi |
|---|---|---|
| Get 15 books on one fixture | 15 scrapers, 15 layouts, 15 breakages | One GET /v4/odds |
| Asian handicap ladder | Rendered client-side, often gated by region | 13 handicap lines in the same payload |
| Corners and team totals | Separate tab, separate DOM | Same nested JSON, own market IDs |
| Historical prices | You store it yourself from day one | /v4/historical-odds, free tier |
| Blocking | Cloudflare, geo-fencing, betslip tokens | API key on a query string |
Step 1: find the right Premier League
Authentication is a query parameter. It is not a header, and every wrapper that assumes otherwise fails on the first call.
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
SOCCER = 10
def find_epl():
r = requests.get(f"{BASE_URL}/tournaments",
params={"apiKey": API_KEY, "sportId": SOCCER})
r.raise_for_status()
named = [t for t in r.json() if t["tournamentName"] == "Premier League"]
england = [t for t in named if t["categoryName"] == "England"]
print(f"{len(named)} tournaments called 'Premier League', {len(england)} in England")
return england[0]
epl = find_epl()
print(epl["tournamentId"], epl["tournamentSlug"], epl["futureFixtures"])
35 tournaments called 'Premier League', 1 in England
17 premier-league 20
Thirty-five competitions in the catalogue answer to “Premier League”. Russia, Ukraine, Israel, Malta, Kazakhstan, Hong Kong and Egypt all use the name. Match on categoryName as well as the name, or hardcode tournamentId 17 once you have it.
Step 2: pull the fixtures that carry prices
/fixtures takes a date range up to ten days wide. Filter on the tournament ID, then filter again on hasOdds, because an unpriced fixture returns metadata only when you ask /odds for it.
def epl_fixtures(start, end, tournament_id):
r = requests.get(f"{BASE_URL}/fixtures", params={
"apiKey": API_KEY, "sportId": SOCCER, "from": start, "to": end})
r.raise_for_status()
return [f for f in r.json() if f["tournamentId"] == tournament_id]
fixtures = epl_fixtures("2026-08-23", "2026-09-01", epl["tournamentId"])
priced = [f for f in fixtures if f["hasOdds"]]
print(f"{len(fixtures)} fixtures, {len(priced)} priced")
for f in priced:
print(f["fixtureId"], f["startTime"][:16],
f["participant1Name"], "v", f["participant2Name"])
14 fixtures, 4 priced
id1000001772221166 2026-08-23T13:00 Brighton & Hove Albion v Aston Villa
id1000001772221168 2026-08-23T13:00 Manchester City v AFC Bournemouth
id1000001772221170 2026-08-23T15:30 Newcastle United v Liverpool FC
id1000001772221172 2026-08-24T19:00 Fulham FC v Chelsea FC
Team names live on participant1Name and participant2Name. The nested participants list comes back empty on soccer fixtures, so read the flat fields.
Step 3: read the 1X2 board
The odds payload nests five levels deep: bookmaker, market, outcome, player, price. Full Time Result is market 101 on soccer, with outcomes 101 home, 102 draw, 103 away.
FIXTURE = "id1000001772221154" # Arsenal FC v Coventry City
FULL_TIME_RESULT = 101
HOME, DRAW, AWAY = 101, 102, 103
def board(fixture_id, market_id, outcome_ids):
r = requests.get(f"{BASE_URL}/odds",
params={"apiKey": API_KEY, "fixtureId": fixture_id})
r.raise_for_status()
books = r.json().get("bookmakerOdds", {})
out = {}
for slug, book in books.items():
market = book["markets"].get(str(market_id))
if not market:
continue
row = {}
for oid in outcome_ids:
outcome = market["outcomes"].get(str(oid))
quote = outcome["players"]["0"] if outcome else None
if quote and quote.get("active") is not False and quote.get("price"):
row[oid] = quote["price"]
if len(row) == len(outcome_ids):
out[slug] = row
return out
prices = board(FIXTURE, FULL_TIME_RESULT, [HOME, DRAW, AWAY])
for slug, row in sorted(prices.items(), key=lambda kv: kv[1][HOME]):
print(f"{slug:18s} {row[HOME]:>6} {row[DRAW]:>6} {row[AWAY]:>6}")
Two details in that filter do real work. Prices on a pre-game fixture sometimes ship active: null rather than true, so test is not False instead of truthiness or you silently drop good quotes. And players["0"] holds the single current price on a game line; player-prop markets key that dict by player ID instead, which is a different parse.
| Bookmaker | Arsenal | Draw | Coventry | Margin |
|---|---|---|---|---|
| BetRivers / Ballybet / FourWinds | 1.16 | 7.50 | 19.00 | 4.80% |
| Pinnacle | 1.155 | 8.02 | 16.05 | 5.28% |
| Hard Rock Bet | 1.154 | 7.50 | 18.50 | 5.39% |
| PointsBet | 1.16 | 7.00 | 18.00 | 6.05% |
| Caesars / William Hill | 1.154 | 7.00 | 19.00 | 6.20% |
| BetParx | 1.14 | 7.50 | 18.00 | 6.61% |
| DraftKings | 1.154 | 7.50 | 15.00 | 6.66% |
| BetMGM / Borgata | 1.17 | 7.00 | 14.00 | 6.90% |
| Bet365 | 1.166 | 7.00 | 13.00 | 7.74% |
| FanDuel | 1.13 | 7.50 | 14.00 | 8.97% |
| SBOBet | 1.17 | 6.20 | 11.50 | 10.29% |
Coventry range from 11.50 to 19.00 across the board. That is a 65% swing in payout on the same outcome, and it is the entire argument for reading more than one book.
Step 4: dedupe before you average
Fifteen slugs quoted this fixture. Eleven of them hold an independent opinion. The rest are the same trading operation shipped under different brands, and the catalogue does not always tell you: /v4/bookmakers reports cloneOf: null for BetMGM and Borgata even though they quoted byte-identical prices on all three outcomes.
def dedupe(prices):
groups = {}
for slug, row in prices.items():
key = tuple(sorted(row.items()))
groups.setdefault(key, []).append(slug)
return {slugs[0]: dict(key) for key, slugs in groups.items()}, groups
independent, groups = dedupe(prices)
print(f"{len(prices)} slugs collapse to {len(independent)} independent quotes")
for key, slugs in groups.items():
if len(slugs) > 1:
print("identical:", ", ".join(slugs))
15 slugs collapse to 11 independent quotes
identical: betmgm, borgata
identical: caesars, williamhill
identical: ballybet, betrivers, fourwinds
Skip this step and a naive consensus triple-weights the Ballybet trading desk. Caesars and William Hill are the one pair the catalogue does flag, since Caesars bought the US arm of William Hill in 2021 and runs both off the same book.
Step 5: de-vig the sharp, then shop the price
Pinnacle takes 5.28% on this market. Strip it out and you get the implied probability the sharpest book on the fixture actually believes, which is the number every other price should be measured against.
def devig(row):
overround = sum(1 / p for p in row.values())
return {oid: (1 / p) / overround for oid, p in row.items()}, overround - 1
def best_price(independent, outcome_id):
slug = max(independent, key=lambda s: independent[s][outcome_id])
return slug, independent[slug][outcome_id]
fair, margin = devig(prices["pinnacle"])
print(f"Pinnacle margin {margin * 100:.2f}%")
for oid, label in [(HOME, "Arsenal"), (DRAW, "Draw"), (AWAY, "Coventry")]:
slug, price = best_price(independent, oid)
print(f"{label:9s} best {price:>6} @ {slug:12s} | "
f"fair {1 / fair[oid]:.3f} ({fair[oid] * 100:.1f}%)")
Pinnacle margin 5.28%
Arsenal best 1.17 @ betmgm | fair 1.216 (82.2%)
Draw best 8.02 @ pinnacle | fair 8.443 (11.8%)
Coventry best 19.00 @ caesars | fair 16.897 (5.9%)
Coventry at 19.00 sits 12.4% above the sharp fair price. Before you call that value, read the next section: Pinnacle’s stake limit on this fixture is tiny, which means its own number is a soft anchor this far from kick-off. Treat 19.00 as the best available price and nothing more.
Ask the Asian book for the Asian handicap
SBOBet posts the widest 1X2 on the board at 10.29%, roughly double Pinnacle. Look at the Asian handicap on the same fixtures and the ranking flips.
| Fixture | Pinnacle 1X2 | SBOBet 1X2 | Pinnacle best AH | SBOBet best AH |
|---|---|---|---|---|
| Arsenal v Coventry | 5.28% | 10.29% | 4.04% (-2) | 2.59% (-2) |
| Newcastle v Liverpool | 5.09% | 10.14% | 3.78% (+0.25) | 2.78% (+0.5) |
| Man City v Bournemouth | 5.74% | 10.52% | 3.85% (-1.25) | 2.57% (-1.25) |
| Brighton v Aston Villa | 4.98% | 10.30% | 3.78% (-0.25) | 3.02% (-0.25) |
| Fulham v Chelsea | 5.15% | 10.19% | 3.85% (+0.25) | 2.61% (+0.25) |
Five fixtures out of five, SBOBet charges about a point less than Pinnacle on the handicap while charging twice as much on the three-way. The Asian book prices the Asian market and treats the 1X2 as an afterthought. If you are building a fair-value model off European football, benchmark the handicap against SBOBet and the 1X2 against Pinnacle rather than picking one sharp for everything.
The ladders differ too. Pinnacle walked nine handicap lines on the Arsenal fixture, from -1 down to -3. DraftKings posted eight, FanDuel two, Bet365 none at all. SBOBet quotes only the two or three lines nearest the true number, which is why its prices there are so tight.
r = requests.get(f"{BASE_URL}/markets", params={"apiKey": API_KEY, "sportId": SOCCER})
names = {m["marketId"]: (m["marketName"], m["handicap"]) for m in r.json()}
books = requests.get(f"{BASE_URL}/odds",
params={"apiKey": API_KEY, "fixtureId": FIXTURE}).json()["bookmakerOdds"]
ladder = sorted((names[mid][1], mid) for mid in map(int, books["pinnacle"]["markets"])
if names.get(mid, ("",))[0] == "Asian Handicap")
print("Pinnacle ladder:", [h for h, _ in ladder])
Pinnacle ladder: [-3, -2.75, -2.5, -2.25, -2, -1.75, -1.5, -1.25, -1]
Soccer carries 32,814 market IDs because every handicap and every total line gets its own ID. Resolve them by name from /v4/markets?sportId=10 rather than pasting a table into your code, and your parser survives the next line the book adds.
Corners, totals, and the mainLine trap
One Premier League fixture carried 225 distinct markets. The big families:
| Market family | Lines on this fixture |
|---|---|
| Over Under Full Time (goals) | 20 |
| Asian Handicap | 13 |
| Over Under First Half | 10 |
| Corners Over Under Full Time | 10 |
| Corners Over Under Team 1 | 10 |
| Asian Handicap First Half | 9 |
Corners are real on English football: 61 corner markets on this one game, covering full-time totals, per-team counts, odd/even, half splits and a corners 1X2. The honest caveat is depth. Seven slugs price them, and after deduping, the main total of 9.5 corners comes down to two independent quotes (the BetParx family at 1.42 / 2.60 and Hard Rock at 1.417 / 2.60). Neither sharp prices corners on this fixture. Cards do not appear at all.
The goals total exposes a parsing trap. Books disagree about which line is the headline: seven quote 2.5 as their main total, six quote 3.5, Pinnacle sits on 3.0 and SBOBet on 2.75. Each outcome carries a mainLine boolean, and it is not consistent between books. FanDuel flagged nine different totals as mainLine: true on this fixture, from 0.5 through 8.5.
So do not trust the flag on its own. Pick the line the most books quote, or the one priced nearest even money, and you get a comparable market across the field.
When does an EPL price become real?
This is where free historical odds earn their place. Pull the price history for the opener and you can watch the market being born.
def pinnacle_history(fixture_id, market_id, outcome_id):
r = requests.get(f"{BASE_URL}/historical-odds", params={
"apiKey": API_KEY, "fixtureId": fixture_id, "bookmakers": "pinnacle"})
r.raise_for_status()
return (r.json()["bookmakers"]["pinnacle"]["markets"][str(market_id)]
["outcomes"][str(outcome_id)]["players"]["0"])
def max_win_base(price, limit):
"""Pinnacle publishes a max win. Recover the per-market base stake."""
return limit if price >= 2 else limit * (price - 1)
snaps = pinnacle_history(FIXTURE, FULL_TIME_RESULT, HOME)
seen = None
for s in snaps:
base = round(max_win_base(s["price"], s["limit"]))
if base != seen:
print(f"{s['createdAt'][:16]} price {s['price']} base {base}")
seen = base
2026-06-19T10:09 price 1.135 base 250
2026-07-30T06:32 price 1.152 base 500
Pinnacle posted Arsenal v Coventry on 19 June, 63 days before kick-off. Across 39 snapshots and six weeks the price crawled from 1.135 to 1.155, a move of under two percent. Then on 30 July the stake limit doubled.
The price barely moved. The size the book will accept doubled. Pinnacle publishes a capped max win rather than a capped stake, so a base of 250 means it wanted no more than £250 of exposure per bet on the opening line, and a base of 500 means it now wants twice that. Newcastle v Liverpool and Fulham v Chelsea show the identical pattern: posted 19 June at a 250 base, sitting at 500 by the end of July.
Compare that with a mid-season market. On the same afternoon, Pinnacle’s bases on three MLB games were 1,875, 3,750 and 3,750. A regular-season baseball game gets between four and fifteen times the exposure of the Premier League opener, because the baseball market has been shaped by real money and the EPL market has not.
Two things follow for anyone modelling English football. Early-summer EPL prices are an opinion, not a consensus, so de-vigged fair values from them carry a wide error bar. And the limit ramp is a usable signal in its own right: when the base steps up, the book has decided the number is worth defending. Everything you need to chart it sits in the free tier, snapshot by snapshot, including the limit field.
Scanning the whole Premier League round
Put the pieces together and a round-wide scan is about twenty lines. Sleep a full second between calls to /odds: the free tier rate-limits per endpoint and a 429 body is valid JSON, so code that only checks for bookmakerOdds reads a rate-limit as “no coverage”.
import time
def scan_round(start, end):
for f in epl_fixtures(start, end, 17):
if not f["hasOdds"]:
continue
prices = board(f["fixtureId"], FULL_TIME_RESULT, [HOME, DRAW, AWAY])
if "pinnacle" not in prices:
continue
independent, _ = dedupe(prices)
fair, margin = devig(prices["pinnacle"])
print(f"\n{f['participant1Name']} v {f['participant2Name']} "
f"({len(independent)} independent books, Pinnacle {margin * 100:.2f}%)")
for oid, label in [(HOME, "home"), (DRAW, "draw"), (AWAY, "away")]:
slug, price = best_price(independent, oid)
edge = (price * fair[oid] - 1) * 100
print(f" {label:5s} {price:>6} @ {slug:12s} "
f"fair {1 / fair[oid]:>7.3f} {edge:+.2f}%")
time.sleep(1.0)
scan_round("2026-08-23", "2026-09-01")
Manchester City v AFC Bournemouth (11 independent books, Pinnacle 5.74%)
home 1.455 @ caesars fair 1.537 -5.37%
draw 5.1 @ fanduel fair 5.298 -3.73%
away 6.5 @ ballybet fair 6.218 +4.54%
Newcastle United v Liverpool FC (10 independent books, Pinnacle 5.09%)
home 3.26 @ pinnacle fair 3.426 -4.85%
draw 4.0 @ fanduel fair 4.025 -0.62%
away 2.15 @ betmgm fair 2.175 -1.17%
The edge column measures the best available price against one book’s de-vigged opinion, and most of it is negative. Across the four priced fixtures in that round, exactly one outcome cleared Pinnacle’s fair price. On a market with a 500 base that is a curiosity. In October, when the same market runs limits ten times higher and every book has been shaped by real money, the same scan is worth acting on.
What it costs
Nothing to start. The free tier covers 383 bookmakers across 69 sports, the full historical archive, and no card at signup. The same fixtureId pattern works for the Champions League, the EFL, and every other competition in the catalogue.
Stop maintaining fifteen scrapers. Get your free API key and pull the Premier League board in one call.
Where to go next
- Football Odds API for the wider soccer feed across 1,757 competitions.
- Asian Handicap API to line-shop the handicap ladder across books.
- Line shopping in Python for the reusable best-price helper.
- No-vig odds for the three ways to strip margin, and when each one is right.
- Betting limits API for the full breakdown of Pinnacle’s max-win field and exchange depth.
FAQ
Is there an official Premier League odds API?
No. The Premier League licenses broadcast and statistical rights, and no club or the league itself publishes betting prices. Odds come from bookmakers, and an aggregator like OddsPapi returns them from 383 books through one endpoint.
Which bookmakers price the Premier League on OddsPapi?
On the 2026/27 opener, 15 books quoted the 1X2: Pinnacle and SBOBet as the sharps, plus Bet365, DraftKings, FanDuel, BetMGM, Caesars, William Hill, BetRivers, PointsBet, Hard Rock Bet, Borgata, BetParx, Ballybet and FourWinds. Four of those slugs duplicate another book’s prices, leaving 11 independent quotes.
Can I get Premier League Asian handicap odds?
Yes. Pinnacle walked nine handicap lines on the opener and DraftKings eight, each with its own market ID. Resolve them by name from /v4/markets?sportId=10 and read the handicap field rather than hardcoding IDs, since a new line means a new ID.
Does the API cover Premier League corner markets?
Yes, with a depth caveat. The opener carried 61 corner markets including full-time totals, per-team counts, odd/even and a corners 1X2. Seven bookmaker slugs price them, and those collapse to two or three independent quotes per line. No sharp book priced corners on that fixture.
How far back does free Premier League historical data go?
/v4/historical-odds returns the full snapshot history for a fixture from the moment a book first posted it. Pinnacle’s price history on the 2026/27 opener starts on 19 June, 63 days before kick-off, and each snapshot carries the price, the timestamp and the stake limit. It is on the free tier, capped at three bookmakers per call.