Unibet API: Map 39 Sportsbook Feeds Through One Event ID
Looking for a Unibet API? You want Unibet’s prices as JSON, per fixture, without a scraper that breaks every time unibet.com ships a new front end. OddsPapi returns them under eight slugs: unibet, unibet.be, unibet.com.au, unibet.dk, unibet.fr, unibet.ie, unibet.nl and unibet.se. All eight carry liveOdds: true in the catalogue.
The useful part sits one field deeper. Every bookmaker row in /v4/odds carries a bookmakerFixtureId, the event ID that the book’s own platform uses. On 28 September 2026 we pulled 22 full boards across the UEFA Nations League, the NFL, the NHL, the SHL and the EuroLeague. On 21 of them, Unibet’s event ID also tagged 38 other feeds: LeoVegas, BetRivers, Paf, Svenska Spel, 888sport Italy, BetParx and more. That is 39 feeds you can map with one string compare, and they do not all quote the same price. This guide shows how to pull the Unibet feeds, find the family, dedupe it, and read its closing lines.
Why one Unibet scraper gets you one brand
The family is the Kambi sportsbook platform, and the payload says so: BetParx’s fixturePath is nj.betparx.com/kambi#event/... and Four Winds points at retail.kambicdn.com. Every brand in the family reads the same event from the platform. Each brand then sets its own margin, or keeps the platform price.
That creates two problems for a scraper. First, you need a separate scraper per brand and per country, because unibet.se, unibet.nl and leovegas.es are different sites with different markups. Second, when you do get all of them, you cannot tell which prices are independent. Feed 26 copies of the same moneyline into a consensus model and you have weighted one opinion 26 times.
The bookmakerFixtureId field solves the first problem and makes the second one measurable. Group the board on it, and the platform family falls out. Compare prices inside the family, and you see which brands reprice.
| Scraping unibet.com | OddsPapi | |
|---|---|---|
| Unibet feeds | One site per scraper | 8 slugs, one key |
| Same-platform feeds | A new scraper per brand and country | 38 more on Unibet’s event ID, same call |
| Which prices are independent | Unknown | Group on bookmakerFixtureId, dedupe on the price |
| Price history | Only what you stored yourself | /v4/historical-odds, free tier |
| Rest of the board | None | 350+ bookmakers in the catalogue, 236 to 289 on each board we measured |
| Push updates | Poll and diff the page | WebSocket feed on the same fixture IDs |
Step 1: find the Unibet slugs
Authentication is one query parameter, apiKey. The helper below retries on a 429 and waits for as long as the error body tells it to. The first call lists the catalogue and keeps every slug that starts with unibet.
import os, time, requests
from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
def get(path, **params):
params["apiKey"] = API_KEY
for _ in range(4):
r = requests.get(f"{BASE}/{path}", params=params, timeout=180)
if r.status_code == 429: # rate limited: wait as told
time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
continue
if r.status_code == 404: # empty window
return None
r.raise_for_status()
return r.json()
raise RuntimeError("rate limited")
books = get("bookmakers")
unibet = [b for b in books if b["slug"].startswith("unibet")]
print(f"{len(books)} bookmakers in the catalogue, {len(unibet)} Unibet feeds\n")
for b in unibet:
print(f"{b['slug']:<15}{b['bookmakerName']:<12} liveOdds={b['liveOdds']!s:<6} cloneOf={b['cloneOf']}")
361 bookmakers in the catalogue, 8 Unibet feeds
unibet Unibet liveOdds=True cloneOf=None
unibet.be Unibet BE liveOdds=True cloneOf=None
unibet.com.au Unibet AU liveOdds=True cloneOf=None
unibet.dk Unibet DK liveOdds=True cloneOf=None
unibet.fr Unibet FR liveOdds=True cloneOf=None
unibet.ie Unibet IE liveOdds=True cloneOf=None
unibet.nl Unibet NL liveOdds=True cloneOf=None
unibet.se Unibet SE liveOdds=True cloneOf=None
Every one of the eight shows cloneOf: None. The catalogue does not tell you that seven of them share a platform, or that unibet.fr runs on a different one. The board does.
Step 2: group the board on the event ID
This step takes the first priced NHL fixture in the next four days, pulls the whole board in one /v4/odds call, and groups every bookmaker row on bookmakerFixtureId. Then it prints the family around unibet and the family around unibet.fr.
# Reuses get() and the imports from Step 1.
now = datetime.now(timezone.utc)
fixtures = get("fixtures", tournamentId=234, # 234 = NHL
**{"from": now.strftime("%Y-%m-%d"),
"to": (now + timedelta(days=4)).strftime("%Y-%m-%d")}) or []
fx = next(f for f in fixtures if f["hasOdds"] and f["statusName"] == "Pre-Game")
board = get("odds", fixtureId=fx["fixtureId"])["bookmakerOdds"]
print(f"{fx['participant1Name']} v {fx['participant2Name']}, {len(board)} books on the board\n")
# Group every feed on the board by the event ID its own platform uses
events = defaultdict(list)
for slug, book in board.items():
events[book.get("bookmakerFixtureId")].append(slug)
for slug in ("unibet", "unibet.fr"):
event_id = board[slug]["bookmakerFixtureId"]
family = sorted(events[event_id])
print(f"{slug} event {event_id}: {len(family)} feeds")
print(" " + ", ".join(family) + "\n")
Carolina Hurricanes v Florida Panthers, 274 books on the board
unibet event 1028398381: 39 feeds
888sport.it, atg.se, ballybet, betcity.nl, betmgm.co.uk, betparx, betplay, betrivers, betuk, bingoal.be, casumo, expekt.se, fourwinds, grosvenor, jacks.nl, kto, kto.bet.br, leovegas, leovegas.es, leovegas.it, livescorebet, paf, paf.es, pmu, prolineplus, rushbet.co, scooore.be, stake.bet.br, sugarhouse, svenskaspel, tabtouch, unibet, unibet.be, unibet.com.au, unibet.dk, unibet.ie, unibet.nl, unibet.se, virginbet
unibet.fr event 3364764: 3 feeds
fdj, unibet.fr, zebet.fr
Unibet’s event ID is the biggest family on this board: 39 of 274 feeds. The next two families on the same fixture hold 24 and 23 feeds. The French Unibet feed sits on a different event ID with FDJ and ZEbet, and that split held on all 22 boards we measured. If you want the French prices, read unibet.fr as its own book. Our Winamax API guide measures what the French licence does to those prices.
The 39-feed family held on 21 of 22 boards. The exception was Turkiye v Italy in the Nations League, where the three US feeds (ballybet, betrivers, sugarhouse) did not quote the fixture and the family dropped to 36.
Step 3: dedupe the family and rank the prices
Inside the family, prices split. This step reads the NHL moneyline (market 151, Winner including overtime) for every feed in the family, drops suspended books, inactive markets and incomplete quotes, and groups the feeds on the exact price pair. It reuses board and events from Step 2.
# Reuses board and events from Step 2.
MARKET = "151" # NHL Winner (incl. overtime), 2-way
def moneyline(book):
m = book.get("markets", {}).get(MARKET)
if book.get("suspended") or not m or m.get("marketActive") is False:
return None
legs = [(oid, p["price"]) for oid, o in m["outcomes"].items()
for p in o["players"].values() if p.get("active") and p["price"] > 1.0]
return tuple(sorted(legs)) if len(legs) == 2 else None
family = events[board["unibet"]["bookmakerFixtureId"]]
prices = defaultdict(list)
for slug in family:
legs = moneyline(board[slug])
if legs:
prices[legs].append(slug)
print(f"{sum(map(len, prices.values()))} feeds quote the moneyline, "
f"{len(prices)} distinct prices\n")
for legs, slugs in sorted(prices.items(), key=lambda kv: sum(1 / p for _, p in kv[0])):
margin = sum(1 / p for _, p in legs) - 1
shown = ", ".join(sorted(slugs)[:6]) + (" ..." if len(slugs) > 6 else "")
print(f"{margin:6.2%} {legs[0][1]:.2f} / {legs[1][1]:.2f} {len(slugs):>2} feed{"s" if len(slugs) > 1 else " "} {shown}")
board_margins = sorted(sum(1 / p for _, p in legs) - 1
for legs in map(moneyline, board.values()) if legs)
print(f"\nWhole board: {len(board_margins)} complete moneylines, "
f"median margin {board_margins[len(board_margins) // 2]:.2%}")
39 feeds quote the moneyline, 10 distinct prices
3.19% 1.83 / 2.06 3 feeds atg.se, leovegas, svenskaspel
3.73% 1.82 / 2.05 2 feeds betrivers, sugarhouse
4.27% 1.81 / 2.04 26 feeds 888sport.it, ballybet, betcity.nl, betmgm.co.uk, betparx, betplay ...
5.06% 1.80 / 2.02 1 feed stake.bet.br
6.18% 1.78 / 2.00 1 feed tabtouch
7.00% 1.77 / 1.98 1 feed betuk
7.32% 1.76 / 1.98 2 feeds kto, kto.bet.br
8.49% 1.74 / 1.96 1 feed leovegas.it
9.69% 1.72 / 1.94 1 feed leovegas.es
12.43% 1.68 / 1.89 1 feed pmu
Whole board: 163 complete moneylines, median margin 5.42%
Thirty-nine feeds collapse to ten prices. Twenty-six of them, Unibet included, quote the platform price at 4.27%, which already beats the 5.42% median across the whole board. Svenska Spel, ATG and LeoVegas shave it to 3.19%. PMU adds eight points on top.
The pattern held across all 22 boards. Inside the family, the main market carried a median 7.5 distinct prices, and the largest identical group held a median 26 feeds. Median margins on the main market (1X2 on soccer, the moneyline elsewhere) across the 22 boards:
| Feed | Median margin | Nations League 1X2 | NHL and SHL moneyline |
|---|---|---|---|
svenskaspel |
4.02% | 3.94% | 3.19% |
atg.se |
4.21% | 3.94% | 3.19% |
unibet.se |
4.38% | 3.97% | 4.27% |
paf |
4.75% | 4.96% | 4.27% |
betrivers |
5.08% | 8.16% | 3.73% |
unibet |
5.27% | 6.82% | 4.27% |
leovegas |
5.60% | 8.10% | 3.19% |
kto |
7.10% | 7.31% | 7.30% |
leovegas.it |
9.72% | 12.27% | 8.67% |
pmu |
12.32% | 10.22% | 12.39% |
On soccer, the Swedish feeds price the Nations League at 3.94% while unibet sits at 6.82%, on the same event ID. And unibet itself moves with the sport: wider than the board median on 6 of 8 Nations League boards, tighter on all 6 NFL boards and all 5 hockey boards.
Across every price the family shared with unibet on those boards, unibet.dk and unibet.ie matched on 5,101 of 5,104. stake.bet.br matched on 41.0% and 888sport.it on 44.1%. Dedupe on the price, per market. Our consensus odds guide shows what to do with the deduplicated prices, and the Bwin API guide runs the same check on the Entain feeds.
Each price in the family also ships a betslip template in the form https://www.unibet.com/betting/sports/event/<event>?coupon={pickType}|<outcome>|{wagerAmounts}|replace. It carries the brand’s own domain, so a best-price table can link each cell straight to the brand that offers the price.
Step 4: read the closing line for three family feeds
The live board shows who reprices now. /v4/historical-odds shows who repriced on the way to kick-off. This step pulls the Manchester derby from 13 September 2026 for unibet, leovegas and svenskaspel in one call (the endpoint takes up to three bookmakers), and prints the first and last active 1X2 before kick-off.
# Reuses get() from Step 1.
FIXTURE = "id1000001772221266" # Man Utd v Man City, 13 Sep 2026
played = get("fixtures", tournamentId=17, **{"from": "2026-09-13", "to": "2026-09-14"})
kickoff = next(f["startTime"] for f in played if f["fixtureId"] == FIXTURE)
hist = get("historical-odds", fixtureId=FIXTURE,
bookmakers="unibet,leovegas,svenskaspel")["bookmakers"]
for slug, book in hist.items():
legs = book["markets"]["101"]["outcomes"] # 1X2: 101 home, 102 draw, 103 away
snaps = {oid: [s for s in o["players"]["0"]
if s["active"] and s["price"] > 1.0 and s["createdAt"] < kickoff]
for oid, o in legs.items()}
open_ = [snaps[o][0]["price"] for o in ("101", "102", "103")]
close = [snaps[o][-1]["price"] for o in ("101", "102", "103")]
moves = sum(sum(a["price"] != b["price"] for a, b in zip(s, s[1:])) for s in snaps.values())
print(f"{slug:<12} open {'/'.join(map(str, open_)):<16} {sum(1/p for p in open_)-1:6.2%}"
f" close {'/'.join(map(str, close)):<16} {sum(1/p for p in close)-1:6.2%}"
f" {moves} price moves")
leovegas open 3.05/3.8/2.1 6.72% close 3.1/3.7/2.14 6.01% 72 price moves
svenskaspel open 3.05/3.8/2.1 6.72% close 3.2/3.85/2.2 2.68% 69 price moves
unibet open 3.05/3.8/2.1 6.72% close 3.15/3.75/2.15 4.92% 70 price moves
All three opened on the identical platform price at 6.72%. Then they split. Unibet closed at 4.92%, LeoVegas at 6.01%, and Svenska Spel at 2.68%. For reference, Pinnacle closed the same fixture at 2.78% after 382 price moves. If you backtest against “the Unibet close”, you are backtesting one brand’s margin policy on top of a shared opening line. Pull the brand you would actually bet at. Our backtesting guide covers the rest of that loop.
What to build with it
- A deduplicated line-shopping table. Group on
bookmakerFixtureId, collapse identical prices, and show one row per distinct price with every brand that offers it. The line shopping guide has the best-price logic. - A platform-weighted consensus. Count the Kambi family once, then add the prices that broke away from it as separate opinions.
- A margin monitor per brand. Track when Svenska Spel or BetRivers cut below the platform price. On this board the cut was worth a full point.
- A live version. The same fixture IDs and slugs run on the WebSocket feed, so the grouping code works on pushed updates without a polling loop.
Unibet API FAQ
How do I get Unibet odds as JSON?
Call /v4/odds with a fixture ID and read the unibet key under bookmakerOdds. OddsPapi carries eight Unibet feeds: unibet, unibet.be, unibet.com.au, unibet.dk, unibet.fr, unibet.ie, unibet.nl and unibet.se. All eight quoted every one of the 22 boards we measured on 28 September 2026.
Which bookmakers share Unibet’s odds platform?
On 21 of 22 boards measured on 28 September 2026, 39 feeds shared Unibet’s event ID, including LeoVegas, BetRivers, SugarHouse, Paf, Svenska Spel, ATG, 888sport Italy, BetMGM UK, Grosvenor, Virgin Bet, PMU and KTO. Group the /v4/odds board on bookmakerFixtureId to rebuild the list on any fixture.
Does BetRivers use the same odds as Unibet?
BetRivers shares Unibet’s event ID and matched Unibet on 93.1% of shared prices across 21 boards. It sets its own margin on some markets: on an NHL moneyline it quoted 3.73% against Unibet’s 4.27%.
Is unibet.fr the same feed as unibet?
No. unibet.fr uses a separate event ID, shared with FDJ and ZEbet, on all 22 boards measured. Treat it as its own book.
Can I get historical Unibet odds?
Yes. /v4/historical-odds returns the full price history for unibet and the other family feeds, up to three bookmakers a call, on the free tier. On the 13 September 2026 Manchester derby, Unibet made 70 price moves on the 1X2 before kick-off.
Which Unibet-platform feed has the lowest margin?
Svenska Spel, with a median 4.02% on the main market across 22 boards measured on 28 September 2026, ahead of ATG at 4.21% and unibet.se at 4.38%. The unibet feed ran at 5.27%.
Pull Unibet and the other 38 feeds
Every call in this guide runs on the free key: 350+ bookmakers in the catalogue, free historical odds for your closing-line checks, and the WebSocket feed when polling gets too slow. Get a free API key and run Step 2 on the next fixture you care about.