La Liga Odds API: 192 Books and No Spanish Book Beats Pinnacle

La Liga Odds API - OddsPapi API Blog
How To Guides August 20, 2026

LaLiga does not publish an odds API. The league sells data rights to Sportradar and Genius, and those contracts start at a sales call and a five-figure minimum. If you want Spanish football prices in JSON today, you have two realistic options: scrape a dozen sportsbooks and maintain a dozen parsers, or pull one aggregated feed.

This guide takes the second route. Every number below came off the live OddsPapi API on August 11, 2026, four days before the 2026/27 season opened. All nine code blocks ran end to end before this post went out.

What the Spanish board actually looks like

La Liga is the first of Europe’s big five to price up this season. On August 11 the Spanish opening round already carried odds on all eight fixtures. The English, Italian and French openers do not start until August 21, and the Bundesliga waits until August 28. If you are testing a soccer pipeline this week, La Liga is the only major league with a full board on it.

Opening-weekend depth, measured across all eight fixtures:

Metric Value
Fixtures with odds 8 of 8
Bookmakers per fixture 16 to 17 (10 on the round-two game)
Markets per fixture 12 (Kalshi) to 131 (BallyBet)
Individual prices in the sample 31,811, of which 94.8% are active
Sharp coverage Pinnacle on 7 of 8, SBOBet on 8 of 8
Prediction markets Kalshi on 8 of 8, Polymarket on 7 of 8

The prediction-market row is the new part. Three weeks ago the same census on the Premier League board returned zero Kalshi and zero Polymarket quotes. Kalshi now runs a dedicated La Liga game series, and the payload carries a deep link to it in fixturePath.

Old way vs OddsPapi

Job Scraping or an enterprise feed OddsPapi
Getting Pinnacle on a Spanish game No public account, no public API bookmakers=pinnacle
Adding a 17th book Write and babysit a 17th parser Already in the same JSON object
Asian handicap ladders Reverse-engineer each book’s line format Native marketName plus handicap
Corner markets Usually missing from generic feeds 16 to 55 corner markets per book
Price history Paid add-on, or you build a recorder Free /historical-odds back to the opening line
Cost to start Sales call Free tier, key in a minute

Step 1: Authenticate and find LaLiga

The API key rides as a query parameter on every call. It is never a header.

import requests, time, collections

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"

def api(path, **params):
    """One GET with the 429 retry the free tier asks for."""
    for _ in range(6):
        r = requests.get(f"{BASE_URL}/{path}", params={"apiKey": API_KEY, **params})
        if r.status_code == 200:
            return r.json()
        wait = r.json().get("error", {}).get("retryMs", 2000)
        time.sleep(wait / 1000 + 0.3)
    raise RuntimeError(f"{path} failed")

tours = api("tournaments", sportId=10)
laliga = [t for t in tours
          if t["tournamentName"] == "LaLiga" and t["categoryName"] == "Spain"]
TID = laliga[0]["tournamentId"]
print(TID, laliga[0]["futureFixtures"], "future fixtures")
# 8 110 future fixtures

Soccer carries 1,762 tournaments. Search on a substring and you will drown: 236 rows contain “liga” and Spain alone lists twelve, including LaLiga 2, Primera Federacion and Segunda Federacion. Match the exact name and the category, then hardcode the ID you get back. La Liga is tournamentId 8.

Note the rate-limit handler. The free tier limits per endpoint and returns a real HTTP 429 whose body tells you the exact wait in retryMs. A 429 body is valid JSON, so code that only checks for a bookmakerOdds key reads a rate limit as “this fixture has no odds”.

Step 2: Pull the round

fixtures = api("fixtures", sportId=10, **{"from": "2026-08-11", "to": "2026-08-21"})
board = sorted([f for f in fixtures if f["tournamentId"] == TID and f["hasOdds"]],
               key=lambda f: f["startTime"])

for f in board:
    print(f"{f['startTime'][:16]}  {f['participant1Name']} v {f['participant2Name']}  {f['fixtureId']}")
