Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way

Ligue 1 Odds API - OddsPapi API Blog
How To Guides August 26, 2026

You want Ligue 1 prices in Python, so you check the fixture list, see hasOdds: true on every game, and write your parser against one match. Then you run it across the round and half your fields come back empty.

The board is not uniform. On 24 August 2026 the nine Ligue 1 fixtures of matchday 4, played 28 to 30 August, carried a median of 174 bookmakers each. The deepest fixture carried 177. The thinnest carried 91. Across the round, 87.9% of prices were active and the nine fixtures between them quoted 573 distinct market IDs.

Coverage in this feed resolves per fixture, not per league. This guide shows you how to probe it before you commit to a schema, using the free tier of the OddsPapi odds API and 350+ bookmakers.

What the Ligue 1 board looks like on matchday 4

Every number in this section comes from live /v4/odds calls made on 24 August 2026, four to six days before kickoff, across all nine fixtures of the 28 to 30 August round.

Measure Matchday 4, 28 to 30 August 2026
Fixtures 9
Books per fixture 174 median, 177 max, 91 min
Prices active 87.9%
Distinct market IDs 573 across the round
Books with a complete three-way 163, on at least seven of the nine fixtures
Dedupe collapse 1,396 slug-quotes to 778 independent, 44.3%

Depth arrives late and it arrives fast. Five days earlier, on 19 August, Kalshi quoted 1.250 / 1.250 / 1.250 on three of these same fixtures. That is a 140% three-way margin and a placeholder. On 24 August the same venue posts a 1.00% median across the round, second tightest of the 163 books that carry a complete three-way.

Those placeholder prices arrive with active: true and a populated exchangeMeta ladder behind them. Neither flag tells you the price is tradeable. Neither does hasOdds.

Old way vs OddsPapi

Job Scraping or a single-book API OddsPapi
Ligue 1 board One book, one menu, and a login wall A median of 174 books on one /odds call
Sharp reference Pinnacle has no public API pinnacle slug, with published limits
Prediction markets Separate Kalshi and Polymarket clients Same payload, decimal odds, depth ladder
Price history Paid add-on or your own recorder /historical-odds on the free tier
Asian handicaps Flattened into a single spread field Native ladder, one market ID per rung
Push updates Poll and hope WebSocket feed

Step 1: Authenticate and survive the rate limit

The key goes in the query string. It is not a header. The free tier rate-limits per endpoint and returns a real HTTP 429 with a retryMs field, so read it and wait. An empty fixture window returns HTTP 404 rather than an empty array, which kills any loop that calls raise_for_status() without a guard.

import requests, time

API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
SESSION = requests.Session()

def api(path, **params):
    """GET a v4 endpoint. Honours 429 retryMs. Treats 404 as an empty result."""
    params["apiKey"] = API_KEY
    for _ in range(6):
        r = SESSION.get(f"{BASE_URL}{path}", params=params, timeout=300)
        if r.status_code == 429:
            wait = r.json().get("error", {}).get("retryMs", 1500) / 1000
            time.sleep(wait + 0.3)
            continue
        if r.status_code == 404:
            return None
        r.raise_for_status()
        return r.json()
    raise RuntimeError(f"gave up on {path}")

Do not thread these calls. Concurrent requests to the same endpoint come back 429 at any worker count. One call per second is the working pace.

Step 2: Find the right Ligue 1

The soccer catalogue holds 1,762 tournaments and seven of them are named exactly “Ligue 1”. France is one. The others sit in Algeria, Tunisia, Ivory Coast, Senegal, Mali and Burkina Faso. There is also a row called “Ligue 1 SRL Simulated Reality League” with simulated teams, which a substring match on “Ligue 1” picks up as well.

Match the name and the category together, then hardcode the ID.

def find_ligue_1():
    rows = api("/tournaments", sportId=10)
    hits = [t for t in rows if t["tournamentName"].strip().lower() == "ligue 1"]
    for t in hits:
        print(t["tournamentId"], t["tournamentName"], "|", t.get("categoryName"))
    return next(t for t in hits if t.get("categoryName") == "France")

