Winamax API: The .fr Suffix Adds 3.3 Points to Four Brands
Winamax is the biggest sportsbook in France and it has no public API. There is no developer portal, no documented endpoint, no key you can request. What Winamax does have in the OddsPapi catalogue is three slugs: winamax.fr, winamax.es and winamax.de. All three carry live odds. None of them is a copy of the others.
This post shows how to read all three in one call, and what a 69-fixture measurement says about which one you should trust. The short version: on the same fixture, at the same second, the French feed quotes a wider price than the German one. That holds on 49 of 49 fixtures where both ship a clean three-way.
Then the pattern widens past Winamax. Six .fr slugs sit on a Ligue 1 board, and each one prices its own domestic competition worse than its own international sibling does.
Three Slugs, Two Feeds
The catalogue call is one line. On 8 September 2026 it returned 357 bookmakers, three of them Winamax.
| Slug | Name | liveOdds | cloneOf |
|---|---|---|---|
winamax.fr |
Winamax FR | true | null |
winamax.es |
Winamax ES | true | null |
winamax.de |
Winamax DE | true | null |
cloneOf is null on all three, which tells you nothing. It is null on plenty of slugs that quote byte-identical prices. The only way to know whether two feeds are the same feed is to compare the numbers.
So we did. Across 69 fixtures in seven competitions, winamax.es and winamax.de posted the same three-way tuple on 42 of the 45 fixtures where both were readable. winamax.fr matched neither one, on any of the 49 fixtures readable against the German feed or the 42 against the Spanish. Three slugs, two pricing engines: a French one, and a shared international one.
Old Way vs OddsPapi
| Scraping Winamax | OddsPapi | |
|---|---|---|
| Access | No public API, geo-locked site, session tokens | ?apiKey= query parameter |
| Regional feeds | Three separate sites, three scrapers | Three slugs in one call |
| Comparison books | Build one scraper per book | 281 books on the same fixture payload |
| Price history | You store it yourself from day one | /historical-odds on the free tier |
| Market coverage | Whatever the page renders | 61 to 67 market IDs per Winamax feed |
| Push updates | Poll and hope | WebSocket stream |
Step 1: Find a French Fixture
Authentication is a query parameter. There is no header, no OAuth dance, no bearer token.
import os
import requests
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
fixtures = requests.get(f"{BASE}/fixtures", params={
"apiKey": API_KEY,
"sportId": 10, # soccer
"from": "2026-09-08",
"to": "2026-09-16",
}).json()
ligue1 = [f for f in fixtures
if f["tournamentName"] == "Ligue 1"
and f["categoryName"] == "France"
and f["hasOdds"]]
for f in ligue1[:3]:
print(f["fixtureId"], f["startTime"],
f["participant1Name"], "v", f["participant2Name"])
id1000003472036180 2026-09-11T18:45:00.000Z Stade Rennais FC v Olympique Marseille
id1000003472036182 2026-09-12T15:15:00.000Z Strasbourg Alsace v AS Monaco
id1000003472036166 2026-09-12T18:45:00.000Z AJ Auxerre v OGC Nice
Filter on categoryName, because “Ligue 1” is also the name of the Algerian top flight. Two Algerian Ligue 1 fixtures sat in the same window while we ran this. The tournamentName alone will hand you the wrong country.
Set to to the day after the last day you want. It is a midnight UTC instant, so a same-day range returns only the 00:00Z kick-offs.
Step 2: Pull All Three Feeds in One Call
The bookmakers filter takes a comma-separated list. Ask for all three Winamax slugs at once.
FIXTURE = "id1000003472036180" # Stade Rennais v Olympique Marseille
WINAMAX = "winamax.fr,winamax.es,winamax.de"
odds = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY,
"fixtureId": FIXTURE,
"bookmakers": WINAMAX,
}).json()["bookmakerOdds"]
print(sorted(odds))
# ['winamax.de', 'winamax.es', 'winamax.fr']
Step 3: Read the Three-Way and Measure the Margin
Soccer’s full-time result is market 101, with outcomes 101 home, 102 draw and 103 away. Three flags decide whether a quote is real, and you have to check all three. The book-level suspended flag and bookmakerIsActive sit on the bookmaker object. The active flag sits on each price. A parked board ships every price with active: true underneath a suspended: true book.
FULL_TIME_RESULT = "101"
LEGS = {"101": "home", "102": "draw", "103": "away"}
def read_1x2(book):
if book.get("suspended") or book.get("bookmakerIsActive") is False:
return None
market = book.get("markets", {}).get(FULL_TIME_RESULT)
if not market or not market.get("marketActive"):
return None
prices = {}
for outcome_id, name in LEGS.items():
outcome = market["outcomes"].get(outcome_id)
if outcome is None:
return None
price = next((p for p in outcome["players"].values() if p["active"]), None)
if price is None:
return None
prices[name] = price["price"]
return prices
def overround(prices):
return (sum(1 / p for p in prices.values()) - 1) * 100
for slug in sorted(odds):
book = odds[slug]
prices = read_1x2(book)
if prices is None:
print(f"{slug:12} skipped: suspended={book['suspended']}")
else:
print(f"{slug:12} {prices} margin {overround(prices):.2f}%")
winamax.de {'home': 2.1, 'draw': 4.0, 'away': 2.9} margin 7.10%
winamax.es skipped: suspended=True
winamax.fr {'home': 2.1, 'draw': 3.95, 'away': 2.9} margin 7.42%
The two live feeds differ by five hundredths on the draw and nothing else. That is enough to move the margin by a third of a point. Across the 69 fixtures, winamax.fr shipped suspended: true on 14, winamax.es on 17 and winamax.de on 7. Skip that check and you will average a parked board into your consensus.
The players dict is keyed by player ID on prop markets and by "0" on game lines. Iterate the values rather than hardcoding players["0"], and the same reader works on both.
Step 4: Rank Winamax Against the Whole Board
Drop the bookmakers filter and the same fixture returns everything the key can read.
board_raw = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY, "fixtureId": FIXTURE,
}).json()["bookmakerOdds"]
board = {}
for slug, book in board_raw.items():
if slug.startswith("pinnacle+") or slug == "demo":
continue # internal test feeds
prices = read_1x2(book)
if prices:
board[slug] = prices
ranked = sorted(board, key=lambda s: overround(board[s]))
tuples = {tuple(p.values()) for p in board.values()}
print(len(board_raw), "books in the payload")
print(len(board), "quote a complete active three-way")
print(len(tuples), "independent price tuples")
for slug in ["pinnacle", "winamax.de", "winamax.fr", "bet365.fr", "bwin.fr"]:
if slug in board:
print(f"{slug:12} rank {ranked.index(slug) + 1:>3} of {len(ranked)}"
f" {overround(board[slug]):.2f}%")
281 books in the payload
267 quote a complete active three-way
87 independent price tuples
pinnacle rank 30 of 267 3.48%
winamax.de rank 187 of 267 7.10%
winamax.fr rank 189 of 267 7.42%
bet365.fr rank 263 of 267 12.90%
bwin.fr rank 264 of 267 14.56%
267 books, 87 independent prices. Two thirds of that board is duplicate feeds quoting the same tuple, which is why you dedupe on the price tuple before you compute a consensus. Counting slugs instead of prices will triple-weight whichever group happens to be largest.
The .fr Premium Runs Across Six Brands
Look at the ranks again. bet365.fr at 263 and bwin.fr at 264, out of 267. The international bet365 slug on the same fixture prices at 10.11% and bwin at 6.51%. Same brand, same match, same second.
We ran the pairing across all 69 fixtures: every .fr slug against its own brand’s international feed on the same fixture. Four brands ship both.
| French feed | Parent feed | Paired reads | Median gap | .fr wider on |
|---|---|---|---|---|
bwin.fr |
bwin |
65 | +8.81 pp | 65 of 65 |
bet365.fr |
bet365 |
69 | +3.02 pp | 69 of 69 |
unibet.fr |
unibet |
64 | +3.01 pp | 62 of 64 |
winamax.fr |
winamax.de |
49 | +1.60 pp | 49 of 49 |
| All four | 247 | +3.28 pp | 245 of 247 | |
Six .fr slugs appeared on these boards: bet365.fr, bwin.fr, pokerstars.fr, unibet.fr, winamax.fr and zebet.fr. On the 18 French league fixtures their median overrounds ran 9.75% to 14.94%, against a board median of 7.20% and a Pinnacle median of 3.88%.
The suffix predicts the price better than the brand does. If you are building a French-facing product and you reach for the French slug because it looks like the right one, you are picking the widest version of that book on purpose.
PAIRS = [("winamax.fr", "winamax.de"), ("unibet.fr", "unibet"),
("bwin.fr", "bwin"), ("bet365.fr", "bet365")]
for local, parent in PAIRS:
if local in board and parent in board:
gap = overround(board[local]) - overround(board[parent])
print(f"{local:12} {overround(board[local]):5.2f}%"
f" {parent:12} {overround(board[parent]):5.2f}%"
f" {gap:+.2f} pp")
winamax.fr 7.42% winamax.de 7.10% +0.32 pp
unibet.fr 6.68% unibet 7.08% -0.40 pp
bwin.fr 14.56% bwin 6.51% +8.04 pp
bet365.fr 12.90% bet365 10.11% +2.79 pp
One fixture is not the study. On this particular match unibet.fr came in 0.40 points tighter than its parent, one of the two reversals in 247. Run the loop over a slate before you trust the direction, which is the whole reason we ran 69.
Where the Gap Is Widest
The two Winamax feeds track each other closely in the top flights and separate in the second tiers.
| Competition | Fixtures | Board median | winamax.fr |
winamax.de |
Gap | pinnacle |
|---|---|---|---|---|---|---|
| Premier League | 10 | 6.04% | 7.70% | 6.83% | 0.79 pp | 3.60% |
| LaLiga | 13 | 6.25% | 7.90% | 7.01% | 1.06 pp | 3.87% |
| Serie A | 10 | 6.22% | 8.00% | 6.93% | 1.28 pp | 3.70% |
| Ligue 1 | 9 | 6.33% | 8.12% | 6.80% | 1.29 pp | 3.35% |
| Bundesliga | 9 | 6.41% | 8.90% | 7.23% | 1.91 pp | 3.68% |
| Ligue 2 | 9 | 7.87% | 14.87% | 12.10% | 2.17 pp | 5.62% |
| 2. Bundesliga | 9 | 7.60% | 14.84% | 11.90% | 2.99 pp | 4.56% |
The ordering is consistent. Premier League 0.79 points apart, 2. Bundesliga 2.99. Both Winamax feeds roughly double their margin when they drop from a first division to a second, and the French feed doubles further. Pinnacle widens too, from 3.35% on Ligue 1 to 5.62% on Ligue 2, so some of this is the competition rather than the book. The difference between two feeds of one brand is not a competition effect.
If your model reads Ligue 2, this is the whole ballgame. A 14.87% overround against a 7.87% board median means you are paying twice the going rate for the same opinion. Ligue 1 alone carries 174 books a fixture, and the second tier still returned 258 on the fixtures we sampled.
Two Brands, One Price
The clone check turned up a pair that has nothing to do with Winamax and everything to do with your consensus maths.
| Slug A | Slug B | Identical three-way on |
|---|---|---|
unibet.fr |
zebet.fr |
66 of 66 fixtures |
winamax.de |
winamax.es |
42 of 45 fixtures |
unibet |
unibet.be |
42 of 67 fixtures |
pmu |
unibet |
30 of 67 fixtures |
Unibet France and Zebet are two brands, two websites and two licences, and they posted the same numbers on every fixture we read. cloneOf is null on both. Add them both to a fair-value average and you have weighted one opinion twice.
The rule that survives every one of these audits: dedupe on the price tuple, never on the metadata. Post 3731 found the same thing inside Bwin’s seven regional feeds, where the international slug turned out to share a price with BetMGM.
When a Slug Returns Nothing
Winamax France stops at the top two divisions. On a French third-tier fixture the German feed priced it and the French one did not. The failure modes look identical from the outside until you probe one slug at a time.
def probe(fixture_id, slug):
r = requests.get(f"{BASE}/odds", params={
"apiKey": API_KEY, "fixtureId": fixture_id, "bookmakers": slug,
})
if r.status_code == 400:
return "not a slug in the catalogue"
if r.status_code == 403:
return "real slug, gated on your plan"
if slug in r.json().get("bookmakerOdds", {}):
return "priced"
return "real slug, no board on this fixture"
NATIONAL = "id1000018372523912" # SM Caen v Paris 13 Atletico
for slug in ["winamax.fr", "winamax.de", "winamax.co.uk"]:
print(f"{slug:14} {probe(NATIONAL, slug)}")
winamax.fr real slug, no board on this fixture
winamax.de priced
winamax.co.uk not a slug in the catalogue
Three distinct answers, three distinct status codes. The 400 body is worth reading on its own: it enumerates every valid slug, which makes it a free catalogue lookup when you have fat-fingered a name.
Note the empty case. When a valid slug has no board on a fixture, the response omits bookmakerOdds entirely rather than returning it empty. Use .get("bookmakerOdds", {}) or your loop dies on a KeyError.
Free Winamax Price History
Historical odds are on the free tier, which is the part competitors put behind an enterprise contract. Point /historical-odds at the same fixture and you get every snapshot recorded since the board opened.
history = requests.get(f"{BASE}/historical-odds", params={
"apiKey": API_KEY,
"fixtureId": FIXTURE,
"bookmakers": "winamax.fr,winamax.de,pinnacle", # max 3 per call
}).json()["bookmakers"]
for slug, book in history.items():
market = book["markets"][FULL_TIME_RESULT]
snapshots = market["outcomes"]["101"]["players"]["0"]
snapshots.sort(key=lambda s: s["createdAt"])
changes = sum(1 for a, b in zip(snapshots, snapshots[1:])
if a["price"] != b["price"]) + 1
print(f"{slug:12} {len(snapshots):>3} snapshots,"
f" {changes:>3} distinct home prices,"
f" first {snapshots[0]['createdAt'][:10]}")
pinnacle 51 snapshots, 38 distinct home prices, first 2026-08-30
winamax.de 69 snapshots, 7 distinct home prices, first 2026-08-31
winamax.fr 26 snapshots, 6 distinct home prices, first 2026-08-31
Collapse consecutive identical prices before you count anything. The feed records a snapshot on a schedule whether or not the price moved, so 69 raw snapshots became 7 repricings. Skip that step and you will measure your own polling interval.
The endpoint caps at three bookmakers per call, and one unreadable slug fails the whole batch. Loop in threes and retry a failed batch one slug at a time.
What This Costs You
Take the Rennes fixture and pull the best available price on each leg from the full 267-book board.
| Leg | winamax.fr |
Best on the board | Posted by | Difference |
|---|---|---|---|---|
| Rennes | 2.10 | 2.20 | balkanbet.rs, fanatics, pointsbet.com.au |
+4.8% |
| Draw | 3.95 | 4.04 | 1xbet |
+2.3% |
| Marseille | 2.90 | 3.40 | betfair-ex and four sharp exchanges |
+17.2% |
Across all 69 fixtures, the three Winamax slugs posted the best price on zero of 207 legs. The books that did keep turning up are wettarena (42 legs), bet3000 (39) and the sharp exchange cluster (33 each). That is what line shopping across 350+ books buys you over reading one national brand.
One warning on that composite. The three best legs on this fixture sum to an implied 99.62%, which reads like free money and is not. Those prices come from four venues with four different liquidity profiles, and the exchange leg is a ladder level rather than a standing offer. Check limit and exchangeMeta before you believe a sub-100% composite, and treat it as a signal that your filter needs tightening.
A Working Checklist
- Request all three Winamax slugs.
winamax.deandwinamax.esare one feed, so two reads cover the brand. - Check
suspendedandbookmakerIsActiveon the book, thenactiveon the price. All three, every time. - Filter
categoryNameon/fixtures. Ligue 1 exists in France and in Algeria. - Dedupe on the price tuple before any average. 267 books were 87 independent prices.
- Drop
pinnacle+anddemoslugs. They are internal test feeds and they will count Pinnacle twice. - Probe a single slug when a book goes missing. 400 means the slug is wrong, 403 means gated, 200 with nothing means unpriced.
- Collapse consecutive identical historical prices before counting repricings.
- Never treat a regional suffix as cosmetic. It is the strongest single predictor of margin we measured.
FAQ
Does Winamax have an official API?
No. Winamax publishes no developer portal and no documented endpoint. OddsPapi carries its prices as three slugs, winamax.fr, winamax.es and winamax.de, readable through the standard /odds call with an API key on the query string.
Which Winamax slug should I use?
Read all three and keep the best price per leg. If you want one, winamax.de priced tighter than winamax.fr on 49 of 49 fixtures we measured on 8 September 2026, and it shares its numbers with winamax.es on 42 of 45.
Why do the three Winamax feeds quote different odds?
They run on separate pricing books under separate licences. winamax.es and winamax.de matched each other on 42 of 45 fixtures; winamax.fr matched neither one on any of the 49 and 42 fixtures where the comparison was readable.
Do other French bookmakers behave the same way?
Yes. Pairing each .fr slug against its own brand’s international feed on the same fixture gave a median gap of 3.28 percentage points across 247 paired reads, with the French feed wider on 245 of them.
Which competitions does Winamax cover?
All three slugs priced every fixture we sampled in the Premier League, LaLiga, Serie A, Bundesliga, 2. Bundesliga, Ligue 1 and Ligue 2, carrying 61 to 67 market IDs each. On French third-tier fixtures, winamax.de and winamax.es priced the board and winamax.fr returned no odds.
Can I get historical Winamax odds?
Yes, on the free tier. /historical-odds returns every recorded snapshot per outcome for up to three bookmakers per call. On the Rennes fixture it returned 69 snapshots for winamax.de and 26 for winamax.fr, back to 31 August.
How many books can I compare Winamax against?
The catalogue held 357 bookmakers on 8 September 2026, and a single Ligue 1 fixture returned 281 of them in one payload. 267 quoted a complete active three-way.
Get the Feeds
One key, one endpoint, and every Winamax feed alongside 350+ other books on the same fixture. Free historical odds, native market structures including Asian handicaps, and a WebSocket stream when polling stops being enough.
Start with the free tier, then read the vig calculator to turn these margins into a ranking of your own, and the no-vig guide to strip the margin out before you compare a price to your model.