Bwin API: One Brand, 7 Feeds, 7 Different Prices
There is no public Bwin API. Bwin runs a sportsbook, not a data business, and the only official way in is an Entain commercial agreement. So developers scrape sports.bwin.com, hit Cloudflare, and rebuild the parser every time the front end ships.
The workaround most people reach for is an aggregator with a bwin slug in its bookmaker list. That works. It also hides something that costs money: Bwin is not one price feed. It is seven. On a Ligue 1 fixture on 28 August 2026, bwin.de quoted a 4.80% margin and bwin.fr quoted 14.45% on the same event, at the same second, under the same brand.
This guide pulls all seven Bwin feeds through the OddsPapi API in Python, shows which ones are real prices and which one is a parked board, and gives you the two flags that tell them apart.
The Seven Bwin Feeds
Query the bookmaker catalogue and the family shows up in full. Every slug reports liveOdds: true.
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
catalog = requests.get(
f"{BASE_URL}/bookmakers", params={"apiKey": API_KEY}
).json()
family = [b for b in catalog if b["slug"].startswith("bwin")]
for b in sorted(family, key=lambda x: x["slug"]):
print(f"{b['slug']:10s} {b['bookmakerName']:12s} liveOdds={b['liveOdds']}")
bwin Bwin liveOdds=True
bwin.be Bwin BE liveOdds=True
bwin.de Bwin DE liveOdds=True
bwin.dk Bwin DK liveOdds=True
bwin.es Bwin ES liveOdds=True
bwin.fr Bwin FR liveOdds=True
bwin.pt Bwin PT liveOdds=True
These are the licensed regional skins: Belgium, Germany, Denmark, Spain, France and Portugal, plus the international bwin book. They share a platform. The fixturePath field in the odds payload proves it, because all six regional deep links carry the identical Bwin event ID 2:7847152 on different hostnames.
| Slug | Deep link on the same fixture |
|---|---|
bwin |
sports.bwin.com/en/sports/events/2:7847152 |
bwin.de |
sports.bwin.de/en/sports/events/2:7847152 |
bwin.dk |
sports.bwin.dk/en/sports/events/2:7847152 |
bwin.es |
sports.bwin.es/en/sports/events/2:7847152 |
bwin.fr |
sports.bwin.fr/en/sports/events/2:7847152 |
One platform, one event, six URLs. The prices behind them do not match.
Old Way vs OddsPapi
| Job | Scraping Bwin | OddsPapi |
|---|---|---|
| Get a price | Headless browser per region, 6 sessions | One GET, ?apiKey= |
| Regional feeds | 6 geo-fenced domains, VPN per country | 7 slugs in one payload |
| Detect a parked board | Guesswork | suspended + bookmakerIsActive |
| Compare against the field | Scrape everyone else too | 350+ bookmakers, same call |
| Price history | Build your own recorder, wait weeks | /historical-odds, free tier |
| Blocked by Cloudflare | Constantly | Never |
Step 1: Authenticate and Find a Fixture
The API key goes in the query string. It is not a header, and sending it as one returns a 401.
import requests, time
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def get(path, **params):
r = requests.get(f"{BASE_URL}{path}", params={"apiKey": API_KEY, **params})
r.raise_for_status()
time.sleep(1.0) # the free tier rate-limits per endpoint
return r.json()
# Ligue 1 is tournamentId 34. Set `to` to the day AFTER the last day you want.
fixtures = get("/fixtures", sportId=10, tournamentId=34,
**{"from": "2026-08-28", "to": "2026-08-29"})
for f in fixtures:
if f["hasOdds"]:
print(f["fixtureId"], f["startTime"], f["participant1Name"], "v", f["participant2Name"])
id1000003472036132 2026-08-28T18:45:00.000Z Lille OSC v Paris Saint-Germain
Two things about that date window. The to parameter is a midnight-UTC instant rather than a whole day, so a same-day query returns almost nothing. And an empty window returns HTTP 404 with a FIXTURE_NOT_FOUND body, which will kill a loop that calls raise_for_status() on every window.
Step 2: Pull All Seven Feeds At Once
Pass the whole family to the bookmakers filter. Prices for game lines live under players["0"], and the active flag sits on that price object rather than on the outcome above it.
FIXTURE = "id1000003472036132" # Lille OSC v Paris Saint-Germain
BWIN = ["bwin", "bwin.be", "bwin.de", "bwin.dk", "bwin.es", "bwin.fr", "bwin.pt"]
r = requests.get(f"{BASE_URL}/odds",
params={"apiKey": API_KEY, "fixtureId": FIXTURE,
"bookmakers": ",".join(BWIN)})
r.raise_for_status()
books = r.json()["bookmakerOdds"]
def quote(book, market_id="101"):
"""Return {outcomeId: price} for the active prices only."""
market = book.get("markets", {}).get(market_id)
if not market:
return {}
out = {}
for outcome_id, outcome in market["outcomes"].items():
price = outcome["players"].get("0") # game lines live under "0"
if price and price.get("active"):
out[outcome_id] = price["price"]
return out
print(f"{'slug':10s} {'susp':>5s} {'bkActive':>8s} {'home':>6s} {'draw':>6s} {'away':>6s} {'margin':>7s}")
for slug in BWIN:
book = books.get(slug)
if not book:
print(f"{slug:10s} not priced on this fixture")
continue
q = quote(book)
if len(q) < 3:
print(f"{slug:10s} incomplete 1X2 ({len(q)} of 3 active)")
continue
margin = sum(1 / p for p in q.values()) - 1
print(f"{slug:10s} {str(book['suspended']):>5s} {str(book['bookmakerIsActive']):>8s} "
f"{q['101']:>6} {q['102']:>6} {q['103']:>6} {margin * 100:6.2f}%")
slug susp bkActive home draw away margin
bwin False True 4.8 3.75 1.71 5.98%
bwin.be not priced on this fixture
bwin.de False True 5.0 3.8 1.71 4.80%
bwin.dk False True 4.8 3.75 1.71 5.98%
bwin.es False True 4.8 3.75 1.71 5.98%
bwin.fr True False 3.9 3.5 1.66 14.45%
bwin.pt not priced on this fixture
Twenty-five lines of Python, and the whole problem is on screen. Three feeds agree exactly. One feed is 1.2 points better on the home side. One feed is a wreck, and it is the one licensed in the country the match is played in.
The bwin.fr Problem
bwin.fr priced Lille at 3.9 while bwin.de paid 5.0. That is 28% more on the same bet from the same company. Across the 182 books returning a complete active 1X2 on this fixture, bwin.fr ranked 182nd. Dead last, behind every offshore book on the board.
The payload flags it twice. The book-level suspended field reads true and bookmakerIsActive reads false, while every individual price underneath still reports active: true. A parser that checks only the price-level flag takes the number.
Price history settles what it is. The /historical-odds endpoint is on the free tier and takes a maximum of three bookmakers per call, which fits this comparison exactly.
r = requests.get(f"{BASE_URL}/historical-odds",
params={"apiKey": API_KEY, "fixtureId": FIXTURE,
"bookmakers": "bwin,bwin.de,bwin.fr"})
r.raise_for_status()
for slug, book in r.json()["bookmakers"].items():
# players["0"] is a LIST here, not a dict. One entry per snapshot.
snaps = book["markets"]["101"]["outcomes"]["101"]["players"]["0"]
prices = [s["price"] for s in snaps]
changes = sum(1 for i in range(1, len(prices)) if prices[i] != prices[i - 1])
print(f"{slug:8s} first seen {snaps[0]['createdAt'][:16]} "
f"{len(snaps):3d} snapshots {changes:2d} price changes "
f"{prices[0]} -> {prices[-1]}")
bwin first seen 2026-08-24T11:05 40 snapshots 13 price changes 4.33 -> 4.8
bwin.de first seen 2026-08-24T11:24 11 snapshots 9 price changes 4.33 -> 5.0
bwin.fr first seen 2026-08-24T20:33 2 snapshots 0 price changes 3.9 -> 3.9
bwin repriced 13 times in four days. bwin.de repriced 9 times. bwin.fr opened at 3.9 and never moved. That is a placeholder rather than a price, and the suspended flag said so on the first call.
Eight Leagues, One Brand, Different Answers
One fixture proves nothing. We pulled the full no-filter board on one fixture in each of eight European competitions on 28 August 2026, between 177 and 195 bookmakers per fixture, and ranked every Bwin feed against its own board.
| Competition | Board | bwin |
bwin.de |
bwin.dk |
bwin.es |
bwin.fr |
pinnacle |
|---|---|---|---|---|---|---|---|
| Bundesliga | 180 | 5.22% | 4.27% | 5.22% | 5.22% | 12.92% | 3.76% |
| LaLiga | 180 | 6.35% | 5.13% | 6.35% | 6.35% | absent | 2.81% |
| Ligue 1 | 182 | 5.98% | 4.80% | 5.98% | 5.98% | 14.45% | 3.37% |
| Liga Portugal | 174 | 6.23% | 6.23% | 6.23% | 6.23% | 16.85% | 3.84% |
| Belgian Pro League | 168 | 5.52% | 5.52% | 6.43% | 5.52% | 18.78% | 3.82% |
| Danish Superliga | 172 | 6.88% | 6.88% | 5.53% | 6.88% | absent | 3.65% |
| Premier League | 185 | 5.56% | 4.95% | 5.56% | 4.95% | 14.40% | 3.47% |
| Serie A | 180 | 5.77% | 5.08% | 5.77% | 5.77% | 13.94% | 3.54% |
Three patterns hold across all eight boards.
bwin.de is the sharpest Bwin feed on six of eight competitions. It beats the international bwin slug by 0.5 to 1.2 percentage points, and it does that on English, French, Italian and Spanish football as well as on its own Bundesliga. If you query one Bwin slug, query that one.
The Danish feed sharpens at home. On the Superliga, bwin.dk quoted 4.4 / 4.0 / 1.73 against 4.33 / 3.9 / 1.72 from its siblings. Better on all three outcomes, 5.53% against 6.88%, and it jumped from mid-board to 30th of 172. Local licensing buys local attention on that one league and nowhere else.
bwin.fr is parked everywhere, not just in France. It appeared on six of the eight fixtures, carried suspended: true on all six, and finished bottom-three of its board on all six. It ranged from 12.92% to 18.78%. There is no fixture in this sample where the French feed is worth reading.
bwin.be and bwin.pt turned up on one fixture out of eight, both flagged suspended. The Portuguese feed did not price the Portuguese league. Both carry liveOdds: true in the catalogue, so the catalogue flag says nothing about which fixtures a feed will price.
The Bigger Trap: bwin Is Also BetMGM
Say you skip the regional feeds and query plain bwin alongside a few other books to build a consensus. On the Lille fixture that consensus contains this:
from collections import defaultdict
books = requests.get(f"{BASE_URL}/odds",
params={"apiKey": API_KEY, "fixtureId": FIXTURE}
).json()["bookmakerOdds"]
groups = defaultdict(list)
for slug, book in books.items():
if book["suspended"] or not book["bookmakerIsActive"]:
continue # parked board, skip it
market = book.get("markets", {}).get("101")
if not market:
continue
prices = {}
for outcome_id, outcome in market["outcomes"].items():
p = outcome["players"].get("0")
if p and p.get("active"):
prices[outcome_id] = p["price"]
if len(prices) == 3:
key = (prices["101"], prices["102"], prices["103"])
groups[key].append(slug)
quotes = sum(len(v) for v in groups.values())
print(f"{quotes} usable quotes -> {len(groups)} independent prices "
f"({(1 - len(groups) / quotes) * 100:.1f}% collapse)")
for key, slugs in sorted(groups.items(), key=lambda kv: -len(kv[1]))[:3]:
print(f" {len(slugs):2d} slugs share {key}: {', '.join(sorted(slugs))}")
171 usable quotes -> 108 independent prices (36.8% collapse)
14 slugs share (4.75, 3.8, 1.67): ballybet, betparx, betrivers, bingoal.be, casumo, expekt.se, fourwinds, paf, paf.es, prolineplus, unibet, unibet.be, unibet.dk, unibet.ie
9 slugs share (4.8, 3.75, 1.71): betboo.bet.br, betmgm, borgata, bwin, bwin.dk, bwin.es, oddset, partypoker, sportingbet
7 slugs share (4.8, 3.75, 1.65): 888sport, 888sport.de, 888sport.dk, 888sport.es, 888sport.ro, mrgreen, mrgreen.dk
The second group is the Entain book. bwin, bwin.dk, bwin.es, betmgm, borgata, oddset, partypoker, sportingbet and betboo.bet.br shipped byte-identical prices. Nine slugs, one number. Adding BetMGM to a consensus that already has Bwin adds nothing and doubles that price’s weight.
Note the collapse rate. Across the whole 191-book board, 171 usable quotes reduce to 108 independent prices. Every one of those slugs reports cloneOf: null, so the catalogue flag will not find the duplicates for you. Dedupe on the price tuple, per fixture. Our study of how many bookmakers a backtest needs works through what that does to a closing line.
Betway and Betsson Behave Differently
Bwin is not the only brand shipping regional skins, and the pattern is not the same for each one.
| Brand | Slugs | Margin range across 8 boards | Behaviour |
|---|---|---|---|
| Bwin | 7 | 4.27% – 18.78% | Regional feeds priced independently; one is parked |
| Betway | betway, betway.de, betway.es |
7.62% – 15.89% | Identical on six of eight; bottom-three of the board on five |
| Betsson | betsson, betsson.it |
5.55% – 8.62% | One live feed; betsson.it never appeared |
Betway is the widest major brand in this sample. Its three slugs quoted the same numbers on six of the eight fixtures. They finished 178th, 179th and 180th of 180 on the Bundesliga, and 183rd to 185th of 185 on the Premier League. The Danish Superliga is the one exception, where Betway tightened to 7.62% and the Spanish skin broke away with its own price. Treat the three Betway slugs as one book unless a per-fixture dedupe says otherwise.
Betsson runs a single live feed and a wide menu. On the Lille fixture it shipped 229 markets, more than Bet365’s 214 and Pinnacle’s 109. Its betsson.it slug is in the catalogue and did not price any of the eight fixtures.
Menu width and price quality do not move together anywhere in this data. On that same fixture bwin carried 129 markets, bwin.dk and bwin.es 126 each, betway 96, and the sharpest feed of the family, bwin.de, carried the second-narrowest at 66. The parked bwin.fr still listed 52.
Three Ways the bookmakers Filter Fails
The bookmakers parameter returns three different results and only one of them is an error you would guess.
| You send | You get | Meaning |
|---|---|---|
| A slug that does not exist | 400 INVALID_PARAMETER |
Typo. The error body lists every valid slug. |
| A slug your key cannot read | 403 RESTRICTED_ACCESS |
Plan limit, not a coverage gap. |
| A valid slug not on that fixture | 200, slug simply missing |
Genuinely unpriced. Check with .get(). |
The 400 is the useful one. Its details string enumerates the entire valid slug list, which makes it a free catalogue lookup when you are guessing at a brand’s naming. bwin,ggbet returns it, because there is no ggbet slug. bwin,bwin.pt returns 200 with only bwin in the payload, because the Portuguese feed exists and did not price that match.
What Bwin Costs You Against the Field
Once the seven feeds are in one payload, the rest of the board is one parameter away. Drop the bookmakers filter and the same call returns every book pricing the fixture. On Lille v PSG that was 191 bookmakers.
| Outcome | bwin |
bwin.de |
bwin.fr |
Best on the board | Gain vs bwin |
|---|---|---|---|---|---|
| Lille | 4.8 | 5.0 | 3.9 | 5.6 (betfair-ex) |
+16.67% |
| Draw | 3.75 | 3.8 | 3.5 | 4.5 (bet3000) |
+20.00% |
| PSG | 1.71 | 1.71 | 1.66 | 1.761 (limitless-ex) |
+2.98% |
Pinnacle closed this market at 3.37%, 23rd of 182. Every Bwin feed sat behind it. That is the normal shape: the sharp sets the fair number and the retail brands price around it. Read our consensus odds guide for turning that board into a fair probability, and the vig calculator for the margin arithmetic used throughout this post.
Coverage Outside Football
Bwin prices more than soccer, and the regional set narrows when it does. Sampling the deepest of six fixtures per sport on 28 August 2026:
| Sport | Board on sampled fixture | Bwin family present |
|---|---|---|
| Tennis | 108 books | bwin, bwin.dk, bwin.es |
| American Football | 159 books | bwin, bwin.dk, bwin.es |
| Basketball | 139 books | bwin, bwin.dk, bwin.es |
| Ice Hockey | 66 books | bwin, bwin.dk |
| Baseball | 102 books | bwin, bwin.de, bwin.dk |
The seven-slug spread is a football phenomenon. Outside it, plan for the core three and check per fixture.
A Working Checklist
- Request the whole family, not the single
bwinslug. - Drop any book where
suspendedis true orbookmakerIsActiveis false, before you look at prices. - Check
activeon the price object atplayers["0"], because the outcome above it has no such field. - Dedupe on the price tuple per fixture.
cloneOfwill not do it for you. - Prefer
bwin.dewhen you want one Bwin number. - Treat
bwinandbetmgmas the same feed until a fixture proves otherwise.
FAQ
Does Bwin have an official API?
No public one. Entain licenses data commercially, and there is no self-serve developer portal or documented endpoint. OddsPapi carries all seven Bwin feeds through one REST call with a query-parameter key.
Which Bwin slug should I use?
Use bwin.de. It posted the tightest margin of the family on six of the eight competitions measured, including English, French, Italian and Spanish football. Use bwin.dk as well if you cover the Danish Superliga, where it sharpens to 5.53%.
Why does bwin.fr quote such bad odds?
It is a parked board rather than a traded one. Over four days it logged 2 snapshots and 0 price changes while bwin repriced 13 times. The payload flags it with suspended: true and bookmakerIsActive: false, so you can filter it out before it reaches your model.
Are bwin and BetMGM really the same price?
On the fixture measured here, yes. bwin, bwin.dk, bwin.es, betmgm, borgata, oddset, partypoker, sportingbet and betboo.bet.br shipped byte-identical 1X2 prices. All nine report cloneOf: null, so dedupe on the prices themselves, per fixture.
Can I get historical Bwin odds?
Yes, on the free tier. /historical-odds returns the full snapshot history for a fixture, capped at three bookmakers per call. Note that players["0"] is a list there and a dict on the live endpoint.
Does Betway split into regional feeds the same way?
Betway ships three slugs and they quoted identical prices on six of the eight fixtures. Bwin’s regional feeds are priced independently, and Betway’s mostly are not. The Danish Superliga was the one fixture where betway.es broke away.
Get the Feeds
Stop scraping six geo-fenced domains for one brand. Grab a free OddsPapi API key and pull all seven Bwin feeds, plus 350+ other bookmakers, from one endpoint. Historical price history is on the free tier, which is what turned bwin.fr from a suspicious number into a proven parked board.
Related reading: BetMGM API access (the book that shares Bwin’s price), Ligue 1 odds API, Bundesliga odds API, line shopping in Python, and the free odds API overview.