# 34 Ligue 1 | France          <- this one
# 841 Ligue 1 | Algeria
# 984 Ligue 1 | Tunisia
# 1211 Ligue 1 | Ivory Coast
# 1226 Ligue 1 | Senegal
# 40817 Ligue 1 | Mali
# 51190 Ligue 1 | Burkina Faso

Ligue 1 is tournamentId 34. The tournamentId parameter on /fixtures is undocumented and it works, cutting the payload by about 20x.

Step 3: Pull the fixtures

The to parameter is a midnight instant, not a whole day. A query with from and to set to the same date returns only the games that start at exactly 00:00Z. Set to to the day after the last day you want.

from datetime import date, timedelta

def ligue_1_fixtures(tid, start, days=10):
    """`to` is a midnight instant, so ask for the day AFTER the last day you want."""
    out = {}
    d = start
    end = start + timedelta(days=days)
    while d < end:
        stop = min(d + timedelta(days=10), end)
        got = api("/fixtures", sportId=10, tournamentId=tid,
                  **{"from": d.isoformat(), "to": stop.isoformat()}) or []
        for f in got:
            out[f["fixtureId"]] = f
        d = stop
        time.sleep(1.0)
    return sorted(out.values(), key=lambda f: f["startTime"])

A full-season walk run on 19 August 2026 out to June 2027 returned 36 unique fixtures across four rounds. Windows past mid-September came back 404. Re-run the walk weekly and merge on fixtureId.

Step 4: Census the board before you trust it

This is the step most Ligue 1 parsers skip. Count the books and the distinct market IDs on each fixture, and the thin fixtures separate themselves in one pass.

def census(fixtures):
    rows = []
    for f in fixtures:
        if not f.get("hasOdds"):
            continue
        d = api("/odds", fixtureId=f["fixtureId"]) or {}
        books = d.get("bookmakerOdds") or {}
        market_ids = {m for b in books.values() for m in (b.get("markets") or {})}
        rows.append({
            "fixtureId": f["fixtureId"],
            "kickoff": f["startTime"][:16],
            "match": f'{f["participant1Name"]} v {f["participant2Name"]}',
            "books": len(books),
            "markets": len(market_ids),
        })
        time.sleep(1.1)
    return rows

Summarise those rows across the round and the shape of the board falls out:

matchday 4, 28 to 30 Aug 2026, sampled 24 Aug 2026

fixtures with a board          9
books per fixture              174 median, 177 max, 91 min
prices active                  87.9%
distinct market IDs (round)    573
complete 1X2 quotes            163 books, on at least 7 of the 9 fixtures
slug-quotes -> independent     1396 -> 778   (44.3% collapse)

One fixture in that round sits at 91 books while the median sits at 174. Read the count per fixture before you decide what your schema can lean on.

Step 5: Parse the three-way with guards

An outcome object has exactly one key, players. The price, the active flag and the limit all live one level deeper, on players["0"]. Testing outcome.active always returns undefined.

Three separate conditions produce a broken quote on this league. A book can ship suspended: true with prices still attached, which BetRivers does on other rounds. A book can ship a partial three-way, which Bet365 does on Serie A and the Bundesliga. And an individual outcome can carry active: false. Check all three.

OUTCOMES = {"101": "home", "102": "draw", "103": "away"}

def three_way(book):
    """Return {home, draw, away} decimal prices, or None if the quote is unusable."""
    if book.get("suspended") or book.get("bookmakerIsActive") is False:
        return None
    market = (book.get("markets") or {}).get("101")
    if not market:
        return None
    prices = {}
    for outcome_id, label in OUTCOMES.items():
        outcome = (market.get("outcomes") or {}).get(outcome_id) or {}
        price = (outcome.get("players") or {}).get("0")
        if not isinstance(price, dict) or price.get("active") is False:
            return None
        if not price.get("price"):
            return None
        prices[label] = price["price"]
    return prices if len(prices) == 3 else None