2026-08-15T17:30  Deportivo Alaves v Getafe CF          id1000000872478446
2026-08-15T19:30  Sevilla FC v Rayo Vallecano           id1000000872478462
2026-08-16T15:00  Racing Santander v Villarreal CF      id1000000872478458
2026-08-16T17:00  Espanyol Barcelona v Levante UD       id1000000872478454
2026-08-16T19:00  RC Deportivo De A Coruna v Elche CF   id1000000872478452
2026-08-16T19:30  RC Celta de Vigo v CA Osasuna         id1000000872478450
2026-08-18T07:00  Atletico Madrid v Malaga CF           id1000000872478448
2026-08-20T19:00  Rayo Vallecano v Deportivo Alaves     id1000000872478502

Two traps live in that call. The to parameter is a midnight-UTC instant, not a whole day. Ask for from=2026-08-15&to=2026-08-15 and you get only the games kicking off at exactly 00:00Z, which looks like an empty league. Always set to one day past the last day you want. The window also caps at ten days.

The second trap is hasOdds. Books open a league one round at a time. The round starting August 31 has eleven La Liga fixtures scheduled and zero with prices on them. Filter on hasOdds before you spend a call on /odds.

Step 3: Read one fixture’s board

The odds payload nests four levels deep: bookmaker, market, outcome, then a players dict that holds the price. On game lines the only player key is "0". On player props the same dict is keyed by player ID, which is why hardcoding players["0"] makes every prop market look empty.

FIXTURE = "id1000000872478462"   # Sevilla FC v Rayo Vallecano, Aug 15 19:30 UTC
odds = api("odds", fixtureId=FIXTURE)

def live_books(payload):
    """Drop books that pulled the market but still return prices."""
    return {slug: b for slug, b in payload["bookmakerOdds"].items()
            if not b.get("suspended")}

def quote(book, market_id):
    """{outcome_id: price} for the active side of one market."""
    market = book["markets"].get(str(market_id))
    if not market:
        return {}
    out = {}
    for oid, outcome in market["outcomes"].items():
        price = outcome["players"].get("0")
        if price and price.get("active") is not False and price.get("price"):
            out[oid] = price["price"]
    return out

def margin(prices):
    return sum(1 / p for p in prices.values()) - 1 if len(prices) >= 2 else None

MONEYLINE = 101          # Full Time Result: 101 home, 102 draw, 103 away

rows = []
for slug, book in live_books(odds).items():
    q = quote(book, MONEYLINE)
    if len(q) == 3:
        rows.append((margin(q), slug, q))

for m, slug, q in sorted(rows):
    print(f"{slug:18s} {m * 100:5.2f}%  {q['101']:6.3f} / {q['102']:6.3f} / {q['103']:6.3f}")

Two guards matter there. book["suspended"] flags a bookmaker that has pulled the market while its last prices still ship in the response, and active lives on the price object rather than the outcome, so outcome["active"] is always undefined.

Bookmaker Margin Sevilla Draw Rayo
kalshi 1.00% 2.326 3.333 3.571
polymarket 2.00% 2.381 3.226 3.448
pinnacle 3.44% 2.350 3.280 3.290
hardrockbet 5.64% 2.200 3.250 3.400
ballybet 5.82% 2.160 3.150 3.600
betmgm 6.00% 2.250 3.200 3.300
caesars 6.04% 2.300 3.100 3.300
pointsbet.com.au 6.11% 2.250 3.100 3.400
fanduel 6.23% 2.150 3.300 3.400
draftkings 6.46% 2.250 3.200 3.250
bet365 9.54% 2.150 3.100 3.250
sbobet 10.15% 2.330 2.960 2.990

Kalshi and Polymarket sit at the top of that table on seven of the eight fixtures. Pinnacle ran 3.23% to 4.41% across the round, the US retail pack clustered at 5% to 7%, and SBOBet posted 10.15% to 10.33% on every single game. SBOBet’s three-way is the worst price on the board and its handicap is one of the best, which is a habit it repeats league after league. The margin study on that split covers why.

Step 4: Dedupe before you average anything

Several slugs quote the same feed. Group on the price tuple and the field shrinks.

groups = collections.defaultdict(list)
for _, slug, q in rows:
    groups[tuple(sorted(q.items()))].append(slug)

print(len(rows), "slugs ->", len(groups), "independent quotes")
for tup, slugs in groups.items():
    if len(slugs) > 1:
        print("  same feed:", ", ".join(slugs))
17 slugs -> 12 independent quotes
  same feed: betmgm, borgata
  same feed: betparx, ballybet, betrivers, fourwinds
  same feed: caesars, williamhill

