Darts Odds API: 154 Books on a PDC Match, 109 on Most 180s
Looking for a darts odds API with PDC match prices, the Most 180s market and closing lines you can backtest against? OddsPapi returns all three from the same fixture ID. Call /v4/odds for the live board, read market 191 for the match winner and market 193 for Most 180s, then call /v4/historical-odds once the match is over to get the closing prices.
We measured both ends on 25 September 2026. The 16 first-round boards of the PDC World Grand Prix drew a median 154 books on the winner and 109 on Most 180s, three days before the first dart. On 170 played PDC matches from late August to 20 September, the closing lines at Pinnacle, bet365 and FanDuel landed within half a point of margin of each other. On the 97 matches that all three books priced, their forecasts scored within 0.004 Brier of each other. This guide shows how to find the fixtures, read both markets without the traps, and pull the closing lines.
Why darts data breaks generic odds code
Darts looks like tennis: two players, one winner, no draw. Code written for a two-way sport runs on the winner market and then fails on three things.
- Most 180s has three outcomes. The tie is a real result and the books price it. Across 1,839 complete World Grand Prix quotes, the median quote put the tie at 20.65%. DraftKings posts the market with two legs and no tie on 16 of 16 boards, so any margin or fair price you compute from that quote is wrong. Count the legs before you compute anything.
- The depth sits in two markets.
WinnerandMost 180scarry the board. The Total Legs and Leg Handicap ladders drew 1 to 14 books a rung on the same fixtures (median 5), and 905 of those 1,422 quotes sat at a placeholder price of 1.0 or 1.01. - Floor events fill late. A Modus Super Series match held 151 to 194 books inside ten hours of the start and none a day out. The World Grand Prix, a televised major, was priced 77 hours out. Poll the board close to the start for the smaller events.
| What a darts model needs | What the v4 feed ships (measured 25 Sep 2026) |
|---|---|
| Match winner depth | Median 154 books on market 191 across 16 World Grand Prix boards |
| A player-performance market | Most 180s (market 193) on a median 109 books, tie leg included |
| A sharp reference | Pinnacle on the winner, with a stake limit on each price; Betfair Exchange at a 1.93% median margin |
| Closing lines for backtests | /v4/historical-odds on played European Tour and World Series matches, on the free tier |
| Results to grade against | /v4/scores returns the final score under periods.result |
| Settled outcomes | /v4/settlements grades Winner, Total Legs and Leg Handicap per outcome |
Step 1: list the darts tournaments
Darts is sport ID 19. /v4/fixtures returns every match in a window of up to ten days, and tournamentName tells you the event. The key goes in the query string, never in a header. A 404 on /fixtures means an empty window, so the helper returns None instead of raising.
import os, time, requests
from datetime import datetime, timedelta, timezone
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
def get(path, **params):
params["apiKey"] = API_KEY
for _ in range(4):
r = requests.get(f"{BASE}/{path}", params=params, timeout=120)
if r.status_code == 429:
time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
continue
if r.status_code == 404: # empty window or no history for this fixture
return None
r.raise_for_status()
return r.json()
raise RuntimeError("rate limited")
def ts(s):
return datetime.fromisoformat(s.replace("Z", "+00:00"))
from collections import Counter
now = datetime.now(timezone.utc)
fixtures = get("fixtures", sportId=19, # 19 = darts
**{"from": now.strftime("%Y-%m-%d"),
"to": (now + timedelta(days=9)).strftime("%Y-%m-%d")}) or []
rows = Counter((f["tournamentId"], f["tournamentName"]) for f in fixtures)
priced = Counter((f["tournamentId"], f["tournamentName"]) for f in fixtures if f["hasOdds"])
for (tid, name), n in rows.most_common():
print(f"{tid:>6} {name:<32}{n:>4} fixtures {priced[(tid, name)]:>4} with odds")
Output on 25 September 2026:
33994 Modus Super Series 80 fixtures 20 with odds
751 World Grand Prix 30 fixtures 16 with odds
Two different kinds of event share the list. The World Grand Prix is a PDC major on television, with its first round on 28 and 29 September. The Modus Super Series runs all day, every day, with a new match about every 20 minutes. Look at the last column: 20 of 80 Modus fixtures carry hasOdds: true, and all 20 start on the same day as the call. hasOdds is a flag, not a depth signal, so open the board before you commit to a fixture.
Step 2: read the winner and Most 180s
This snippet opens the next twelve priced boards, keeps the deepest one, and reads both markets. A quote counts only if the book is not suspended, every leg is active, every price sits above 1.0, and the outcome set matches the market exactly. That last check is the one that drops a Most 180s quote without its tie leg.
# Reuses get(), ts(), now and fixtures from Step 1.
import statistics
WINNER, MOST_180S = "191", "193" # outcomes 191/192 and 193 (1) / 194 (tie) / 195 (2)
LEGS = {WINNER: ("191", "192"), MOST_180S: ("193", "194", "195")}
def quotes(board, market):
out = {}
for slug, book in board.items():
if "+" in slug or slug == "demo": # internal test feeds
continue
if book.get("suspended") or book.get("bookmakerIsActive") is False:
continue
mkt = (book.get("markets") or {}).get(market)
if not mkt or set(mkt["outcomes"]) != set(LEGS[market]):
continue # a Most 180s quote without its tie leg
legs = [mkt["outcomes"][o]["players"]["0"] for o in LEGS[market]]
if all(p["active"] and p["price"] > 1 for p in legs):
out[slug] = tuple(p["price"] for p in legs)
return out
margin = lambda q: (sum(1 / p for p in q) - 1) * 100
upcoming = sorted((f for f in fixtures if f["hasOdds"] and ts(f["startTime"]) > now),
key=lambda f: ts(f["startTime"]))
best = None
for fx in upcoming[:12]: # keep the deepest of the next 12 boards
time.sleep(1.0)
board = (get("odds", fixtureId=fx["fixtureId"]) or {}).get("bookmakerOdds", {})
if best is None or len(board) > len(best[1]):
best = (fx, board)
fx, board = best
print(fx["tournamentName"], "|", fx["participant1Name"], "v", fx["participant2Name"],
"|", fx["startTime"], "|", len(board), "books on the fixture")
for market, name in ((WINNER, "Winner"), (MOST_180S, "Most 180s")):
q = quotes(board, market)
posted = sum(1 for b in board.values() if market in (b.get("markets") or {}))
print(f"\n{name}: {posted} books post it, {len(q)} tradeable, "
f"{len(set(q.values()))} distinct price tuples, "
f"median margin {statistics.median(map(margin, q.values())):.2f}%")
for slug in ("pinnacle", "betfair-ex", "orbitx", "bet365", "fanduel", "draftkings", "paddypower"):
if slug in q:
print(f" {slug:<11}{' '.join(f'{p:>6}' for p in q[slug])} {margin(q[slug]):5.2f}%")
elif market in (board.get(slug, {}).get("markets") or {}):
print(f" {slug:<11}posted, dropped: {sorted(board[slug]['markets'][market]['outcomes'])}")
Output on 25 September 2026, minutes before a Modus Super Series match:
Modus Super Series | Winder, Scott v Haines, Johnny | 2026-09-25T13:45:00.000Z | 194 books on the fixture
Winner: 194 books post it, 185 tradeable, 43 distinct price tuples, median margin 8.35%
betfair-ex 3.9 1.25 5.64%
orbitx 3.9 1.25 5.64%
bet365 3.75 1.22 8.63%
draftkings 3.2 1.323 6.84%
paddypower 3.2 1.3 8.17%
Most 180s: 120 books post it, 118 tradeable, 25 distinct price tuples, median margin 11.43%
bet365 2.75 2.87 2.5 11.21%
paddypower 2.75 2.88 2.6 9.55%
Look at the gap between 185 tradeable winner quotes and 43 distinct price tuples. Many slugs are regional skins of one trading desk, and they post byte-identical prices. orbitx is one of four exchanges that mirror Betfair (fair999-ex, orbitx, piwi247-ex, sharpxch); all four matched betfair-ex on 14 of 14 World Grand Prix boards, and the catalogue flags them with cloneOf: betfair-ex. Dedupe on the price tuple before you average, or one desk votes thirty times. The consensus odds calculator shows the full method.
The World Grand Prix board, 16 first-round matches
We ran the same reader over all 16 priced World Grand Prix boards at 13:12 UTC on 25 September, 77 to 104 hours before the matches:
| Median across 16 boards | Winner (191) | Most 180s (193) |
|---|---|---|
| Books that post the market | 154 | 109 |
| Tradeable quotes | 148 | 106 |
| Distinct price tuples | 35.5 | 22.5 |
| Median margin | 6.36% | 11.33% |
Most 180s costs you almost twice the margin of the winner, and the gap between books is wider too. Here are the named books, ranked on their median margin across the 16 boards:
| Book | Winner margin (rank of 149) | Most 180s margin (rank of 106) |
|---|---|---|
betfair-ex |
1.93% (1) | not posted |
fanduel |
4.90% (6) | 6.30% (1) |
betway |
5.36% (14) | 10.88% (37) |
paddypower |
5.51% (17) | 10.49% (30) |
draftkings |
5.66% (31) | two legs, unranked |
pinnacle |
6.03% (53) | not posted |
bet365 |
6.78% (94) | 10.81% (34) |
1xbet |
7.53% (107) | 11.00% (48) |
FanDuel ranks first on Most 180s (on the 12 boards where it posted the market) and sixth on the winner. Pinnacle quoted the winner on all 16 boards and no other market. Its stake limit follows the max-win rule: the underdog side carried a flat 125 on every board, and the favourite side carried 125 divided by the favourite’s price minus one, so a 1.151 favourite showed a limit of 827. The betting limits guide covers that formula across sports.
Step 3: pull the closing lines
The snippet below takes every finished match of the Hungarian Darts Trophy, reads the result from /v4/scores, and reads the last active pre-match price from /v4/historical-odds at three books. It reuses get() and ts() from Step 1. The loop spends about 95 requests, so budget for it on the free plan.
# Reuses get() and ts() from Step 1.
import statistics
BOOKS = "pinnacle,bet365,fanduel" # /historical-odds takes up to three slugs a call
def close(snaps, start):
"""Last active price before the start. Snapshots keep recording in-play."""
live = [s for s in snaps if ts(s["createdAt"]) < start and s["active"] and s["price"] > 1]
return max(live, key=lambda s: s["createdAt"]) if live else None
played = [f for f in get("fixtures", sportId=19, tournamentId=31615, # Hungarian Darts Trophy
**{"from": "2026-08-28", "to": "2026-09-01"}) or []
if f["statusName"] == "Finished"]
rows = {slug: [] for slug in BOOKS.split(",")}
for fx in played:
time.sleep(1.0)
result = ((get("scores", fixtureId=fx["fixtureId"]) or {})
.get("scores", {}).get("periods", {}).get("result"))
time.sleep(1.0)
hist = get("historical-odds", fixtureId=fx["fixtureId"], bookmakers=BOOKS)
if not result or not hist:
continue
time.sleep(4.6) # /historical-odds wants a longer gap
p1_won = result["participant1Score"] > result["participant2Score"]
start = ts(fx["trueStartTime"] or fx["startTime"])
for slug, book in hist["bookmakers"].items():
legs = book.get("markets", {}).get("191", {}).get("outcomes", {})
if "191" not in legs or "192" not in legs:
continue
c1 = close(legs["191"]["players"]["0"], start)
c2 = close(legs["192"]["players"]["0"], start)
if c1 and c2:
p1 = (1 / c1["price"]) / (1 / c1["price"] + 1 / c2["price"]) # de-vigged
rows[slug].append({"margin": 1 / c1["price"] + 1 / c2["price"] - 1,
"brier": 2 * (p1 - p1_won) ** 2,
"fav_won": (p1 > 0.5) == p1_won,
"limit": min(c1["limit"], c2["limit"]) if c1["limit"] and c2["limit"] else None})
print(f"{len(played)} finished matches")
print(f"{'book':<10}{'closes':>7}{'margin':>9}{'fav won':>9}{'Brier':>8}{'limit':>7}")
for slug, r in rows.items():
if not r:
continue
lims = [x["limit"] for x in r if x["limit"]]
print(f"{slug:<10}{len(r):>7}{statistics.median(x['margin'] for x in r) * 100:>8.2f}%"
f"{sum(x['fav_won'] for x in r) / len(r) * 100:>8.1f}%"
f"{statistics.mean(x['brier'] for x in r):>8.4f}"
f"{statistics.median(lims) if lims else '-':>7}")
47 finished matches
book closes margin fav won Brier limit
pinnacle 41 5.03% 63.4% 0.4734 250.0
bet365 41 5.40% 65.9% 0.4793 -
fanduel 44 5.47% 61.4% 0.4765 -
The closing price is the last active snapshot before the start time. Snapshots keep recording in-play, so the createdAt < start filter matters. Brier here is the two-outcome score, where a coin flip scores 0.5 and a perfect forecast scores 0. One event is a small sample, so we ran the same code over four PDC events: the Hungarian, Czech and Flanders European Tour events and the World Series of Darts Finals, 170 played matches from 28 August to 20 September 2026.
| Book | Closes | Median close margin | Favourite won | Brier, own sample | Brier, 97 shared matches |
|---|---|---|---|---|---|
pinnacle |
149 | 5.19% | 103 of 148 (69.6%) | 0.4040 | 0.4118 |
bet365 |
127 | 5.44% | 86 of 122 (70.5%) | 0.4089 | 0.4131 |
fanduel |
112 | 5.64% | 77 of 112 (68.8%) | 0.3966 | 0.4153 |
Read the last column first. On the 97 matches all three books priced, the three closes forecast the winner within 0.0035 Brier of each other, and the average of the three scored 0.4116. The own-sample column ranks FanDuel first only because FanDuel priced a different set of matches. Compare books on a shared sample or the ranking means nothing. Pinnacle’s closing limit on the smaller side was 250 on 138 of 149 matches, a fraction of what it takes on football, so treat any one darts close as one opinion and average the deduplicated board. The no-vig guide shows three ways to strip the margin first.
Most 180s closes on the same 170 matches tell the same story as the live board. bet365 closed the three-way at a median 10.49% margin across 115 matches, FanDuel at 6.52% across 102. Both put the tie near 18% (18.10% and 18.48%). If you bet 180s, the book you pick costs more than any model edge you are likely to find, so shop the line before you size the bet.
What to build with it
- A Most 180s price scanner. Run Step 2 on every World Grand Prix or Modus board inside ten hours and flag any book whose tie leg sits above the deduplicated consensus. With 22 distinct tuples on a median board, the outliers are easy to see.
- A darts rating model. Step 3 gives you a labelled closing line for every played European Tour match. Train on the results and score your forecast against the 0.41 Brier that the market closes hit. The backtesting guide covers the walk-forward loop, and the football labels study shows the same scoring on 250 fixtures.
- A settlement grader.
/v4/settlementsreturns WIN, LOSE or PUSH for every Winner, Total Legs and Leg Handicap outcome on a played match. We checked it on a World Series Finals match that ended 11-5: 44 Total Legs outcomes and 50 Leg Handicap outcomes came back graded.
If polling falls behind in-play, the same fixture IDs stream on the WebSocket feed.
Darts odds API FAQ
Is there a darts odds API with PDC prices?
Yes. OddsPapi’s /v4/odds returns match prices for PDC events such as the World Grand Prix, the European Tour and the World Series, and for the Modus Super Series. On 25 September 2026 the median World Grand Prix first-round board drew 154 books on the match winner.
Which market IDs does darts use?
Market 191 is the match winner, with outcome 191 for participant 1 and 192 for participant 2. Market 193 is Most 180s, with 193 for participant 1, 194 for the tie and 195 for participant 2. Total Legs and Leg Handicap use one market ID per line, for example 1930 for Total Legs 9.5.
Does the API return Most 180s odds?
Yes. Market 193 carried a median 109 books across the 16 World Grand Prix first-round boards we measured on 25 September 2026, at a median margin of 11.33%. FanDuel posted the tightest three-way, at 6.30%.
Can I get historical darts odds for backtesting?
Yes. /v4/historical-odds returns the full price history for played PDC matches on the free tier, up to three bookmakers a call. Across 170 European Tour and World Series matches from August and September 2026, Pinnacle’s median closing margin on the winner was 5.19%.
How accurate are darts closing odds?
On 97 PDC matches priced by Pinnacle, bet365 and FanDuel, the favourite at the close won 68% to 71% of the time, and the three closes scored between 0.4118 and 0.4153 Brier, where a coin flip scores 0.5.
Pull your first darts board
Every call in this guide runs on the free key: 350+ bookmakers in the catalogue, free historical odds for your closing-line backtests, native three-way markets like Most 180s, and the same fixture IDs on the WebSocket feed when polling gets too slow for in-play. Get a free API key and run Step 2 on the next PDC match on the schedule.