def board(payload):
    books = payload.get("bookmakerOdds") or {}
    return {slug: q for slug, b in books.items() if (q := three_way(b))}

Step 6: Dedupe on the price tuple, per fixture

Several slugs quote byte-identical prices. Across the matchday 4 round, 1,396 slug-quotes collapsed to 778 independent quotes. That is a 44.3% collapse. Any book count you publish without running this is inflated by close to half.

The repeat offenders: ballybet, betparx, betrivers and fourwinds ship one tuple, betmgm and borgata another, caesars and williamhill a third. The cloneOf field in /v4/bookmakers reports null for all four members of the BetParx group.

Run the dedupe per fixture rather than off a static clone list. On the 21 to 23 August round, bet365 collided with the Caesars tuple on two fixtures and DraftKings collided with it on one. Neither pairing is a shared feed. Round numbers produce coincidences.

def independent(quotes):
    groups = {}
    for slug, q in quotes.items():
        groups.setdefault((q["home"], q["draw"], q["away"]), []).append(slug)
    return groups

# round total: 1396 slug-quotes -> 778 independent (44.3% collapse)
# collapse ['ballybet', 'betparx', 'betrivers', 'fourwinds']
# collapse ['betmgm', 'borgata']
# collapse ['caesars', 'williamhill']

Step 7: Margin, fair odds and best price

Sum the inverse prices and subtract one. Then strip the margin with the power method, which scales each implied probability by an exponent instead of dividing them all by the same number.

def margin(q):
    return sum(1 / p for p in q.values()) - 1

def power_devig(q, tol=1e-9):
    raw = {k: 1 / v for k, v in q.items()}
    lo, hi = 0.5, 3.0
    while hi - lo > tol:
        k = (lo + hi) / 2
        if sum(r ** k for r in raw.values()) > 1:
            lo = k
        else:
            hi = k
    k = (lo + hi) / 2
    return {name: r ** k for name, r in raw.items()}, k

def best_price(quotes):
    best = {}
    for slug, q in quotes.items():
        for leg, price in q.items():
            if leg not in best or price > best[leg][1]:
                best[leg] = (slug, price)
    return best

Median 1X2 margin across the nine matchday 4 fixtures, 163 books ranked:

Book Median 1X2 margin Rank of 163
polymarket.us 1.00% 1
kalshi 1.00% 2
1xbet 1.55% 3
duel 1.88% 4
betfair-ex 1.95% 5
polymarket 1.99% 6
betcity.nl 2.69% 7
svenskaspel 2.69% 8
pinnacle 3.52% 16
draftkings 6.80% 85
bet365 9.78% 146

Pinnacle ranks 16th. Fifteen books quote a tighter three-way than the sharp reference, and four of the eight tightest are exchanges or prediction markets. Use Pinnacle as a benchmark because its limits are published and its number is honest, not because it is the smallest margin on the board.

Here is the same maths on a single fixture. Marseille against Strasbourg, matchday 3, sampled on 19 August 2026, twelve independent quotes on that call, sorted by margin:

Book Home Draw Away Margin
kalshi 1.786 4.348 4.762 -0.01%
polymarket 1.786 4.167 4.545 1.99%
pinnacle 1.735 4.230 4.460 3.70%
pointsbet.com.au 1.770 4.000 4.300 4.75%
draftkings 1.741 4.100 4.300 5.08%
betmgm = borgata 1.720 4.100 4.330 5.62%
caesars = williamhill 1.714 4.000 4.400 6.07%
bet365 1.680 4.000 4.500 6.75%
hardrockbet 1.645 4.000 4.750 6.84%
fanduel 1.670 4.200 4.300 6.95%
ballybet group 1.650 3.950 4.700 7.20%
sbobet 1.760 3.660 3.850 10.11%

Pinnacle’s three-way de-vigs at k = 1.0381 to 56.44% / 22.38% / 21.18%, which is fair odds of 1.772 / 4.469 / 4.721. Kalshi held the best price on all three legs. That is the best available number, not an edge, and the next section explains why.