BetParx, BallyBet, BetRivers and FourWinds shipped byte-identical prices on all eight fixtures. Every one of them reports cloneOf: null in the bookmaker catalogue, so the flag will not save you. Averaging the raw seventeen gives that one feed four votes and Pinnacle one. The consensus-odds guide goes further on weighting.

Market count is not liquidity

Polymarket lists 60 two-sided markets on Sevilla v Rayo, which beats Bet365 (34) and roughly matches DraftKings (62). Read the market count alone and you would call Polymarket a deep book on Spanish football. The order book says otherwise.

Exchange quotes carry an exchangeMeta ladder with the stake available at each level. Sum it and you get the money behind the price.

def exchange_depth(book, market_id):
    """Stake you can actually get on, per outcome, from the order book."""
    market = book["markets"].get(str(market_id), {})
    depth = {}
    for oid, outcome in market.get("outcomes", {}).items():
        price = outcome["players"].get("0")
        meta = (price or {}).get("exchangeMeta") or {}
        depth[oid] = sum(level.get("limit") or 0 for level in meta.get("back", []))
    return depth

pm = odds["bookmakerOdds"]["polymarket"]
for market_id, label in [(101, "Full Time Result"), (104, "Both Teams To Score"),
                         (10803, "Corners O/U 9.5"), (101740, "2H Corners O/U 5.5")]:
    q = quote(pm, market_id)
    d = exchange_depth(pm, market_id)
    if q:
        print(f"{label:22s} margin {margin(q) * 100:6.2f}%   thinnest side ${min(d.values()):,.0f}")
Full Time Result       margin   2.00%   thinnest side $5,240
Both Teams To Score    margin   2.00%   thinnest side $5,004
Corners O/U 9.5        margin   2.99%   thinnest side $854
2H Corners O/U 5.5     margin  90.02%   thinnest side $96

Run that across all 60 markets and the shape is stark. Median margin is 6.5%. Twenty-seven markets price inside 5%, fifteen price wider than 50%, and the median thinnest side holds $96 of stake. Nine markets clear both bars, under 5% margin and at least $500 behind the thin side. The 22 corner markets carry a median margin of 60.0% on a median depth of $95, while the 38 non-corner markets run 4.0%.

So the rule for prediction-market quotes: score them on depth, not on presence. One line of filtering does it.

tradeable = margin(q) < 0.05 and min(exchange_depth(pm, market_id).values()) >= 500

Kalshi runs the opposite policy. It lists twelve markets on this fixture and prices ten of them inside 4%, with $2,916 behind the three-way and $3,506 behind goals over/under 2.5. Fewer markets, all of them real. Our Kalshi and Polymarket comparison breaks down the two venues at the API level.

One more caveat with a number on it. Both exchanges widen fast when the game is further out. On the August 20 fixture, nine days from kickoff at the time of writing, Kalshi’s three-way margin was 14.00% and Polymarket’s was 9.00%. Tight prediction-market pricing is a near-kickoff phenomenon.

De-vig Pinnacle, then shop the board

Pinnacle is the sharp reference on La Liga. Strip its margin with the power method and you get a fair price to compare the rest of the board against.

def devig_power(prices):
    """Solve sum(p_i ** k) = 1 by bisection. Keeps favourite-longshot shape."""
    lo, hi = 0.5, 2.0
    for _ in range(60):
        k = (lo + hi) / 2
        if sum((1 / p) ** k for p in prices.values()) > 1:
            lo = k
        else:
            hi = k
    k = (lo + hi) / 2
    return {oid: (1 / p) ** k for oid, p in prices.items()}

pin = quote(odds["bookmakerOdds"]["pinnacle"], MONEYLINE)
fair = devig_power(pin)

best = {}
for slug, book in live_books(odds).items():
    for oid, price in quote(book, MONEYLINE).items():
        if oid not in best or price > best[oid][0]:
            best[oid] = (price, slug)
Outcome Pinnacle Fair (no-vig) Best on the board
Sevilla 2.350 2.415 (41.4%) 2.381 @ polymarket
Draw 3.280 3.408 (29.3%) 3.333 @ kalshi
Rayo Vallecano 3.290 3.418 (29.3%) 3.600 @ betparx

Shopping twelve independent quotes recovers most of Pinnacle’s margin on the favourite and the draw. The Rayo price sits above Pinnacle’s fair number at the time of the pull, which is a best-available price rather than a verified edge: BetParx belongs to the four-slug feed group, its limits are unpublished, and a de-vig from a single book is one opinion. Treat it as a starting point for the line-shopping workflow, and read the three de-vig methods before you build on it.

