{"id":3672,"date":"2026-08-26T10:00:00","date_gmt":"2026-08-26T10:00:00","guid":{"rendered":"https:\/\/oddspapi.io\/blog\/?p=3672"},"modified":"2026-09-07T14:01:57","modified_gmt":"2026-09-07T14:01:57","slug":"ligue-1-odds-api","status":"publish","type":"post","link":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/","title":{"rendered":"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way"},"content":{"rendered":"<p>You want Ligue 1 prices in Python, so you check the fixture list, see <code>hasOdds: true<\/code> on every game, and write your parser against one match. Then you run it across the round and half your fields come back empty.<\/p>\n<p>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.<\/p>\n<p>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 <a href=\"https:\/\/oddspapi.io\/blog\/free-odds-api-350-bookmakers\/\">OddsPapi odds API<\/a> and 350+ bookmakers.<\/p>\n<h2>What the Ligue 1 board looks like on matchday 4<\/h2>\n<p>Every number in this section comes from live <code>\/v4\/odds<\/code> calls made on 24 August 2026, four to six days before kickoff, across all nine fixtures of the 28 to 30 August round.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Measure<\/th>\n<th>Matchday 4, 28 to 30 August 2026<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Fixtures<\/td>\n<td>9<\/td>\n<\/tr>\n<tr>\n<td>Books per fixture<\/td>\n<td>174 median, 177 max, 91 min<\/td>\n<\/tr>\n<tr>\n<td>Prices active<\/td>\n<td>87.9%<\/td>\n<\/tr>\n<tr>\n<td>Distinct market IDs<\/td>\n<td>573 across the round<\/td>\n<\/tr>\n<tr>\n<td>Books with a complete three-way<\/td>\n<td>163, on at least seven of the nine fixtures<\/td>\n<\/tr>\n<tr>\n<td>Dedupe collapse<\/td>\n<td>1,396 slug-quotes to 778 independent, 44.3%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>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.<\/p>\n<p>Those placeholder prices arrive with <code>active: true<\/code> and a populated <code>exchangeMeta<\/code> ladder behind them. Neither flag tells you the price is tradeable. Neither does <code>hasOdds<\/code>.<\/p>\n<h2>Old way vs OddsPapi<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Job<\/th>\n<th>Scraping or a single-book API<\/th>\n<th>OddsPapi<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Ligue 1 board<\/td>\n<td>One book, one menu, and a login wall<\/td>\n<td>A median of 174 books on one <code>\/odds<\/code> call<\/td>\n<\/tr>\n<tr>\n<td>Sharp reference<\/td>\n<td>Pinnacle has no public API<\/td>\n<td><code>pinnacle<\/code> slug, with published limits<\/td>\n<\/tr>\n<tr>\n<td>Prediction markets<\/td>\n<td>Separate Kalshi and Polymarket clients<\/td>\n<td>Same payload, decimal odds, depth ladder<\/td>\n<\/tr>\n<tr>\n<td>Price history<\/td>\n<td>Paid add-on or your own recorder<\/td>\n<td><code>\/historical-odds<\/code> on the free tier<\/td>\n<\/tr>\n<tr>\n<td>Asian handicaps<\/td>\n<td>Flattened into a single spread field<\/td>\n<td>Native ladder, one market ID per rung<\/td>\n<\/tr>\n<tr>\n<td>Push updates<\/td>\n<td>Poll and hope<\/td>\n<td>WebSocket feed<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Step 1: Authenticate and survive the rate limit<\/h2>\n<p>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 <code>retryMs<\/code> field, so read it and wait. An empty fixture window returns HTTP 404 rather than an empty array, which kills any loop that calls <code>raise_for_status()<\/code> without a guard.<\/p>\n<pre class=\"wp-block-code\"><code>import requests, time\n\nAPI_KEY = \"YOUR_API_KEY\"\nBASE_URL = \"https:\/\/api.oddspapi.io\/v4\"\nSESSION = requests.Session()\n\ndef api(path, **params):\n    \"\"\"GET a v4 endpoint. Honours 429 retryMs. Treats 404 as an empty result.\"\"\"\n    params[\"apiKey\"] = API_KEY\n    for _ in range(6):\n        r = SESSION.get(f\"{BASE_URL}{path}\", params=params, timeout=300)\n        if r.status_code == 429:\n            wait = r.json().get(\"error\", {}).get(\"retryMs\", 1500) \/ 1000\n            time.sleep(wait + 0.3)\n            continue\n        if r.status_code == 404:\n            return None\n        r.raise_for_status()\n        return r.json()\n    raise RuntimeError(f\"gave up on {path}\")\n<\/code><\/pre>\n<p>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.<\/p>\n<h2>Step 2: Find the right Ligue 1<\/h2>\n<p>The soccer catalogue holds 1,762 tournaments and seven of them are named exactly &#8220;Ligue 1&#8221;. France is one. The others sit in Algeria, Tunisia, Ivory Coast, Senegal, Mali and Burkina Faso. There is also a row called &#8220;Ligue 1 SRL Simulated Reality League&#8221; with simulated teams, which a substring match on &#8220;Ligue 1&#8221; picks up as well.<\/p>\n<p>Match the name and the category together, then hardcode the ID.<\/p>\n<pre class=\"wp-block-code\"><code>def find_ligue_1():\n    rows = api(\"\/tournaments\", sportId=10)\n    hits = [t for t in rows if t[\"tournamentName\"].strip().lower() == \"ligue 1\"]\n    for t in hits:\n        print(t[\"tournamentId\"], t[\"tournamentName\"], \"|\", t.get(\"categoryName\"))\n    return next(t for t in hits if t.get(\"categoryName\") == \"France\")\n\n# 34 Ligue 1 | France          &lt;- this one\n# 841 Ligue 1 | Algeria\n# 984 Ligue 1 | Tunisia\n# 1211 Ligue 1 | Ivory Coast\n# 1226 Ligue 1 | Senegal\n# 40817 Ligue 1 | Mali\n# 51190 Ligue 1 | Burkina Faso\n<\/code><\/pre>\n<p><strong>Ligue 1 is <code>tournamentId<\/code> 34.<\/strong> The <code>tournamentId<\/code> parameter on <code>\/fixtures<\/code> is undocumented and it works, cutting the payload by about 20x.<\/p>\n<h2>Step 3: Pull the fixtures<\/h2>\n<p>The <code>to<\/code> parameter is a midnight instant, not a whole day. A query with <code>from<\/code> and <code>to<\/code> set to the same date returns only the games that start at exactly 00:00Z. Set <code>to<\/code> to the day after the last day you want.<\/p>\n<pre class=\"wp-block-code\"><code>from datetime import date, timedelta\n\ndef ligue_1_fixtures(tid, start, days=10):\n    \"\"\"`to` is a midnight instant, so ask for the day AFTER the last day you want.\"\"\"\n    out = {}\n    d = start\n    end = start + timedelta(days=days)\n    while d &lt; end:\n        stop = min(d + timedelta(days=10), end)\n        got = api(\"\/fixtures\", sportId=10, tournamentId=tid,\n                  **{\"from\": d.isoformat(), \"to\": stop.isoformat()}) or []\n        for f in got:\n            out[f[\"fixtureId\"]] = f\n        d = stop\n        time.sleep(1.0)\n    return sorted(out.values(), key=lambda f: f[\"startTime\"])\n<\/code><\/pre>\n<p>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 <code>fixtureId<\/code>.<\/p>\n<h2>Step 4: Census the board before you trust it<\/h2>\n<p>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.<\/p>\n<pre class=\"wp-block-code\"><code>def census(fixtures):\n    rows = []\n    for f in fixtures:\n        if not f.get(\"hasOdds\"):\n            continue\n        d = api(\"\/odds\", fixtureId=f[\"fixtureId\"]) or {}\n        books = d.get(\"bookmakerOdds\") or {}\n        market_ids = {m for b in books.values() for m in (b.get(\"markets\") or {})}\n        rows.append({\n            \"fixtureId\": f[\"fixtureId\"],\n            \"kickoff\": f[\"startTime\"][:16],\n            \"match\": f'{f[\"participant1Name\"]} v {f[\"participant2Name\"]}',\n            \"books\": len(books),\n            \"markets\": len(market_ids),\n        })\n        time.sleep(1.1)\n    return rows\n<\/code><\/pre>\n<p>Summarise those rows across the round and the shape of the board falls out:<\/p>\n<pre class=\"wp-block-code\"><code>matchday 4, 28 to 30 Aug 2026, sampled 24 Aug 2026\n\nfixtures with a board          9\nbooks per fixture              174 median, 177 max, 91 min\nprices active                  87.9%\ndistinct market IDs (round)    573\ncomplete 1X2 quotes            163 books, on at least 7 of the 9 fixtures\nslug-quotes -&gt; independent     1396 -&gt; 778   (44.3% collapse)\n<\/code><\/pre>\n<p>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.<\/p>\n<h2>Step 5: Parse the three-way with guards<\/h2>\n<p>An outcome object has exactly one key, <code>players<\/code>. The price, the <code>active<\/code> flag and the limit all live one level deeper, on <code>players[\"0\"]<\/code>. Testing <code>outcome.active<\/code> always returns undefined.<\/p>\n<p>Three separate conditions produce a broken quote on this league. A book can ship <code>suspended: true<\/code> 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 <code>active: false<\/code>. Check all three.<\/p>\n<pre class=\"wp-block-code\"><code>OUTCOMES = {\"101\": \"home\", \"102\": \"draw\", \"103\": \"away\"}\n\ndef three_way(book):\n    \"\"\"Return {home, draw, away} decimal prices, or None if the quote is unusable.\"\"\"\n    if book.get(\"suspended\") or book.get(\"bookmakerIsActive\") is False:\n        return None\n    market = (book.get(\"markets\") or {}).get(\"101\")\n    if not market:\n        return None\n    prices = {}\n    for outcome_id, label in OUTCOMES.items():\n        outcome = (market.get(\"outcomes\") or {}).get(outcome_id) or {}\n        price = (outcome.get(\"players\") or {}).get(\"0\")\n        if not isinstance(price, dict) or price.get(\"active\") is False:\n            return None\n        if not price.get(\"price\"):\n            return None\n        prices[label] = price[\"price\"]\n    return prices if len(prices) == 3 else None\n\ndef board(payload):\n    books = payload.get(\"bookmakerOdds\") or {}\n    return {slug: q for slug, b in books.items() if (q := three_way(b))}\n<\/code><\/pre>\n<h2>Step 6: Dedupe on the price tuple, per fixture<\/h2>\n<p>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.<\/p>\n<p>The repeat offenders: <code>ballybet<\/code>, <code>betparx<\/code>, <code>betrivers<\/code> and <code>fourwinds<\/code> ship one tuple, <code>betmgm<\/code> and <code>borgata<\/code> another, <code>caesars<\/code> and <code>williamhill<\/code> a third. The <code>cloneOf<\/code> field in <code>\/v4\/bookmakers<\/code> reports <code>null<\/code> for all four members of the BetParx group.<\/p>\n<p>Run the dedupe per fixture rather than off a static clone list. On the 21 to 23 August round, <code>bet365<\/code> 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.<\/p>\n<pre class=\"wp-block-code\"><code>def independent(quotes):\n    groups = {}\n    for slug, q in quotes.items():\n        groups.setdefault((q[\"home\"], q[\"draw\"], q[\"away\"]), []).append(slug)\n    return groups\n\n# round total: 1396 slug-quotes -&gt; 778 independent (44.3% collapse)\n# collapse ['ballybet', 'betparx', 'betrivers', 'fourwinds']\n# collapse ['betmgm', 'borgata']\n# collapse ['caesars', 'williamhill']\n<\/code><\/pre>\n<h2>Step 7: Margin, fair odds and best price<\/h2>\n<p>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.<\/p>\n<pre class=\"wp-block-code\"><code>def margin(q):\n    return sum(1 \/ p for p in q.values()) - 1\n\ndef power_devig(q, tol=1e-9):\n    raw = {k: 1 \/ v for k, v in q.items()}\n    lo, hi = 0.5, 3.0\n    while hi - lo &gt; tol:\n        k = (lo + hi) \/ 2\n        if sum(r ** k for r in raw.values()) &gt; 1:\n            lo = k\n        else:\n            hi = k\n    k = (lo + hi) \/ 2\n    return {name: r ** k for name, r in raw.items()}, k\n\ndef best_price(quotes):\n    best = {}\n    for slug, q in quotes.items():\n        for leg, price in q.items():\n            if leg not in best or price > best[leg][1]:\n                best[leg] = (slug, price)\n    return best\n<\/code><\/pre>\n<p>Median 1X2 margin across the nine matchday 4 fixtures, 163 books ranked:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Book<\/th>\n<th>Median 1X2 margin<\/th>\n<th>Rank of 163<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>polymarket.us<\/code><\/td>\n<td>1.00%<\/td>\n<td>1<\/td>\n<\/tr>\n<tr>\n<td><code>kalshi<\/code><\/td>\n<td>1.00%<\/td>\n<td>2<\/td>\n<\/tr>\n<tr>\n<td><code>1xbet<\/code><\/td>\n<td>1.55%<\/td>\n<td>3<\/td>\n<\/tr>\n<tr>\n<td><code>duel<\/code><\/td>\n<td>1.88%<\/td>\n<td>4<\/td>\n<\/tr>\n<tr>\n<td><code>betfair-ex<\/code><\/td>\n<td>1.95%<\/td>\n<td>5<\/td>\n<\/tr>\n<tr>\n<td><code>polymarket<\/code><\/td>\n<td>1.99%<\/td>\n<td>6<\/td>\n<\/tr>\n<tr>\n<td><code>betcity.nl<\/code><\/td>\n<td>2.69%<\/td>\n<td>7<\/td>\n<\/tr>\n<tr>\n<td><code>svenskaspel<\/code><\/td>\n<td>2.69%<\/td>\n<td>8<\/td>\n<\/tr>\n<tr>\n<td><code>pinnacle<\/code><\/td>\n<td>3.52%<\/td>\n<td>16<\/td>\n<\/tr>\n<tr>\n<td><code>draftkings<\/code><\/td>\n<td>6.80%<\/td>\n<td>85<\/td>\n<\/tr>\n<tr>\n<td><code>bet365<\/code><\/td>\n<td>9.78%<\/td>\n<td>146<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>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.<\/p>\n<p>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:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Book<\/th>\n<th>Home<\/th>\n<th>Draw<\/th>\n<th>Away<\/th>\n<th>Margin<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>kalshi<\/td>\n<td>1.786<\/td>\n<td>4.348<\/td>\n<td>4.762<\/td>\n<td>-0.01%<\/td>\n<\/tr>\n<tr>\n<td>polymarket<\/td>\n<td>1.786<\/td>\n<td>4.167<\/td>\n<td>4.545<\/td>\n<td>1.99%<\/td>\n<\/tr>\n<tr>\n<td>pinnacle<\/td>\n<td>1.735<\/td>\n<td>4.230<\/td>\n<td>4.460<\/td>\n<td>3.70%<\/td>\n<\/tr>\n<tr>\n<td>pointsbet.com.au<\/td>\n<td>1.770<\/td>\n<td>4.000<\/td>\n<td>4.300<\/td>\n<td>4.75%<\/td>\n<\/tr>\n<tr>\n<td>draftkings<\/td>\n<td>1.741<\/td>\n<td>4.100<\/td>\n<td>4.300<\/td>\n<td>5.08%<\/td>\n<\/tr>\n<tr>\n<td>betmgm = borgata<\/td>\n<td>1.720<\/td>\n<td>4.100<\/td>\n<td>4.330<\/td>\n<td>5.62%<\/td>\n<\/tr>\n<tr>\n<td>caesars = williamhill<\/td>\n<td>1.714<\/td>\n<td>4.000<\/td>\n<td>4.400<\/td>\n<td>6.07%<\/td>\n<\/tr>\n<tr>\n<td>bet365<\/td>\n<td>1.680<\/td>\n<td>4.000<\/td>\n<td>4.500<\/td>\n<td>6.75%<\/td>\n<\/tr>\n<tr>\n<td>hardrockbet<\/td>\n<td>1.645<\/td>\n<td>4.000<\/td>\n<td>4.750<\/td>\n<td>6.84%<\/td>\n<\/tr>\n<tr>\n<td>fanduel<\/td>\n<td>1.670<\/td>\n<td>4.200<\/td>\n<td>4.300<\/td>\n<td>6.95%<\/td>\n<\/tr>\n<tr>\n<td>ballybet group<\/td>\n<td>1.650<\/td>\n<td>3.950<\/td>\n<td>4.700<\/td>\n<td>7.20%<\/td>\n<\/tr>\n<tr>\n<td>sbobet<\/td>\n<td>1.760<\/td>\n<td>3.660<\/td>\n<td>3.850<\/td>\n<td>10.11%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Pinnacle&#8217;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.<\/p>\n<h2>Step 8: A negative margin is a bug in your parser<\/h2>\n<p>Kalshi&#8217;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.<\/p>\n<p>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.<\/p>\n<p><code>Corners - 1X2<\/code> is a three-way market. Its first-half and second-half variants ship two of the three outcomes, so a parser that grabs &#8220;the priced legs&#8221; and sums them reads a 6% arbitrage that does not exist. Guard both conditions at once.<\/p>\n<pre class=\"wp-block-code\"><code>def two_sided(market):\n    \"\"\"Only pair a market when it has exactly two priced legs AND they sum over 100%.\"\"\"\n    legs = []\n    for outcome in (market.get(\"outcomes\") or {}).values():\n        price = (outcome.get(\"players\") or {}).get(\"0\")\n        if isinstance(price, dict) and price.get(\"price\") and price.get(\"active\") is not False:\n            legs.append(price[\"price\"])\n    if len(legs) != 2:\n        return None\n    if sum(1 \/ p for p in legs) &lt;= 1.0:\n        return None   # a single book never offers a free arb: you paired the wrong legs\n    return legs\n\n# Corners - 1X2              -&gt; None (three outcomes)\n# Corners - 1X2 First Half   -&gt; None (two legs, sums to 93.5%)\n# Corners - 1X2 Second Half  -&gt; None (two legs, sums to 94.7%)\n<\/code><\/pre>\n<h2>Step 9: Probe coverage per fixture<\/h2>\n<p>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.<\/p>\n<pre class=\"wp-block-code\"><code>def market_names(sport_id=10):\n    rows = api(\"\/markets\", sportId=sport_id)\n    return {str(m[\"marketId\"]): m[\"marketName\"] for m in rows}\n\ndef coverage(payload, names):\n    books = payload.get(\"bookmakerOdds\") or {}\n    report = {\"books\": len(books), \"corners\": set(), \"cards\": set(),\n              \"props\": set(), \"prop_prices\": 0, \"pinnacle_corners\": False}\n    for slug, b in books.items():\n        for market_id, market in (b.get(\"markets\") or {}).items():\n            name = names.get(market_id, \"\")\n            if \"Corner\" in name:\n                report[\"corners\"].add(slug)\n                if slug == \"pinnacle\":\n                    report[\"pinnacle_corners\"] = True\n            if \"Card\" in name or \"Bookings\" in name:\n                report[\"cards\"].add(slug)\n            for outcome in (market.get(\"outcomes\") or {}).values():\n                players = [k for k in (outcome.get(\"players\") or {}) if k != \"0\"]\n                if players:\n                    report[\"props\"].add(slug)\n                    report[\"prop_prices\"] += len(players)\n    report[\"pinnacle\"] = \"pinnacle\" in books\n    return report\n<\/code><\/pre>\n<p>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.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Fixture<\/th>\n<th>Pinnacle<\/th>\n<th>Corner books<\/th>\n<th>Pinnacle corners<\/th>\n<th>Card books<\/th>\n<th>Prop prices<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Marseille v Strasbourg<\/td>\n<td>yes<\/td>\n<td>11<\/td>\n<td>yes<\/td>\n<td>5<\/td>\n<td>2,884<\/td>\n<\/tr>\n<tr>\n<td>Lens v Auxerre<\/td>\n<td>yes<\/td>\n<td>11<\/td>\n<td>yes<\/td>\n<td>3<\/td>\n<td>2,462<\/td>\n<\/tr>\n<tr>\n<td>Le Mans v Brest<\/td>\n<td>yes<\/td>\n<td>9<\/td>\n<td><strong>no<\/strong><\/td>\n<td>3<\/td>\n<td>2,613<\/td>\n<\/tr>\n<tr>\n<td>Nice v Lorient<\/td>\n<td>yes<\/td>\n<td>11<\/td>\n<td>yes<\/td>\n<td>3<\/td>\n<td>2,162<\/td>\n<\/tr>\n<tr>\n<td>Toulouse v Lyon<\/td>\n<td>yes<\/td>\n<td>11<\/td>\n<td>yes<\/td>\n<td>3<\/td>\n<td>2,401<\/td>\n<\/tr>\n<tr>\n<td>Troyes v Paris FC<\/td>\n<td>yes<\/td>\n<td>9<\/td>\n<td><strong>no<\/strong><\/td>\n<td>3<\/td>\n<td>1,801<\/td>\n<\/tr>\n<tr>\n<td>Angers v Lille<\/td>\n<td>yes<\/td>\n<td>11<\/td>\n<td>yes<\/td>\n<td>3<\/td>\n<td>2,384<\/td>\n<\/tr>\n<tr>\n<td>Le Havre v Monaco<\/td>\n<td>yes<\/td>\n<td>11<\/td>\n<td>yes<\/td>\n<td>3<\/td>\n<td>2,268<\/td>\n<\/tr>\n<tr>\n<td>PSG v Rennes<\/td>\n<td><strong>no<\/strong><\/td>\n<td>10<\/td>\n<td><strong>no<\/strong><\/td>\n<td>3<\/td>\n<td>2,830<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>Three things fall out of that table.<\/p>\n<p><strong>Pinnacle skipped the biggest game.<\/strong> 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 <code>\/historical-odds<\/code> request for <code>pinnacle<\/code> 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.<\/p>\n<p><strong>Pinnacle priced Ligue 1 corners on six fixtures out of nine.<\/strong> 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.<\/p>\n<p><strong>Card markets are live on Ligue 1.<\/strong> Three to five books quoted <code>Player To Be Carded<\/code> on every fixture of that round, and Bet365 posted a full <code>Bookings - Over Under<\/code> 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.<\/p>\n<h2>Step 10: The Asian handicap ladder and what limits tell you<\/h2>\n<p>Ligue 1 handicaps sit in <code>marketName: \"Asian Handicap\"<\/code> with <code>period: \"fulltime\"<\/code>, one market ID per rung. Resolve by name and handicap. Never hardcode a market ID for a line.<\/p>\n<pre class=\"wp-block-code\"><code>def catalogue(sport_id=10):\n    return {str(m[\"marketId\"]): (m[\"marketName\"], m.get(\"handicap\"), m.get(\"period\"))\n            for m in api(\"\/markets\", sportId=sport_id)}\n\ndef handicap_ladder(book, cat, family=\"Asian Handicap\", period=\"fulltime\"):\n    rungs = []\n    for market_id, market in (book.get(\"markets\") or {}).items():\n        name, hcap, per = cat.get(market_id, (\"\", None, None))\n        if name != family or per != period:\n            continue\n        legs = two_sided(market)\n        if not legs:\n            continue\n        rungs.append((hcap, round((sum(1 \/ p for p in legs) - 1) * 100, 2), legs))\n    return sorted(rungs)\n\ndef limit_base(price_obj):\n    \"\"\"Pinnacle publishes a capped max WIN. Recover the per-market base.\"\"\"\n    limit, price = price_obj.get(\"limit\"), price_obj.get(\"price\")\n    if not limit or not price:\n        return None            # sbobet and US retail ship limit: null\n    return round(limit if price >= 2 else limit * (price - 1))\n<\/code><\/pre>\n<p>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:<\/p>\n<pre class=\"wp-block-code\"><code>[(-1.75, 3.62), (-1.5, 3.71), (-1.25, 3.22), (-1.0, 3.13),\n (-0.75, 2.46),                                 &lt;- minimum\n (-0.5, 3.09), (-0.25, 3.23), (0.0, 3.61), (0.25, 3.57)]\n<\/code><\/pre>\n<p>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&#8217;s 2.46% at -0.75 is tighter. Check the competition rather than carrying the rule across.<\/p>\n<p><strong>Null-guard the limit before you do arithmetic on it.<\/strong> Only Pinnacle and the exchanges populate it. SBOBet ships <code>limit: null<\/code> on every outcome, and so does US retail.<\/p>\n<h2>Step 11: Watch the limit ramp on free historical data<\/h2>\n<p>Pinnacle&#8217;s limit is a capped maximum win, so dividing it back out recovers a per-market base. That base is the book&#8217;s own confidence signal, and <code>\/historical-odds<\/code> gives you the whole ramp at no cost.<\/p>\n<pre class=\"wp-block-code\"><code>def limit_ramp(fixture_id, slug=\"pinnacle\", market=\"101\", outcome=\"101\"):\n    data = api(\"\/historical-odds\", fixtureId=fixture_id, bookmakers=slug) or {}\n    book = (data.get(\"bookmakers\") or {}).get(slug)\n    if not book:\n        return []\n    snaps = (((book.get(\"markets\") or {}).get(market) or {})\n             .get(\"outcomes\", {}).get(outcome, {}).get(\"players\", {}).get(\"0\")) or []\n    ramp, previous = [], None\n    for snap in snaps:\n        base = limit_base(snap)\n        if base != previous:\n            ramp.append((snap[\"createdAt\"][:16], snap[\"price\"], base))\n            previous = base\n    return ramp\n<\/code><\/pre>\n<p>Marseille against Strasbourg, read on 19 August 2026: 36 snapshots and 19 price changes since 6 July.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Timestamp (UTC)<\/th>\n<th>Days to kickoff<\/th>\n<th>Home price<\/th>\n<th>Implied base<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>2026-07-06 07:39<\/td>\n<td>46<\/td>\n<td>1.704<\/td>\n<td>$250<\/td>\n<\/tr>\n<tr>\n<td>2026-08-14 18:45<\/td>\n<td>7<\/td>\n<td>1.746<\/td>\n<td>$500<\/td>\n<\/tr>\n<tr>\n<td>2026-08-16 08:00<\/td>\n<td>5<\/td>\n<td>1.769<\/td>\n<td>$750<\/td>\n<\/tr>\n<tr>\n<td>2026-08-19 05:01<\/td>\n<td>2<\/td>\n<td>1.735<\/td>\n<td>$1,000<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>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.<\/p>\n<p>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&#8217;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 <a href=\"https:\/\/oddspapi.io\/blog\/kalshi-vs-polymarket-api\/\">Kalshi API vs Polymarket API<\/a>.<\/p>\n<h2>Step 12: Player props are keyed by player ID<\/h2>\n<p>On game lines, <code>players<\/code> has a single <code>\"0\"<\/code> key. On player props it is keyed by player ID, and each entry carries a <code>playerName<\/code> in &#8220;Last, First&#8221; format. Hardcoding <code>players[\"0\"]<\/code> makes every prop market look empty.<\/p>\n<pre class=\"wp-block-code\"><code>def player_prop(payload, names, family):\n    rows = []\n    for slug, b in (payload.get(\"bookmakerOdds\") or {}).items():\n        for market_id, market in (b.get(\"markets\") or {}).items():\n            if names.get(market_id) != family:\n                continue\n            for outcome_id, outcome in (market.get(\"outcomes\") or {}).items():\n                for player_id, price in (outcome.get(\"players\") or {}).items():\n                    if player_id == \"0\" or not isinstance(price, dict):\n                        continue\n                    if not price.get(\"price\") or price.get(\"active\") is False:\n                        continue\n                    rows.append((slug, price.get(\"playerName\"), outcome_id, price[\"price\"]))\n    return rows\n\n# ('ballybet', 'Gouiri, Amine', '10730', 2.2)\n# ('bet365', 'Chilwell, Ben', '102732', 4.333)\n<\/code><\/pre>\n<p>Marseille against Strasbourg carried 2,884 player-level prices across 13 prop families on 19 August 2026:<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Market<\/th>\n<th>Books<\/th>\n<th>Prices<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Anytime Goal Scorer<\/td>\n<td>14<\/td>\n<td>377<\/td>\n<\/tr>\n<tr>\n<td>First Goal Scorer<\/td>\n<td>13<\/td>\n<td>362<\/td>\n<\/tr>\n<tr>\n<td>Player Assists<\/td>\n<td>9<\/td>\n<td>180<\/td>\n<\/tr>\n<tr>\n<td>Player Goals<\/td>\n<td>9<\/td>\n<td>268<\/td>\n<\/tr>\n<tr>\n<td>Last Goal Scorer<\/td>\n<td>7<\/td>\n<td>206<\/td>\n<\/tr>\n<tr>\n<td>Over Under Player Shots On Goal<\/td>\n<td>5<\/td>\n<td>195<\/td>\n<\/tr>\n<tr>\n<td>Player To Be Carded<\/td>\n<td>5<\/td>\n<td>144<\/td>\n<\/tr>\n<tr>\n<td>Player Shots<\/td>\n<td>4<\/td>\n<td>502<\/td>\n<\/tr>\n<tr>\n<td>Player Offsides<\/td>\n<td>3<\/td>\n<td>46<\/td>\n<\/tr>\n<tr>\n<td>Player Fouls Committed<\/td>\n<td>1<\/td>\n<td>50<\/td>\n<\/tr>\n<tr>\n<td>Player Tackles<\/td>\n<td>1<\/td>\n<td>75<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>DraftKings is the only book quoting tackles and fouls committed. If you want the same treatment for US sports, see the <a href=\"https:\/\/oddspapi.io\/blog\/player-props-api-nfl-nba-mlb-odds-python\/\">player props API guide<\/a>.<\/p>\n<h2>What the books disagree about, and what they do not<\/h2>\n<p>One test worth publishing as a negative result. De-vig each sportsbook&#8217;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.<\/p>\n<p>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.<\/p>\n<p>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 <a href=\"https:\/\/oddspapi.io\/blog\/line-shopping-python-best-odds\/\">shopping the line across every independent quote on the board<\/a> rather than picking a house book.<\/p>\n<h2>Five French-licensed books quote Ligue 1, and none of them is your best price<\/h2>\n<p>Measured on 24 August 2026 across the nine matchday 4 fixtures, a no-filter <code>\/odds<\/code> 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: <code>bet365.fr<\/code>, <code>pmu<\/code>, <code>pokerstars.fr<\/code>, <code>unibet.fr<\/code> and <code>winamax.fr<\/code>. That is the thinnest local set in the big five. Germany fields twelve on the same measurement basis.<\/p>\n<p>Four of the five post a complete three-way often enough to score a margin. <code>pokerstars.fr<\/code> 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.<\/p>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Book<\/th>\n<th>Median 1X2 margin<\/th>\n<th>Rank of 163<\/th>\n<th>Fixtures<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>pinnacle<\/code><\/td>\n<td>3.52%<\/td>\n<td>16<\/td>\n<td>reference<\/td>\n<\/tr>\n<tr>\n<td><code>pmu<\/code><\/td>\n<td>6.70%<\/td>\n<td>83<\/td>\n<td>8 of 9<\/td>\n<\/tr>\n<tr>\n<td><code>unibet.fr<\/code><\/td>\n<td>8.76%<\/td>\n<td>138<\/td>\n<td>9 of 9<\/td>\n<\/tr>\n<tr>\n<td><code>winamax.fr<\/code><\/td>\n<td>8.93%<\/td>\n<td>141<\/td>\n<td>9 of 9<\/td>\n<\/tr>\n<tr>\n<td><code>bet365.fr<\/code><\/td>\n<td>12.65%<\/td>\n<td>159<\/td>\n<td>8 of 9<\/td>\n<\/tr>\n<tr>\n<td><code>pokerstars.fr<\/code><\/td>\n<td>no complete 1X2<\/td>\n<td>unranked<\/td>\n<td>unranked<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<p>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: <code>polymarket.us<\/code> 1.00%, <code>kalshi<\/code> 1.00%, <code>1xbet<\/code> 1.55%, <code>duel<\/code> 1.88%, <code>betfair-ex<\/code> 1.95%, <code>polymarket<\/code> 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.<\/p>\n<p>One caveat on the catalogue. <code>genybet.fr<\/code>, <code>feelingbet.fr<\/code>, <code>netbet.fr<\/code>, <code>fdj<\/code> and <code>zebet.fr<\/code> 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 <code>\/odds<\/code> per fixture before you promise a reader that a given book is on the board.<\/p>\n<h2>Ligue 1 quick reference<\/h2>\n<figure class=\"wp-block-table\">\n<table>\n<thead>\n<tr>\n<th>Item<\/th>\n<th>Value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Sport ID<\/td>\n<td>10 (<code>soccer<\/code>)<\/td>\n<\/tr>\n<tr>\n<td>Tournament ID<\/td>\n<td>34, category &#8220;France&#8221;<\/td>\n<\/tr>\n<tr>\n<td>Three-way market<\/td>\n<td>101, outcomes 101 home \/ 102 draw \/ 103 away<\/td>\n<\/tr>\n<tr>\n<td>Asian handicap<\/td>\n<td><code>marketName: \"Asian Handicap\"<\/code>, <code>period: \"fulltime\"<\/code>, one ID per rung<\/td>\n<\/tr>\n<tr>\n<td>Books per fixture<\/td>\n<td>174 median, 177 max, 91 min (28 to 30 Aug round)<\/td>\n<\/tr>\n<tr>\n<td>Dedupe collapse<\/td>\n<td>1,396 slug-quotes to 778 independent, 44.3%<\/td>\n<\/tr>\n<tr>\n<td>Prices active<\/td>\n<td>87.9% across the round<\/td>\n<\/tr>\n<tr>\n<td>Sharp reference<\/td>\n<td><code>pinnacle<\/code>, 3.52% median 1X2, rank 16 of 163<\/td>\n<\/tr>\n<tr>\n<td>Player props<\/td>\n<td>1,801 to 2,884 prices per fixture (21 to 23 Aug round)<\/td>\n<\/tr>\n<tr>\n<td>Rate limit<\/td>\n<td>Per endpoint, ~1 call\/second, honour <code>retryMs<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n<h2>Where to go next<\/h2>\n<p>The same probe runs on any competition. Swap the tournament ID and read the coverage table before you write the schema:<\/p>\n<ul>\n<li><a href=\"https:\/\/oddspapi.io\/blog\/premier-league-odds-api\/\">Premier League Odds API<\/a> for the English board and the SBOBet handicap split<\/li>\n<li><a href=\"https:\/\/oddspapi.io\/blog\/la-liga-odds-api\/\">La Liga Odds API<\/a> for why market count is not liquidity on the exchanges<\/li>\n<li><a href=\"https:\/\/oddspapi.io\/blog\/bundesliga-odds-api\/\">Bundesliga Odds API<\/a> for how a negative exchange margin can sit on $3.50 of depth<\/li>\n<li><a href=\"https:\/\/oddspapi.io\/blog\/serie-a-odds-api\/\">Serie A Odds API<\/a> for the fourteen-day exchange window<\/li>\n<li><a href=\"https:\/\/oddspapi.io\/blog\/football-odds-api-soccer-data\/\">Football Odds API<\/a> for the sport-wide view across 1,762 tournaments<\/li>\n<\/ul>\n<p>For the maths that sits on top of this feed, read the <a href=\"https:\/\/oddspapi.io\/blog\/no-vig-odds-api\/\">no-vig odds guide<\/a>, the <a href=\"https:\/\/oddspapi.io\/blog\/consensus-odds-fair-odds-calculator-python\/\">consensus odds calculator<\/a>, the <a href=\"https:\/\/oddspapi.io\/blog\/asian-handicap-calculator-python\/\">Asian handicap calculator<\/a> and <a href=\"https:\/\/oddspapi.io\/blog\/why-sharps-bet-asian-handicap\/\">why sharps bet the handicap<\/a>. If you need limits rather than prices, start with the <a href=\"https:\/\/oddspapi.io\/blog\/betting-limits-api-stake-sizing\/\">betting limits guide<\/a>. To stop polling and take pushes instead, move to the <a href=\"https:\/\/oddspapi.io\/blog\/websocket-odds-api-real-time-betting-data\/\">WebSocket feed<\/a>.<\/p>\n<h2>Get your key<\/h2>\n<p>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.<\/p>\n<p><a href=\"https:\/\/oddspapi.io\/\">Get your free OddsPapi API key<\/a> and run the coverage probe above on your own competition.<\/p>\n<h2>FAQ<\/h2>\n<h3>What is the Ligue 1 tournament ID in the OddsPapi API?<\/h3>\n<p>Ligue 1 is <code>tournamentId<\/code> 34 with <code>categoryName<\/code> &#8220;France&#8221;. Seven tournaments in the soccer catalogue are named exactly &#8220;Ligue 1&#8221;, covering Algeria, Tunisia, Ivory Coast, Senegal, Mali and Burkina Faso as well as France, so match the name and the category together.<\/p>\n<h3>How many bookmakers quote a Ligue 1 match?<\/h3>\n<p>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.<\/p>\n<h3>Does Pinnacle cover Ligue 1?<\/h3>\n<p>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 <code>\/historical-odds<\/code> confirms it never opened that fixture. Check for the slug in each payload rather than assuming a sharp reference exists.<\/p>\n<h3>Are card and corner markets available for Ligue 1?<\/h3>\n<p>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 <code>Player To Be Carded<\/code> on every fixture, while Bet365 alone posted the match and team booking totals.<\/p>\n<h3>Why does a single bookmaker show a negative margin?<\/h3>\n<p>Because you paired legs from different markets. <code>Corners - 1X2<\/code> 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.<\/p>\n<h3>Is Ligue 1 historical odds data free?<\/h3>\n<p>Yes. <code>\/historical-odds<\/code> 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.<\/p>\n<p><script type=\"application\/ld+json\">\n{\n  \"@context\": \"https:\/\/schema.org\",\n  \"@type\": \"FAQPage\",\n  \"mainEntity\": [\n    {\n      \"@type\": \"Question\",\n      \"name\": \"What is the Ligue 1 tournament ID in the OddsPapi API?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"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.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"How many bookmakers quote a Ligue 1 match?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"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 percent collapse.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Does Pinnacle cover Ligue 1?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Pinnacle quotes Ligue 1 at a 3.52 percent 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.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Are card and corner markets available for Ligue 1?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"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.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Why does a single bookmaker show a negative margin?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"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 percent. One book never offers a free arbitrage. Reject any two-sided pair whose inverse prices sum to 1.0 or below.\"\n      }\n    },\n    {\n      \"@type\": \"Question\",\n      \"name\": \"Is Ligue 1 historical odds data free?\",\n      \"acceptedAnswer\": {\n        \"@type\": \"Answer\",\n        \"text\": \"Yes. The historical-odds endpoint 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 dollar base to 1000 dollars.\"\n      }\n    }\n  ]\n}\n<\/script><\/p>\n<p><!--\nFocus Keyphrase: ligue 1 odds api\nSEO Title: Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way\nMeta Description: Pull live Ligue 1 odds from 174 bookmakers per fixture in Python. Free OddsPapi tier, tested code, French books ranked, and a per-fixture coverage probe.\nSlug: ligue-1-odds-api\n--><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Pull live Ligue 1 odds from 174 bookmakers per fixture in Python. Free OddsPapi tier, tested code, French books ranked, and a per-fixture coverage probe.<\/p>\n","protected":false},"author":2,"featured_media":3673,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[8,91,9,11,15],"class_list":["post-3672","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-how-to-guides","tag-free-api","tag-ligue-1","tag-odds-api","tag-python","tag-soccer"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way | OddsPapi Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way | OddsPapi Blog\" \/>\n<meta property=\"og:description\" content=\"Pull live Ligue 1 odds from 174 bookmakers per fixture in Python. Free OddsPapi tier, tested code, French books ranked, and a per-fixture coverage probe.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\" \/>\n<meta property=\"og:site_name\" content=\"OddsPapi Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-26T10:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-07T14:01:57+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"2560\" \/>\n\t<meta property=\"og:image:height\" content=\"1429\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\n<meta name=\"author\" content=\"Odds API Writer\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/oddspapi.io\/logo-v2.webp\" \/>\n<meta name=\"twitter:creator\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:site\" content=\"@oddspapiapi\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Odds API Writer\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"21 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\"},\"author\":{\"name\":\"Odds API Writer\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\"},\"headline\":\"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way\",\"datePublished\":\"2026-08-26T10:00:00+00:00\",\"dateModified\":\"2026-09-07T14:01:57+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\"},\"wordCount\":2908,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp\",\"keywords\":[\"Free API\",\"Ligue 1\",\"Odds API\",\"Python\",\"Soccer\"],\"articleSection\":[\"How To Guides\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\",\"name\":\"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way | OddsPapi Blog\",\"isPartOf\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp\",\"datePublished\":\"2026-08-26T10:00:00+00:00\",\"dateModified\":\"2026-09-07T14:01:57+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp\",\"width\":2560,\"height\":1429,\"caption\":\"Ligue 1 Odds API - OddsPapi API Blog\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/oddspapi.io\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#website\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"name\":\"OddsPapi\",\"description\":\"Sports Odds API Tutorials &amp; Guides\",\"publisher\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\"},\"alternateName\":\"Odds Papi\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/oddspapi.io\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#organization\",\"name\":\"OddsPapi\",\"url\":\"https:\/\/oddspapi.io\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"contentUrl\":\"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png\",\"width\":135,\"height\":135,\"caption\":\"OddsPapi\"},\"image\":{\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/oddspapiapi\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13\",\"name\":\"Odds API Writer\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g\",\"caption\":\"Odds API Writer\"},\"url\":\"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way | OddsPapi Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/","og_locale":"en_US","og_type":"article","og_title":"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way | OddsPapi Blog","og_description":"Pull live Ligue 1 odds from 174 bookmakers per fixture in Python. Free OddsPapi tier, tested code, French books ranked, and a per-fixture coverage probe.","og_url":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/","og_site_name":"OddsPapi Blog","article_published_time":"2026-08-26T10:00:00+00:00","article_modified_time":"2026-09-07T14:01:57+00:00","og_image":[{"width":2560,"height":1429,"url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp","type":"image\/webp"}],"author":"Odds API Writer","twitter_card":"summary_large_image","twitter_image":"https:\/\/oddspapi.io\/logo-v2.webp","twitter_creator":"@oddspapiapi","twitter_site":"@oddspapiapi","twitter_misc":{"Written by":"Odds API Writer","Est. reading time":"21 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#article","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/"},"author":{"name":"Odds API Writer","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13"},"headline":"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way","datePublished":"2026-08-26T10:00:00+00:00","dateModified":"2026-09-07T14:01:57+00:00","mainEntityOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/"},"wordCount":2908,"commentCount":0,"publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp","keywords":["Free API","Ligue 1","Odds API","Python","Soccer"],"articleSection":["How To Guides"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/","url":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/","name":"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way | OddsPapi Blog","isPartOf":{"@id":"https:\/\/oddspapi.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage"},"thumbnailUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp","datePublished":"2026-08-26T10:00:00+00:00","dateModified":"2026-09-07T14:01:57+00:00","breadcrumb":{"@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#primaryimage","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2026\/08\/ligue-1-odds-api-scaled.webp","width":2560,"height":1429,"caption":"Ligue 1 Odds API - OddsPapi API Blog"},{"@type":"BreadcrumbList","@id":"https:\/\/oddspapi.io\/blog\/ligue-1-odds-api\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/oddspapi.io\/blog\/"},{"@type":"ListItem","position":2,"name":"Ligue 1 Odds API: 174 Books a Fixture, 163 With a Full Three-Way"}]},{"@type":"WebSite","@id":"https:\/\/oddspapi.io\/blog\/#website","url":"https:\/\/oddspapi.io\/blog\/","name":"OddsPapi","description":"Sports Odds API Tutorials &amp; Guides","publisher":{"@id":"https:\/\/oddspapi.io\/blog\/#organization"},"alternateName":"Odds Papi","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/oddspapi.io\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/oddspapi.io\/blog\/#organization","name":"OddsPapi","url":"https:\/\/oddspapi.io\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","contentUrl":"https:\/\/oddspapi.io\/blog\/wp-content\/uploads\/2025\/11\/oddspapi.png","width":135,"height":135,"caption":"OddsPapi"},"image":{"@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/oddspapiapi"]},{"@type":"Person","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/b6f21e649c4f556f0a95c23a0f1efa13","name":"Odds API Writer","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/oddspapi.io\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/33b204f24af3d02e35b25ae730c0536121ca6a783fdb196e7611c9e49fcd13eb?s=96&d=mm&r=g","caption":"Odds API Writer"},"url":"https:\/\/oddspapi.io\/blog\/author\/andy-lavelle\/"}]}},"_links":{"self":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/3672","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/comments?post=3672"}],"version-history":[{"count":5,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/3672\/revisions"}],"predecessor-version":[{"id":3901,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/posts\/3672\/revisions\/3901"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media\/3673"}],"wp:attachment":[{"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/media?parent=3672"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/categories?post=3672"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oddspapi.io\/blog\/wp-json\/wp\/v2\/tags?post=3672"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}