Step 8: A negative margin is a bug in your parser

Kalshi’s -0.01% margin on that fixture is real and it is backed by money. The top rung of its back ladder held $2,804 on the home side and $266 on the away side. Split $100 for an equal payout and you can scale it to $1,266.89 of stake before the away rung runs out. That stake returns $1,267.02. Your profit is thirteen cents.

So a margin at or below zero is not an opportunity even when the depth is genuine. More often it means you paired the wrong legs. Across the nine fixtures of the 21 to 23 August round, 23 of 7,662 two-sided quotes summed under 100%, and 14 of those came from one market family.

Corners - 1X2 is a three-way market. Its first-half and second-half variants ship two of the three outcomes, so a parser that grabs “the priced legs” and sums them reads a 6% arbitrage that does not exist. Guard both conditions at once.

def two_sided(market):
    """Only pair a market when it has exactly two priced legs AND they sum over 100%."""
    legs = []
    for outcome in (market.get("outcomes") or {}).values():
        price = (outcome.get("players") or {}).get("0")
        if isinstance(price, dict) and price.get("price") and price.get("active") is not False:
            legs.append(price["price"])
    if len(legs) != 2:
        return None
    if sum(1 / p for p in legs) <= 1.0:
        return None   # a single book never offers a free arb: you paired the wrong legs
    return legs

# Corners - 1X2              -> None (three outcomes)
# Corners - 1X2 First Half   -> None (two legs, sums to 93.5%)
# Corners - 1X2 Second Half  -> None (two legs, sums to 94.7%)

Step 9: Probe coverage per fixture

This probe changes how you build the schema. Run it across a round and read the columns rather than assuming the league has a feature.

def market_names(sport_id=10):
    rows = api("/markets", sportId=sport_id)
    return {str(m["marketId"]): m["marketName"] for m in rows}

def coverage(payload, names):
    books = payload.get("bookmakerOdds") or {}
    report = {"books": len(books), "corners": set(), "cards": set(),
              "props": set(), "prop_prices": 0, "pinnacle_corners": False}
    for slug, b in books.items():
        for market_id, market in (b.get("markets") or {}).items():
            name = names.get(market_id, "")
            if "Corner" in name:
                report["corners"].add(slug)
                if slug == "pinnacle":
                    report["pinnacle_corners"] = True
            if "Card" in name or "Bookings" in name:
                report["cards"].add(slug)
            for outcome in (market.get("outcomes") or {}).values():
                players = [k for k in (outcome.get("players") or {}) if k != "0"]
                if players:
                    report["props"].add(slug)
                    report["prop_prices"] += len(players)
    report["pinnacle"] = "pinnacle" in books
    return report

The table below is the matchday 3 round, 21 to 23 August 2026, sampled on 19 August. It is the clearest per-fixture split the league has produced so far.

Fixture Pinnacle Corner books Pinnacle corners Card books Prop prices
Marseille v Strasbourg yes 11 yes 5 2,884
Lens v Auxerre yes 11 yes 3 2,462
Le Mans v Brest yes 9 no 3 2,613
Nice v Lorient yes 11 yes 3 2,162
Toulouse v Lyon yes 11 yes 3 2,401
Troyes v Paris FC yes 9 no 3 1,801
Angers v Lille yes 11 yes 3 2,384
Le Havre v Monaco yes 11 yes 3 2,268
PSG v Rennes no 10 no 3 2,830

Three things fall out of that table.

Pinnacle skipped the biggest game. PSG against Rennes was the marquee fixture of that round and Pinnacle carried no quote on it. This is not a transient gap in a single call. A /historical-odds request for pinnacle on that fixture returns nothing at all, while DraftKings has 27 snapshots going back to 6 July. FanDuel is missing too. If your model benchmarks against a sharp price, handle the case where the sharp book is not on the board.