Handicaps and totals: resolve by name, never by ID

Spanish football trades on the Asian handicap, and every handicap rung is its own market ID. Asian handicap 0 is 1072, minus 0.25 is 1070, minus 0.5 is 1068. Totals work the same way, and books quote quarter lines like 2.25 and 2.75 that most tutorials never mention. Hardcode 1010 as “the total” and you will miss the line SBOBet actually trades.

catalog = api("markets", sportId=10)
market_name = {m["marketId"]: (m["marketName"], m.get("handicap")) for m in catalog}

def lines(book, name):
    found = {}
    for mid in book["markets"]:
        nm, handicap = market_name.get(int(mid), ("", None))
        if nm == name:
            q = quote(book, mid)
            if len(q) == 2:
                found[handicap] = (mid, q, margin(q))
    return dict(sorted(found.items(), key=lambda kv: kv[0]))

for slug in ("pinnacle", "sbobet"):
    ah = lines(odds["bookmakerOdds"][slug], "Asian Handicap")
    print(slug, "walks", len(ah), "rungs:",
          ", ".join(f"{h:+.2f} {m * 100:.2f}%" for h, (_, _, m) in ah.items()))
pinnacle walks 9 rungs: -1.25 3.49%, -1.00 3.58%, -0.75 3.25%, -0.50 3.05%,
                        -0.25 2.36%, +0.00 2.90%, +0.25 3.25%, +0.50 3.57%, +0.75 3.58%
sbobet walks 3 rungs:   -0.50 3.89%, -0.25 2.90%, +0.00 3.90%

Pinnacle’s margin dips to 2.36% at minus 0.25 and widens toward both wings, so the tightest rung tells you where it thinks the true number sits. SBOBet quotes three rungs and charges 2.90% at the same handicap where it charges 10.15% on the three-way. The Asian handicap calculator covers settling quarter lines.

One catalogue warning: sportId on /markets does nothing. The endpoint returns the same 32,815-row global catalogue whatever you pass. Use it as a name lookup and read the live market IDs off the odds payload.

Corners, and a sharp who prices them

Corner markets are usually the first thing a generic feed drops. On this fixture nine books quote them.

Bookmaker Corner markets Corners O/U 9.5 Margin
betparx / ballybet / betrivers 55 1.73 / 1.97 8.56%
hardrockbet 45 1.741 / 1.952 8.67%
fourwinds 23 1.73 / 1.97 8.56%
polymarket 22 1.852 / 2.041 2.99%
pinnacle 16 1.819 / 2.010 4.73%
betmgm / borgata 4 not quoted

Pinnacle prices corners on La Liga, including a corners handicap at 6.42%. It skipped corners entirely on the English board earlier this month, so this is league by league, not a blanket rule. Probe the competition you care about rather than assuming.

The retail books charge roughly twice Pinnacle’s margin on the same corner line, and Polymarket’s headline corner number is tighter than both. Check the depth first, as above: Polymarket’s 9.5 line held $854 on the thin side, while its half-time and per-team corner ladders held under $100.

Free historical odds: watch the limit ramp

Historical price history sits on the free tier. Competitors charge for it. The response shape differs from the live endpoint in one important way: the top-level key is bookmakers, and players["0"] is a list of snapshots rather than a single price.

hist = requests.get(f"{BASE_URL}/historical-odds",
                    params={"apiKey": API_KEY, "fixtureId": FIXTURE,
                            "bookmakers": "pinnacle"}).json()

snaps = hist["bookmakers"]["pinnacle"]["markets"]["101"]["outcomes"]["101"]["players"]["0"]
changes = sum(1 for a, b in zip(snaps, snaps[1:]) if a["price"] != b["price"])

print(f"{len(snaps)} snapshots, {changes} price changes, "
      f"{snaps[0]['createdAt'][:10]} -> {snaps[-1]['createdAt'][:10]}")
print(f"price {snaps[0]['price']} -> {snaps[-1]['price']}, "
      f"limit ${snaps[0]['limit']:,.0f} -> ${snaps[-1]['limit']:,.0f}")
32 snapshots, 12 price changes, 2026-07-06 -> 2026-08-11
price 2.34 -> 2.35, limit $250 -> $1,500

