Bwin API: One Brand, 7 Feeds, 7 Different Prices

Bwin API - OddsPapi API Blog
How To Guides September 6, 2026

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

  1. Request the whole family, not the single bwin slug.
  2. Drop any book where suspended is true or bookmakerIsActive is false, before you look at prices.
  3. Check active on the price object at players["0"], because the outcome above it has no such field.
  4. Dedupe on the price tuple per fixture. cloneOf will not do it for you.
  5. Prefer bwin.de when you want one Bwin number.
  6. Treat bwin and betmgm as 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.