Pinnacle priced Ligue 1 corners on six fixtures out of nine. It prices corners on La Liga and skips them on the Bundesliga and on Serie A. Corner coverage is a per-fixture fact even inside one round of one league.

Card markets are live on Ligue 1. Three to five books quoted Player To Be Carded on every fixture of that round, and Bet365 posted a full Bookings - Over Under ladder plus per-team booking totals on two of them. The player market runs on Bet365, BetMGM, BetParx, Borgata and FourWinds, and two of those five are the known duplicate pairs, so you get about three independent feeds. Bet365 is alone on the match booking totals, which means there is no consensus to de-vig against.

Step 10: The Asian handicap ladder and what limits tell you

Ligue 1 handicaps sit in marketName: "Asian Handicap" with period: "fulltime", one market ID per rung. Resolve by name and handicap. Never hardcode a market ID for a line.

def catalogue(sport_id=10):
    return {str(m["marketId"]): (m["marketName"], m.get("handicap"), m.get("period"))
            for m in api("/markets", sportId=sport_id)}

def handicap_ladder(book, cat, family="Asian Handicap", period="fulltime"):
    rungs = []
    for market_id, market in (book.get("markets") or {}).items():
        name, hcap, per = cat.get(market_id, ("", None, None))
        if name != family or per != period:
            continue
        legs = two_sided(market)
        if not legs:
            continue
        rungs.append((hcap, round((sum(1 / p for p in legs) - 1) * 100, 2), legs))
    return sorted(rungs)

def limit_base(price_obj):
    """Pinnacle publishes a capped max WIN. Recover the per-market base."""
    limit, price = price_obj.get("limit"), price_obj.get("price")
    if not limit or not price:
        return None            # sbobet and US retail ship limit: null
    return round(limit if price >= 2 else limit * (price - 1))

Pinnacle walked nine rungs on Marseille against Strasbourg and its margin traced a U across them, tightest at the rung closest to the true number:

[(-1.75, 3.62), (-1.5, 3.71), (-1.25, 3.22), (-1.0, 3.13),
 (-0.75, 2.46),                                 <- minimum
 (-0.5, 3.09), (-0.25, 3.23), (0.0, 3.61), (0.25, 3.57)]

SBOBet ran its usual split on that fixture: 10.11% on the three-way against 2.84% on its tightest handicap. On the Premier League, La Liga, the Bundesliga and Serie A that handicap number beats Pinnacle. On Ligue 1 it does not. Pinnacle’s 2.46% at -0.75 is tighter. Check the competition rather than carrying the rule across.

Null-guard the limit before you do arithmetic on it. Only Pinnacle and the exchanges populate it. SBOBet ships limit: null on every outcome, and so does US retail.

Step 11: Watch the limit ramp on free historical data

Pinnacle’s limit is a capped maximum win, so dividing it back out recovers a per-market base. That base is the book’s own confidence signal, and /historical-odds gives you the whole ramp at no cost.

def limit_ramp(fixture_id, slug="pinnacle", market="101", outcome="101"):
    data = api("/historical-odds", fixtureId=fixture_id, bookmakers=slug) or {}
    book = (data.get("bookmakers") or {}).get(slug)
    if not book:
        return []
    snaps = (((book.get("markets") or {}).get(market) or {})
             .get("outcomes", {}).get(outcome, {}).get("players", {}).get("0")) or []
    ramp, previous = [], None
    for snap in snaps:
        base = limit_base(snap)
        if base != previous:
            ramp.append((snap["createdAt"][:16], snap["price"], base))
            previous = base
    return ramp

Marseille against Strasbourg, read on 19 August 2026: 36 snapshots and 19 price changes since 6 July.

Timestamp (UTC) Days to kickoff Home price Implied base
2026-07-06 07:39 46 1.704 $250
2026-08-14 18:45 7 1.746 $500
2026-08-16 08:00 5 1.769 $750
2026-08-19 05:01 2 1.735 $1,000

The price moved three ticks in six weeks. The limit went up four times. A $250 base is the number Pinnacle posts when it is holding a placeholder, and it appears at the same value on the opening fixtures of the Premier League, La Liga, the Bundesliga and Serie A before each one ramps.