Pinnacle opened Sevilla v Rayo on July 6, forty days out, at 2.34 with a $250 base limit. Five weeks and twelve price changes later the number had moved one tick, to 2.35, and the limit had gone up six times over. The confidence lives in the limit, not the price.

The same ladder runs across markets inside the fixture. Pinnacle’s base is $1,500 on the three-way and the Asian handicap, $1,000 on goals over/under, $250 on corners and $75 across the correct-score long tail. That is a twenty-fold spread inside one game, and it ranks the markets by how much the book wants to be involved. Our guide to the limit field unpacks the arithmetic (Pinnacle caps the win, not the stake).

Two limits on this endpoint: three bookmakers per call, and no market filter, so each response is large. Sevilla v Rayo with three books came back at 8.33 MB. Sleep about 4.5 seconds between calls and skip the exchanges for bulk pulls, because a Polymarket history runs to tens of megabytes on its own.

What is missing

Straight answers, so you can plan around them.

  • No Spanish book gives you the best price. Re-measured on 24 August 2026 across four fixtures, nine Spanish-licensed slugs ship live La Liga odds: 888sport.es, betway.es, bwin.es, codere.es, leovegas.es, paf.es, pokerstars.es and winamax.es quoted all four, bet365.es three, and marathonbet two. A no-filter /odds call returns 174 to 192 books per fixture, and 160 of them carry a complete 1X2. Rank those 160 by median three-way margin and the local brands land mid-table or worse: codere.es 4.36% (34th), paf.es 5.70% (84th), bwin.es 5.87% (96th), winamax.es 6.71% (123rd), 888sport.es 7.44% (136th), leovegas.es 9.72% (152nd), betway.es 13.73% (156th). pinnacle sits 17th at 2.93%, so the tightest Spanish book still charges 1.43 points more than the sharp. The top five are betfair-ex 0.71%, sx.bet 1.12%, polymarket 1.50%, kalshi 1.51% and 1xbet 1.57%. Two catalogue rows stayed absent, betfair.es and goldenpark.es. Pull the Spanish slugs for local coverage or a comparison page. Do not use them as your fair-value anchor.
  • No scores and no player stats. The fixtures endpoint carries status, participants and third-party IDs in externalProviders (Sofascore, Betradar, OpticOdds, Flashscore, Pinnacle), which you can use as a join key against a stats provider.
  • No player props on the opening round. The board is game lines, goals, corners and correct score. Prop menus fill in closer to kickoff and come from US retail books, never from the sharps.
  • Outrights are absent. No La Liga winner market on this feed.

Where to take it next

The board refreshes on a poll, and WebSocket streaming pushes the same prices if you need them the moment they move. Feed the fair prices into a value scanner, or store the round in SQLite and run the same census every matchday.

Stop scraping Spanish sportsbooks. Grab a free API key and pull the whole La Liga board in one call.

FAQ

Is there an official La Liga odds API?

No. LaLiga licenses data through commercial partners on enterprise contracts. OddsPapi aggregates the sportsbooks that price La Liga and serves them as one JSON payload, with a free tier.

Which bookmakers cover La Liga?

Sixteen to seventeen per fixture on the opening round: Pinnacle, SBOBet, Bet365, DraftKings, FanDuel, BetMGM, Caesars, William Hill, BetRivers, BetParx, BallyBet, FourWinds, Hard Rock Bet, PointsBet, Borgata, plus Kalshi and Polymarket. They dedupe to twelve independent price feeds.

What is the La Liga tournament ID?

8, with slug laliga under category Spain. Confirm it yourself with /v4/tournaments?sportId=10, because 236 soccer tournaments have “liga” in the name.

Can I get Asian handicap odds for La Liga?

Yes. Pinnacle walked nine handicap rungs on the fixture measured here and SBOBet walked three. Each rung is a separate market ID, so resolve by marketName plus handicap instead of hardcoding IDs.

Do prediction markets price La Liga?

Kalshi covered all eight opening fixtures and Polymarket covered seven. Both post tighter three-way margins than any sportsbook near kickoff, and both widen sharply further out. Check the exchangeMeta depth before you trust a quote.

Is the historical odds data free?

Yes, on the free tier, back to the opening line. The Pinnacle history for the fixture above starts forty days before kickoff and carries the limit on every snapshot. Three bookmakers per call.