Bundesliga Odds API: Tiptorro Is the Tightest German Book
The 2026/27 Bundesliga kicks off on August 28. Eleven days out, the board is already deep: 187 bookmakers on the median matchday 1 fixture, 640 distinct market IDs across the round, Pinnacle and SBOBet both pricing it.
The Premier League, Serie A and Ligue 1 all play that same weekend. Their fixtures are round 2, and round 2 is not open yet.
This guide pulls live Bundesliga odds in Python from the free OddsPapi tier, and then does the part most tutorials skip: it shows you how to tell whether the prices you just fetched are worth anything.
Why the German board is deeper than the English one this week
Sportsbooks price a season opener months ahead of the fixtures that follow it. Pinnacle has been quoting Bayern v Stuttgart since July 6, 53 days before kickoff. The Bundesliga starts a week later than the other big-five leagues, so on the weekend of August 28 its round 1 sits next to everyone else’s round 2. Books open one round at a time, and the opener is the exception they price early.
Every fixture on that weekend returns hasOdds: true. The flag tells you a price exists. It says nothing about how many books wrote one, and nothing about whether the number is real. We hit the same wall on the NFL schedule feed, where 123 of 132 fixtures reported true and the board thinned from 18 books to 3 across three weeks.
So the Bundesliga opener gives you a 187-book board at a horizon where the leagues around it are still closed. It also gives you a board priced like it is 11 days out, which is a different thing from a board priced like it is tomorrow.
Old way vs OddsPapi
| What you want | Scraping Bundesliga books | OddsPapi |
|---|---|---|
| Coverage | One book per scraper, per layout change | 350+ bookmakers in the catalogue, 187 on a single fixture |
| Sharp prices | Pinnacle blocks most retail traffic | pinnacle and sbobet in the same payload |
| Prediction markets | Separate API, separate auth, share prices not odds | kalshi and polymarket as decimal odds |
| Stake depth | Not published anywhere | limit and exchangeMeta ladders |
| Price history | Build your own recorder, wait a season | Free /historical-odds, back to July 6 |
| Asian handicaps | Parse each book’s own line format | Native marketName + handicap pairs |
Step 1: Authenticate
The key travels as a query parameter. Not a header. Grab a free one and this whole tutorial runs on it.
import time, requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def op_get(path, **params):
params["apiKey"] = API_KEY
for attempt in range(6):
r = requests.get(f"{BASE_URL}/{path}", params=params, timeout=30)
if r.status_code == 429:
wait = r.json()["error"].get("retryMs", 1500) / 1000
time.sleep(wait + 0.3)
continue
if r.status_code == 404:
return None # empty fixture window
r.raise_for_status()
time.sleep(1.0) # same-endpoint cooldown
return r.json()
raise RuntimeError(f"rate limited on /{path}")
print(len(op_get("sports"))) # 69
Two things in that helper earn their keep. A 429 response carries a JSON body with retryMs, so the API tells you exactly how long to wait. And an empty fixture window returns 404, not an empty list, so a bare raise_for_status() kills any loop that crosses an off-season month.
Step 2: Resolve the Bundesliga
Two tournaments in the catalogue are called Bundesliga. One is German, one is Austrian. Match on the category as well as the name, then hardcode the ID you get back.
def find_tournament(sport_id, tournament_name, category_name):
for t in op_get("tournaments", sportId=sport_id):
if t["tournamentName"] == tournament_name and t.get("categoryName") == category_name:
return t
raise LookupError(f"{tournament_name} / {category_name} not found")
bl = find_tournament(10, "Bundesliga", "Germany")
print(bl["tournamentId"], bl["tournamentSlug"], bl["futureFixtures"])
# 35 bundesliga 35
dupes = [t for t in op_get("tournaments", sportId=10) if t["tournamentName"] == "Bundesliga"]
print([(t["tournamentId"], t["categoryName"]) for t in dupes])
# [(35, 'Germany'), (45, 'Austria')]
Soccer carries 1,762 tournaments on sportId=10. Name collisions are the rule, not the exception: 35 rows are called “Premier League”. Germany is tournamentId 35.
Step 3: Find the round that is actually priced
tournamentId filters /fixtures server-side. It is not in the published parameter list and it works, cutting the payload about twentyfold on a league pull.
def priced_fixtures(tournament_id, start, end):
fixtures = op_get("fixtures", sportId=10, tournamentId=tournament_id,
**{"from": start, "to": end}) or []
out = [f for f in fixtures if f.get("hasOdds")]
return sorted(out, key=lambda f: f["startTime"])
board = priced_fixtures(35, "2026-08-17", "2026-09-08")
print(len(board)) # 9
for f in board[:3]:
print(f["startTime"][:16], f["participant1Name"], "v", f["participant2Name"])
# 2026-08-28T18:30 Bayern Munich v VfB Stuttgart
# 2026-08-29T13:30 FSV Mainz v SC Paderborn 07
# 2026-08-29T13:30 Union Berlin v Eintracht Frankfurt
Nine priced fixtures out of 16 in that window. All nine are matchday 1. The September round returns hasOdds: false, so books open one round at a time.
One trap on the date range: to is a midnight-UTC instant, not a whole day. Set it to the day after your last fixture or you will lose the final day’s games and conclude the league is dead.
Step 4: Parse the board
The odds payload nests five levels deep, and two guards decide whether your numbers mean anything.
from collections import defaultdict
def three_way(fixture_id):
data = op_get("odds", fixtureId=fixture_id)
rows = {}
for slug, book in data["bookmakerOdds"].items():
if book.get("suspended"): # book pulled the market, still returns prices
continue
market = book["markets"].get("101") # Full Time Result
if not market:
continue
prices = {}
for outcome_id, outcome in market["outcomes"].items():
price = outcome["players"].get("0") # game lines live under "0"
if price and price.get("active") and price.get("price"):
prices[outcome_id] = price["price"]
if len(prices) == 3:
rows[slug] = prices
return rows
board_101 = three_way("id1000003572513148")
# books shipping a complete, active three-way on most of the round
seen = defaultdict(int)
for f in board:
for slug in three_way(f["fixtureId"]):
seen[slug] += 1
print(sum(1 for slug, n in seen.items() if n >= 7)) # 171
The payload carries 187 books on the median fixture, 183 at the low end and 188 at the high end, and 85.9% of the prices come back active. After the two guards, 171 books ship a complete, active three-way on at least seven of the nine fixtures.
Two books show you why the guards exist. BetRivers arrives with suspended: true and prices still attached. Bet365 quotes the draw and the away side and leaves the home price out, so it fails the three-outcome check.
The active flag lives on the price object at players["0"], not on the outcome. An outcome dict has exactly one key, players. Checking outcome["active"] gets you None forever, and every dead line stays in your dataset.
Market 101 is Full Time Result. Outcomes are 101 home, 102 draw, 103 away.
Step 5: 1,537 quotes, 871 opinions
Several slugs quote byte-identical prices because they run one trading feed behind several brands. Count them separately and your “consensus” triple-weights one desk.
def dedupe(rows):
groups = defaultdict(list)
for slug, prices in rows.items():
groups[tuple(sorted(prices.items()))].append(slug)
return groups
for prices, slugs in dedupe(board_101).items():
if len(slugs) > 1:
print("identical:", slugs)
# identical: ['betmgm', 'borgata']
# identical: ['betparx', 'ballybet', 'fourwinds']
# identical: ['caesars', 'williamhill']
# ... (first three groups shown)
quotes = independent = 0
for f in board:
rows = three_way(f["fixtureId"])
quotes += len(rows)
independent += len(dedupe(rows))
print(quotes, independent, f"{(1 - independent / quotes) * 100:.1f}%")
# 1537 871 43.3%
Across the round, 1,537 slug-quotes collapse to 871 independent ones. That is a 43.3% collapse rate: two of every five prices you fetch are a copy of a price you already have. The BetMGM, BetParx and Caesars groups showed up on La Liga and on MLS before that. None of them carry a cloneOf flag in the catalogue, so the flag will not do this job for you. Dedupe on the price tuple, per fixture.
Step 6: The margin ladder
def margin(prices):
return (sum(1 / p for p in prices.values()) - 1) * 100
for slug, prices in sorted(board_101.items(), key=lambda kv: margin(kv[1])):
print(f"{slug:18s} {margin(prices):6.2f}%")
Run that across all nine fixtures and take each book’s median. Here is the top of the 171-book field, plus the reference books most readers care about:
| Book | Median 1X2 margin | Rank of 171 |
|---|---|---|
1xbet |
1.55% | 1 |
betfair-ex |
1.86% | 2 |
kalshi |
1.99% | 3 |
polymarket.us |
2.01% | 4 |
polymarket |
2.02% | 5 |
duel |
2.11% | 6 |
atg.se |
2.78% | 7 |
betcity.nl |
2.78% | 8 |
pinnacle |
3.67% | 17 |
bet365 |
7.65% | 125 |
draftkings |
9.34% | 157 |
sbobet |
10.23% | 161 |
Pinnacle ranks 17th and every book above it is an exchange, a prediction market or a crypto-facing book. That ordering is a margin ranking and nothing more. Step 7 shows what happens when you try to bet the top of it.
Now zoom in on the opener itself. These are the live three-way prices on Bayern v Stuttgart for the sharps, the exchanges and the US books:
| Book | Bayern | Draw | Stuttgart | Margin |
|---|---|---|---|---|
| kalshi | 1.333 | 7.692 | 10.00 | -1.98% |
| polymarket | 1.299 | 6.667 | 9.091 | 2.98% |
| betmgm = borgata | 1.32 | 6.50 | 7.25 | 4.94% |
| pinnacle | 1.296 | 6.05 | 7.85 | 6.43% |
| caesars = williamhill | 1.294 | 5.75 | 8.50 | 6.44% |
| pointsbet.com.au | 1.29 | 6.00 | 8.00 | 6.69% |
| betparx = ballybet = fourwinds | 1.27 | 6.00 | 8.50 | 7.17% |
| hardrockbet | 1.25 | 6.50 | 7.50 | 8.72% |
| fanduel | 1.24 | 6.00 | 8.00 | 9.81% |
| sbobet | 1.28 | 5.60 | 6.70 | 10.91% |
| draftkings | 1.25 | 5.50 | 7.00 | 12.47% |
Pinnacle charges 6.43% on this fixture against a 3.67% median across the round. Bayern at 1.296 is the most lopsided price on the matchday, and the sharp widens on it. Read the whole German board and it is priced wide at this horizon. The reason is in Step 8.
Now read the top of the opener table. Kalshi’s three prices sum to 98.02%. A negative margin means the book is offering more than 100% back across its own outcomes, which is supposed to be impossible.
Step 7: The negative margin is worth 69 cents
Kalshi’s own three prices sum under 100%, so backing all three in the right proportion collects no matter what happens in Munich.
Then you read the ladder.
def back_ladder(price_obj):
meta = price_obj.get("exchangeMeta") or {}
return meta.get("back") or []
def lockable(prices, capacities):
"""Largest total stake that fills every side at the quoted price."""
overround = sum(1 / p for p in prices.values())
shares = {o: (1 / p) / overround for o, p in prices.items()}
total = min(capacities[o] / shares[o] for o in prices)
stakes = {o: total * shares[o] for o in prices}
payout = min(stakes[o] * prices[o] for o in prices)
return total, stakes, payout
data = op_get("odds", fixtureId="id1000003572513148")
market = data["bookmakerOdds"]["kalshi"]["markets"]["101"]
prices = {o: market["outcomes"][o]["players"]["0"]["price"] for o in ("101", "102", "103")}
capacity = {o: back_ladder(market["outcomes"][o]["players"]["0"])[0]["limit"]
for o in prices}
print(prices, f"{margin(prices):.2f}%")
print({o: round(c, 2) for o, c in capacity.items()})
total, stakes, payout = lockable(prices, capacity)
print(f"max stake ${total:.2f} -> payout ${payout:.2f} -> profit ${payout - total:.2f}")
# {'101': 1.333, '102': 7.692, '103': 10.0} -1.98%
# {'101': 375.2, '102': 78.39, '103': 3.5}
# max stake $34.31 -> payout $35.00 -> profit $0.69
The Stuttgart side holds $3.50. That single rung caps the whole position at $34.31 of stake for a locked profit of 69 cents, before you pay a single fee or wait 11 days for settlement.
Take one more step down the ladder and the edge evaporates:
def ladder_average(price_obj):
rungs = back_ladder(price_obj)
stake = sum(r["limit"] for r in rungs)
return sum(r["limit"] * r["price"] for r in rungs) / stake
full = {o: ladder_average(market["outcomes"][o]["players"]["0"]) for o in prices}
print({o: round(v, 4) for o, v in full.items()}, f"{margin(full):.2f}%")
# {'101': 1.3113, '102': 7.5709, '103': 8.3749} 1.41%
Sweep all three rungs on every side and the average price gives Kalshi a +1.41% margin. The negative number describes the top of an empty book, and the top of that book is $3.50 deep.
This is not a Kalshi-specific quirk. Across the nine Bundesliga fixtures its three-way margin ranges from -1.98% to +14.00%, and the two negative readings sit on the two thinnest ladders in the set ($3.50 and $3.30 on the binding side). Polymarket runs the opposite way: 60 markets on every fixture, margins banded between 2.98% and 5.01%, thinnest top rung between $56 and $141.
| Fixture | Kalshi margin | Thinnest Kalshi rung | Polymarket margin |
|---|---|---|---|
| Bayern v Stuttgart | -1.98% | $3.50 | 2.98% |
| Dortmund v Hamburger SV | -0.01% | $3.30 | 3.02% |
| Elversberg v Leverkusen | 1.01% | $5.55 | 5.01% |
| Leipzig v Monchengladbach | 3.02% | $6.12 | 4.02% |
| Union Berlin v Frankfurt | 4.01% | $11.48 | 5.00% |
| Cologne v Hoffenheim | 5.00% | $14.50 | 3.99% |
| Freiburg v Werder Bremen | 6.01% | $10.92 | 4.00% |
| Augsburg v Schalke 04 | 13.01% | $11.10 | 4.00% |
| Mainz v Paderborn | 14.00% | $9.80 | 3.00% |
Score an exchange quote on the depth behind it, never on the price itself. That rule applies to every book above Pinnacle in the Step 6 ranking. Our Kalshi and Polymarket comparison covers how the two venues differ under the hood, and the betting limits guide covers the same question for sportsbooks.
Step 8: Read price maturity off Pinnacle’s limit
Pinnacle publishes a limit on every outcome, and that limit is a capped maximum win rather than a capped stake. Divide it back out and you recover a per-market base figure. The base is the book’s own statement of how much it wants on the number.
def limit_base(price_obj):
limit = price_obj.get("limit")
if not limit:
return None
return limit if price_obj["price"] >= 2 else limit * (price_obj["price"] - 1)
pin = data["bookmakerOdds"]["pinnacle"]["markets"]["101"]
print([round(limit_base(pin["outcomes"][o]["players"]["0"]), 1)
for o in ("101", "102", "103")])
# [249.8, 250.0, 250.0]
Two hundred and fifty dollars. Run the same three lines against the openers that kick off four and five days out:
| League opener | Days to kickoff | Pinnacle base | Pinnacle margin |
|---|---|---|---|
| Premier League | 4 | $1,500 | 4.20% |
| Serie A | 5 | $1,000 | 4.35% |
| Ligue 1 | 4 | $750 | 3.73% |
| Bundesliga | 11 | $250 | 6.43% |
Base and margin move together, and both track the horizon rather than the league. A $250 base is Pinnacle’s placeholder figure. We measured the same number on the Premier League opener 63 days out and watched it double to $500, and on the La Liga opener it went from $250 to $1,500 over six weeks while the price moved a single tick.
The free history endpoint shows how long that has been true here:
hist = requests.get(f"{BASE_URL}/historical-odds",
params={"apiKey": API_KEY,
"fixtureId": "id1000003572513148",
"bookmakers": "pinnacle"}, timeout=120).json()
snaps = hist["bookmakers"]["pinnacle"]["markets"]["101"]["outcomes"]["101"]["players"]["0"]
changes = [s for i, s in enumerate(snaps) if i == 0 or s["price"] != snaps[i - 1]["price"]]
print(len(snaps), len(changes))
print(changes[0]["createdAt"][:16], changes[0]["price"], round(limit_base(changes[0]), 1))
print(changes[-1]["createdAt"][:16], changes[-1]["price"], round(limit_base(changes[-1]), 1))
# 40 27
# 2026-07-06T07:43 1.211 249.8
# 2026-08-15T23:01 1.296 249.8
Forty snapshots, 27 price changes, Bayern drifting from 1.211 out to 1.296 over 41 days. The base has not moved off $250 once. Pinnacle has an opinion on this game and it does not yet want size on it.
Note the shape difference: on /historical-odds the top key is bookmakers, not bookmakerOdds, and players["0"] holds a list of snapshots rather than one price object. Three bookmakers per call, maximum.
Step 9: De-vig and shop the price
With the margin stripped out, Pinnacle’s numbers become a fair-price reference. The power method solves for the exponent that makes the implied probabilities sum to one.
def power_devig(prices):
implied = [1 / p for p in prices]
lo, hi = 0.5, 3.0
for _ in range(100):
k = (lo + hi) / 2
if sum(i ** k for i in implied) > 1:
lo = k
else:
hi = k
k = (lo + hi) / 2
return [i ** k for i in implied]
order = ["101", "102", "103"]
fair = power_devig([board_101["pinnacle"][o] for o in order])
print([round(1 / f, 3) for f in fair], [round(f, 4) for f in fair])
# [1.327, 7.118, 9.455] [0.7537, 0.1405, 0.1058]
for outcome_id, label in zip(order, ["Bayern", "Draw", "Stuttgart"]):
quotes = sorted(((p[outcome_id], s) for s, p in board_101.items()), reverse=True)
print(f"{label:10s} best {quotes[0][0]} @ {quotes[0][1]}"
f" runner-up {quotes[1][0]} @ {quotes[1][1]}")
Pinnacle’s fair line reads Bayern 75.4%, draw 14.1%, Stuttgart 10.6%. Run the best-price loop over the deduped board and the top quote on each outcome comes from an exchange or a prediction market, which is what the Step 6 ranking predicts. Step 7 already told you what those top-of-book prices are worth, so pull exchangeMeta.back alongside every one of them and read the runner-up as the price a normal-sized bettor can take.
These are best available prices, not value bets. Shopping 871 independent quotes recovers the margin and stops there. The no-vig guide compares three de-vig methods and consensus odds builds a fair line from the whole board instead of one book.
Step 10: Handicaps and totals, resolved by name
Every handicap and every goal line has its own market ID. There is no single “Asian handicap” market to fetch. Resolve by marketName plus handicap and take whatever the book actually quotes.
catalog = op_get("markets", sportId=10)
market_name = {m["marketId"]: (m["marketName"], m.get("handicap")) for m in catalog}
def lines(book, family):
found = []
for market_id, market in book["markets"].items():
name, handicap = market_name.get(int(market_id), ("", None))
if name != family:
continue
prices = [o["players"]["0"] for o in market["outcomes"].values() if "0" in o["players"]]
if len(prices) == 2 and all(p.get("active") for p in prices):
odds = [p["price"] for p in prices]
found.append((handicap, odds, round((sum(1 / o for o in odds) - 1) * 100, 2)))
return sorted(found, key=lambda r: r[0])
for handicap, odds, marg in lines(data["bookmakerOdds"]["pinnacle"], "Asian Handicap"):
print(f"{handicap:>6} {odds} {marg:>6.2f}%")
| Handicap | Pinnacle prices | Margin |
|---|---|---|
| -2.75 | 3.19 / 1.349 | 5.48% |
| -2.5 | 2.70 / 1.46 | 5.53% |
| -2.25 | 1.555 / 2.45 | 5.13% |
| -2 | 2.19 / 1.68 | 5.19% |
| -1.75 | 1.943 / 1.892 | 4.32% |
| -1.5 | 1.751 / 2.09 | 4.96% |
| -1.25 | 1.581 / 2.39 | 5.09% |
| -1 | 1.406 / 2.91 | 5.49% |
| -0.75 | 3.25 / 1.338 | 5.51% |
Nine rungs, and the margin traces a U. The cheapest line is -1.75 at 4.32%, which is where Pinnacle thinks the game sits. Push out to the wings and you pay a point more. The base limit stays at $250 on every rung, so the raw limit swings from 250 to 739 as price arithmetic.
SBOBet quotes two rungs and inverts its own pricing:
print(lines(data["bookmakerOdds"]["sbobet"], "Asian Handicap"))
# [(-1.75, [1.935, 1.955], 2.83), (-1.5, [2.18, 1.735], 3.51)]
SBOBet charges 10.91% on the three-way of this fixture, and 2.83% on the handicap. It beats Pinnacle’s best rung by a point and a half. That split now holds on a sixth competition, and we took it apart in why sharps bet the Asian handicap. If you want to settle quarter lines like -1.75 correctly, the handicap calculator has the split-stake maths.
Availability is the first filter. Pinnacle walks nine rungs, DraftKings eight, SBOBet two, and plenty of books on this board quote no full-time handicap at all.
What the German board does not have
Four honest limits, all measured on the same nine fixtures.
No German book beats Pinnacle. Twelve locally licensed brands priced every one of the nine matchday 1 fixtures. tiptorro is the tightest of them at a 4.82% median 1X2 margin, rank 28 of the 171 books that quoted a complete three-way on most of the round. Pinnacle sits at 3.67%, rank 17, so the tightest German book is still 1.15 points behind the sharp. A best-price scan on Bundesliga sides lands on the exchanges and the sharps, not on tipico or bwin.de. The bottom of the German list is worse than anything else on the board bar one book: betway.de posts 17.36%, rank 170 of 171.
| Book | Median 1X2 margin | Rank of 171 |
|---|---|---|
pinnacle |
3.67% | 17 |
tiptorro |
4.82% | 28 |
bet3000 |
4.95% | 31 |
wettarena |
4.95% | 32 |
betano.de |
4.96% | 33 |
bwin.de |
4.96% | 34 |
oddset |
5.44% | 58 |
tipico |
5.54% | 62 |
winamax.de |
6.90% | 103 |
bet365.de |
7.48% | 120 |
cashpoint |
7.99% | 135 |
888sport.de |
8.53% | 142 |
betway.de |
17.36% | 170 |
Two catalogue entries never showed up. interwetten and merkurbets are listed in /v4/bookmakers and appeared on none of the nine fixtures. Being in the catalogue does not put a book in the payload, so probe /odds for the slug you need before you build against it.
Corners are thin here. The opener carries 58 corner markets, quoted by seven slugs that dedupe to three independent feeds: the BetParx group, the BetMGM pair, and Polymarket. Pinnacle prices corners on La Liga and skips them on the Bundesliga, so check the competition you care about rather than assuming.
Zero card markets. Nothing on bookings, on either team, on any book.
No player props on the opening round. Every outcome on all nine fixtures is keyed players["0"]. Prop menus fill in closer to kickoff, and they come from US retail books rather than from the sharps.
The three-signal screen
Put the pieces together and you can grade any board in about ten lines, whichever league it is.
| Signal | Where it lives | Placeholder board | Traded board |
|---|---|---|---|
| Sharp appetite | limit on Pinnacle, divided back to a base |
$250 | $1,000 and up |
| Cross-book spread | margin across deduped quotes | the sharp sits well above its own round median | the sharp sits at or below its round median |
| Exchange depth | exchangeMeta.back[0].limit |
single dollars | hundreds and up |
By all three, the Bundesliga opener on August 17 is a board that exists rather than a board that trades. Pinnacle holds a $250 base, charges 6.43% against a 3.67% round median, and the best three-way price on the board is backed by $3.50. That is worth knowing before you point a scanner at it. Re-pull it on August 27 and every one of those three numbers will have moved.
Going further
Polling gets you a snapshot. The WebSocket feed pushes changes as books make them, which is what you want once the Bundesliga round is live and lines move on a goal. Our WebSocket guide covers the connection, and the line shopping tutorial scales the best-price function in Step 9 across a whole slate.
For the other big-five leagues, we have the same treatment on the Premier League and La Liga, and a sport-wide walkthrough in the football odds API guide.
Get a key
Everything above ran on the free tier: 69 sports, 350+ bookmakers in the catalogue, 187 of them on the median Bundesliga fixture, and full price history back to July 6 on a game that has not kicked off. No sales call, no trial clock.
Get your free OddsPapi API key and pull the Bundesliga board yourself.
FAQ
What is the tournament ID for the Bundesliga?
Germany’s Bundesliga is tournamentId 35 on sportId 10. Austria’s Bundesliga is 45. Match on categoryName as well as tournamentName when you resolve it, because soccer carries 1,762 tournaments and names repeat.
How many bookmakers price a Bundesliga match?
187 on the median matchday 1 fixture, 183 at the low end and 188 at the high end, including Pinnacle, SBOBet, Bet365, DraftKings, FanDuel, Kalshi and Polymarket. 171 of them ship a complete, active three-way on at least seven of the nine fixtures. Dedupe first: 1,537 slug-quotes across the round collapse to 871 independent ones, a 43.3% collapse rate, because BetMGM/Borgata, Caesars/WilliamHill and the BetParx group each run one feed across several brands.
Can I get Bundesliga odds for free?
Yes. The free OddsPapi tier covers live odds, the full fixture list and /historical-odds price history with no credit card. Rate limits apply per endpoint, so leave about a second between calls to the same one and honour the retryMs value in any 429 response.
Why do the odds sometimes sum to less than 100%?
On prediction markets, a three-way that sums under 100% means the top rung of each ladder is priced generously and holds almost no money. Kalshi showed -1.98% on the Bundesliga opener, and the binding side held $3.50, which capped the locked position at 69 cents of profit. Read exchangeMeta.back before you treat any such quote as tradeable.
Does the API cover Bundesliga player props?
Not on the opening round. Every outcome across the nine priced fixtures sits under the players["0"] key, which is the game-line shape. Prop markets are keyed by player ID and appear closer to kickoff, mostly from US retail books.
Which German bookmakers are in the feed?
Twelve German-licensed books priced all nine matchday 1 fixtures: 888sport.de, bet3000, bet365.de, betano.de, betway.de, bwin.de, cashpoint, oddset, tipico, tiptorro, wettarena and winamax.de. None of them is your best price. tiptorro leads the group at a 4.82% median 1X2 margin, rank 28 of 171, against Pinnacle’s 3.67% at rank 17. betway.de trails at 17.36%, rank 170 of 171. interwetten and merkurbets sit in the catalogue and quoted none of the nine fixtures.