The same history answers who quotes a Ligue 1 fixture first. FanDuel opened on 5 July, Pinnacle on 6 July, and Bet365 arrived last of the sportsbooks on 14 August, seven days out. Kalshi’s first quote landed on 7 August, exactly fourteen days before kickoff, which is the same fourteen-day window it runs on Serie A. Read more on how the two venues behave in Kalshi API vs Polymarket API.

Step 12: Player props are keyed by player ID

On game lines, players has a single "0" key. On player props it is keyed by player ID, and each entry carries a playerName in “Last, First” format. Hardcoding players["0"] makes every prop market look empty.

def player_prop(payload, names, family):
    rows = []
    for slug, b in (payload.get("bookmakerOdds") or {}).items():
        for market_id, market in (b.get("markets") or {}).items():
            if names.get(market_id) != family:
                continue
            for outcome_id, outcome in (market.get("outcomes") or {}).items():
                for player_id, price in (outcome.get("players") or {}).items():
                    if player_id == "0" or not isinstance(price, dict):
                        continue
                    if not price.get("price") or price.get("active") is False:
                        continue
                    rows.append((slug, price.get("playerName"), outcome_id, price["price"]))
    return rows

# ('ballybet', 'Gouiri, Amine', '10730', 2.2)
# ('bet365', 'Chilwell, Ben', '102732', 4.333)

Marseille against Strasbourg carried 2,884 player-level prices across 13 prop families on 19 August 2026:

Market Books Prices
Anytime Goal Scorer 14 377
First Goal Scorer 13 362
Player Assists 9 180
Player Goals 9 268
Last Goal Scorer 7 206
Over Under Player Shots On Goal 5 195
Player To Be Carded 5 144
Player Shots 4 502
Player Offsides 3 46
Player Fouls Committed 1 50
Player Tackles 1 75

DraftKings is the only book quoting tackles and fouls committed. If you want the same treatment for US sports, see the player props API guide.

What the books disagree about, and what they do not

One test worth publishing as a negative result. De-vig each sportsbook’s three-way, take the fair home probability, and measure the spread across independent quotes. Across the nine fixtures of the 21 to 23 August round the standard deviation ran from 0.62 to 1.43 percentage points, with a maximum spread of 5.30 points on Marseille against Strasbourg.

Newly promoted clubs made no difference. Le Mans, in its first Ligue 1 season since 2008, drew a 0.83 point standard deviation, below the round average. The books had converged on the numbers by matchday three.

That means book selection is not where your effort belongs on Ligue 1 sides. Price selection is. Kalshi and Polymarket sit first and sixth of 163 books by median margin, and the useful work is shopping the line across every independent quote on the board rather than picking a house book.

Five French-licensed books quote Ligue 1, and none of them is your best price

Measured on 24 August 2026 across the nine matchday 4 fixtures, a no-filter /odds call returns a median of 174 bookmakers per fixture. 163 of them carry a complete three-way on at least seven of the nine. Five French-licensed books appear: bet365.fr, pmu, pokerstars.fr, unibet.fr and winamax.fr. That is the thinnest local set in the big five. Germany fields twelve on the same measurement basis.

Four of the five post a complete three-way often enough to score a margin. pokerstars.fr is the exception: it shows up on the board but ships an incomplete 1X2 too often to rank, so treat its presence as a data point and not as a price.

Book Median 1X2 margin Rank of 163 Fixtures
pinnacle 3.52% 16 reference
pmu 6.70% 83 8 of 9
unibet.fr 8.76% 138 9 of 9
winamax.fr 8.93% 141 9 of 9
bet365.fr 12.65% 159 8 of 9
pokerstars.fr no complete 1X2 unranked unranked

PMU is the best of the French set at 6.70%, which is roughly double what Pinnacle charges on the same match. Bet365 FR sits at 12.65% and lands 159th of 163. The tightest quotes on the board come from elsewhere: polymarket.us 1.00%, kalshi 1.00%, 1xbet 1.55%, duel 1.88%, betfair-ex 1.95%, polymarket 1.99%. Pull the French books when you need a French price, for compliance work or a local comparison page, then shop the line somewhere else.

One caveat on the catalogue. genybet.fr, feelingbet.fr, netbet.fr, fdj and zebet.fr are all listed, and not one of them appeared on any of the nine fixtures. A catalogue row is a mapping, not a guarantee of coverage. Probe /odds per fixture before you promise a reader that a given book is on the board.

Ligue 1 quick reference

Item Value
Sport ID 10 (soccer)
Tournament ID 34, category “France”
Three-way market 101, outcomes 101 home / 102 draw / 103 away
Asian handicap marketName: "Asian Handicap", period: "fulltime", one ID per rung
Books per fixture 174 median, 177 max, 91 min (28 to 30 Aug round)
Dedupe collapse 1,396 slug-quotes to 778 independent, 44.3%
Prices active 87.9% across the round
Sharp reference pinnacle, 3.52% median 1X2, rank 16 of 163
Player props 1,801 to 2,884 prices per fixture (21 to 23 Aug round)
Rate limit Per endpoint, ~1 call/second, honour retryMs

Where to go next

The same probe runs on any competition. Swap the tournament ID and read the coverage table before you write the schema:

For the maths that sits on top of this feed, read the no-vig odds guide, the consensus odds calculator, the Asian handicap calculator and why sharps bet the handicap. If you need limits rather than prices, start with the betting limits guide. To stop polling and take pushes instead, move to the WebSocket feed.

Get your key

Stop scraping Ligue 1 pages. One call returns a median of 174 books on a fixture, the round carries 573 distinct market IDs, and the free tier covers all of it including the historical odds that other providers put behind a paywall.

Get your free OddsPapi API key and run the coverage probe above on your own competition.

FAQ

What is the Ligue 1 tournament ID in the OddsPapi API?

Ligue 1 is tournamentId 34 with categoryName “France”. Seven tournaments in the soccer catalogue are named exactly “Ligue 1”, covering Algeria, Tunisia, Ivory Coast, Senegal, Mali and Burkina Faso as well as France, so match the name and the category together.

How many bookmakers quote a Ligue 1 match?

Measured on 24 August 2026 across the nine fixtures of 28 to 30 August, a Ligue 1 fixture carries a median of 174 bookmakers, 177 on the deepest and 91 on the thinnest. 163 of them post a complete three-way. Dedupe on the price tuple first: 1,396 slug-quotes collapse to 778 independent quotes across the round, a 44.3% collapse.

Does Pinnacle cover Ligue 1?

Pinnacle quotes Ligue 1 at a 3.52% median three-way margin, rank 16 of 163 books on the 28 to 30 August round. Coverage is per fixture: on the 21 to 23 August round it priced eight of nine games and carried no quote on Paris Saint-Germain against Rennes, and /historical-odds confirms it never opened that fixture. Check for the slug in each payload rather than assuming a sharp reference exists.

Are card and corner markets available for Ligue 1?

Yes for both, and coverage varies by fixture. On the 21 to 23 August 2026 round, corner markets appeared on 9 to 11 books per game, with Pinnacle pricing them on six fixtures out of nine. Three to five books quoted Player To Be Carded on every fixture, while Bet365 alone posted the match and team booking totals.

Why does a single bookmaker show a negative margin?

Because you paired legs from different markets. Corners - 1X2 has three outcomes, and its half variants ship only two of them, so summing the priced legs returns less than 100%. One book never offers a free arbitrage. Reject any two-sided pair whose inverse prices sum to 1.0 or below.

Is Ligue 1 historical odds data free?

Yes. /historical-odds is on the free tier, with a limit of three bookmakers per call. The Marseille against Strasbourg fixture returned 36 Pinnacle snapshots going back to 6 July, covering 19 price changes and the full limit ramp from a $250 base to $1,000.