Moneyline Odds API: Find the Winner Market in 59 Sports
Looking for a moneyline odds API that runs the same code on the NFL, the NHL and the Premier League? OddsPapi defines the match winner market for 59 sports and serves it through one endpoint, and you can compute its market ID without a lookup table: take the sport ID, multiply by 10, add 1. NFL is sport 14, so the NFL moneyline is market 141. NHL is 15, so its moneyline is 151. The two outcome IDs follow the same rule: 141 is participant 1 and 142 is participant 2.
We pulled 28 pregame boards across eight in-season sports on 14 October 2026, between 13:10 and 13:15 UTC. Each NFL board carried the moneyline from 229 to 252 bookmakers, every quote with the decimal price and the American price side by side. Soccer is the one sport where the formula lands on a three-way market, and four US sports ship a second, three-way “regulation time” market next to the moneyline that settles on a different clock. This guide shows how to resolve the right ID, pull the board, and keep the two clocks apart.
The Moneyline Across Eight Sports
Each row is the median over the boards we pulled for that tournament. “Distinct prices” counts unique price pairs after we removed duplicate feeds, because many slugs are skins of one feed and quote byte-identical prices. Margins are the median overround across those distinct pairs. The last two columns cover the twin market: the three-way regulation-time result in the US sports, and Draw No Bet for soccer. Do not merge a twin into the moneyline (Step 4).
| Sport (boards) | Moneyline ID | Books | Distinct prices | Median margin | Pinnacle margin | Twin market | Twin books / margin |
|---|---|---|---|---|---|---|---|
| NFL (4) | 141 | 244.5 | 71.5 | 5.00% | 3.04% | 143 | 70 / 8.82% |
| NHL (2) | 151 | 234.5 | 68 | 5.22% | 2.41% | 153 | 229 / 7.80% |
| MLB postseason (2) | 131 | 230.5 | 63.5 | 5.27% | 2.27% | 133 | 40.5 / 10.36% |
| NBA preseason (4) | 111 | 212 | 57 | 6.14% | 6.28% | 113 | 51 / 9.84% |
| UFC (4) | 201 | 210.5 | 47 | 5.38% | 3.87% | 203 | 47.5 / 8.03% |
| Premier League (4) | 101 (3-way) | 254.5 | n/a | 6.02% | 3.3% | 10214 Draw No Bet | 212.5 / 7.02% |
| WTA Wuhan (4) | 121 | 178.5 | 44 | 5.98% | 2.94% (1 board) | none | n/a |
| Counter-Strike 2 (4) | 171 | 102.5 | 19 | 7.83% | 7.25% (2 boards) | none quoted | n/a |
Three things stand out. Pinnacle runs at 43% to 61% of the board’s median margin on the NFL, NHL and MLB moneylines, so it stays the fair-value anchor there. On NBA preseason it sits level with the median, which tells you the preseason line is still soft at the sharp end too. And the three-way twin costs you 2.6 to 5.1 points of extra margin in the four US sports, because it adds a draw leg that the books price wide.
Why a Cross-Sport Moneyline Trips Up Most Code
A moneyline sounds like the simplest market there is. Two participants, two prices, pick the winner. The trouble starts when the same code has to run on more than one sport:
- Every sport has its own market ID. Hardcode 141 and your NHL job returns nothing. Most feeds make you look each one up.
- “Winner” has two clocks. In the NFL, NBA, MLB and NHL the moneyline includes overtime or extra innings. A second market, Regular Time Result, settles on the score at the end of regulation and pays a draw. On a Capitals v Canadiens board, 229 books priced that 60-minute market.
- Soccer flips the default. The soccer “moneyline” a US bettor means is the three-way 1X2. The two-way equivalent is Draw No Bet, a different market with a different ID.
- Book counts lie. 252 books quoted the Broncos v Seahawks moneyline, but only 76 distinct price pairs existed. Average over 252 and you count the biggest white-label family 21 times.
| Job | Per-book APIs or a single-sport feed | OddsPapi |
|---|---|---|
| Find the moneyline market | A different key, name or endpoint per book and per sport | sportId * 10 + 1, confirmed by name from /v4/markets |
| Coverage | One sportsbook, or one league | 350+ bookmakers in the catalogue; 229 to 252 on each NFL moneyline we pulled |
| American odds | Convert yourself | priceAmerican on every quote: 11,046 of 11,046 legs on our 28 boards |
| Sharp reference | Separate account | Pinnacle on the same board, with its stake limit |
| Closing lines for backtests | Paid add-on or none | /v4/historical-odds on the free tier |
| Live updates | Polling | WebSocket push on every price change |
Step 1: Resolve the Moneyline ID for Any Sport
The formula gets you a candidate. Confirm it against /v4/markets by name before you trust it, and resolve the three-way twin by name as well, because the twin does not sit at a fixed offset. In Counter-Strike 2 the 1X2 market is 178077, nowhere near 171.
import os, requests
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
sports = requests.get(f"{BASE}/sports", params={"apiKey": API_KEY}).json()
markets = requests.get(f"{BASE}/markets", params={"apiKey": API_KEY}).json()
by_id = {m["marketId"]: m for m in markets}
def moneyline(sport_id):
"""The match winner is market sportId * 10 + 1. Confirm it by name, never trust the formula alone."""
m = by_id.get(sport_id * 10 + 1)
if not m or m["sportId"] != sport_id:
return None
if not (m["marketName"].startswith("Winner") or m["marketName"] == "Full Time Result"):
return None
return m
def regulation_twins(sport_id):
"""Three-way markets that settle the same match on a different clock. Resolve by name."""
return sorted(m["marketId"] for m in markets
if m["sportId"] == sport_id and m["marketName"] in ("Regular Time Result", "1X2")
and m["handicap"] == 0 and m["period"] in ("fulltime", "result") and not m["playerProp"])
found = {s["sportId"]: moneyline(s["sportId"]) for s in sports}
print(sum(1 for m in found.values() if m and m["marketLength"] == 2), "sports ship a two-way winner at sportId*10+1")
for sid in (10, 11, 12, 13, 14, 15, 17, 20):
m = found[sid]
outs = sorted(o["outcomeId"] for o in m["outcomes"])
print(f"{sid:>3} {m['marketId']:>4} {m['marketName'][:38]:38} legs={m['marketLength']} "
f"outcomes={outs} period={m['period']:8} twins={regulation_twins(sid)}")
Output on 14 October 2026:
59 sports ship a two-way winner at sportId*10+1
10 101 Full Time Result legs=3 outcomes=[101, 102, 103] period=fulltime twins=[]
11 111 Winner (incl. overtime) legs=2 outcomes=[111, 112] period=result twins=[113]
12 121 Winner legs=2 outcomes=[121, 122] period=result twins=[]
13 131 Winner (incl. extra innings) legs=2 outcomes=[131, 132] period=result twins=[133]
14 141 Winner (incl. overtime) legs=2 outcomes=[141, 142] period=result twins=[143]
15 151 Winner (incl. overtime and penalties) legs=2 outcomes=[151, 152] period=result twins=[153]
17 171 Winner legs=2 outcomes=[171, 172] period=result twins=[178077]
20 201 Winner legs=2 outcomes=[201, 202] period=result twins=[203]
The period field carries the clock. result means the final result, including overtime, extra innings or a shootout. fulltime means the end of regulation. Soccer’s 101 is fulltime because a soccer match ends at full time, and that is why the formula hands you the three-way market there.
Step 2: Pull the Moneyline Board
Find the next fixture in a tournament, call /v4/odds, and keep a book only when both legs are active and the book itself is open. The key goes in the query string as apiKey, never in a header.
import os, requests, datetime as dt
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
SPORT_ID, TOURNAMENT_ID = 14, 31 # American football, NFL
WINNER = SPORT_ID * 10 + 1 # 141, outcomes 141 (participant 1) and 142 (participant 2)
today = dt.date.today()
fixtures = requests.get(f"{BASE}/fixtures", params={
"apiKey": API_KEY, "sportId": SPORT_ID, "tournamentId": TOURNAMENT_ID,
"from": today.isoformat(), "to": (today + dt.timedelta(days=6)).isoformat(),
}).json()
fx = sorted((f for f in fixtures if f["hasOdds"]), key=lambda f: f["startTime"])[0]
odds = requests.get(f"{BASE}/odds", params={"apiKey": API_KEY, "fixtureId": fx["fixtureId"]}).json()
def legs(book, market_id, n):
"""Return {outcomeId: price object} if the book quotes every leg as active, else None."""
mk = book["markets"].get(str(market_id))
if not mk:
return None
out = {int(o): v["players"]["0"] for o, v in mk["outcomes"].items()
if v["players"].get("0", {}).get("active") and v["players"]["0"]["price"] > 1}
return out if len(out) == n else None
board = {}
for slug, book in odds["bookmakerOdds"].items():
if "+" in slug or slug == "demo" or book.get("suspended") or not book.get("bookmakerIsActive", True):
continue
ml = legs(book, WINNER, 2)
if ml:
board[slug] = ml
print(fx["participant1Name"], "v", fx["participant2Name"], fx["startTime"])
print(len(odds["bookmakerOdds"]), "books on the fixture,", len(board), "with an active two-way moneyline")
for slug in ("pinnacle", "draftkings", "fanduel", "kalshi"):
if slug in board:
a, b = board[slug][WINNER], board[slug][WINNER + 1]
print(f"{slug:11} {a['price']:>6} ({a['priceAmerican']:>5}) {b['price']:>6} ({b['priceAmerican']:>5})")
Output:
Denver Broncos v Seattle Seahawks 2026-10-16T00:15:00.000Z
278 books on the fixture, 252 with an active two-way moneyline
pinnacle 2.24 ( 124) 1.714 ( -140)
draftkings 2.2 ( 120) 1.704 ( -142)
fanduel 2.28 ( 128) 1.66 ( -152)
kalshi 2.273 ( 127) 1.754 ( -133)
priceAmerican arrives as a string, so cast it before you do maths on it. The tournamentId filter on /v4/fixtures is not in the reference docs, but it works and it cuts the payload hard. Swap SPORT_ID and TOURNAMENT_ID to point the same script at any sport: 15 and 234 for the NHL, 13 and 109 for MLB.
Step 3: Dedupe, Then Rank the Prices
Before you compute a median, a consensus or a best price, collapse the books that quote the same pair. Then de-vig Pinnacle for a fair probability and find the best price on each side.
import statistics
# continues from s2_board.py: `board`, `WINNER`, `fx`
pairs = {slug: (ml[WINNER]["price"], ml[WINNER + 1]["price"]) for slug, ml in board.items()}
margin = lambda p: 1 / p[0] + 1 / p[1] - 1
# Many slugs are skins of one feed and quote byte-identical prices. Count distinct quotes first.
distinct = {}
for slug, p in pairs.items():
distinct.setdefault(p, []).append(slug)
print(len(pairs), "books,", len(distinct), "distinct price pairs")
print(f"median margin {statistics.median(margin(p) for p in distinct):.2%}")
if "pinnacle" in pairs:
pin = pairs["pinnacle"]
fair = (1 / pin[0]) / (1 / pin[0] + 1 / pin[1])
print(f"pinnacle margin {margin(pin):.2%}, no-vig {fx['participant1Name']} {fair:.1%}")
for i, name in enumerate((fx["participant1Name"], fx["participant2Name"])):
best = max(p[i] for p in distinct)
who = sorted(s for s, p in pairs.items() if p[i] == best)
print(f"best {name}: {best}, quoted by {who[:3]}")
Output:
252 books, 76 distinct price pairs
median margin 4.91%
pinnacle margin 2.99%, no-vig Denver Broncos 43.3%
best Denver Broncos: 2.326, quoted by ['polymarket.us']
best Seattle Seahawks: 1.769, quoted by ['prophetx']
The best prices on this board came from two prediction venues, Polymarket US and ProphetX. Pull exchanges and sportsbooks into one board and you see those prices; a sportsbook-only feed hides them. For the fair-value step in more depth, the no-vig odds guide compares three de-vig methods, and line shopping in Python turns the best-price loop into a scanner.
Step 4: Keep the Regulation-Time Twin Apart
In the NFL, NBA, MLB and NHL, the moneyline (*1) and Regular Time Result (*3 in these four sports) settle the same match on different clocks. If a hockey game goes to overtime, the moneyline pays the winner and the 60-minute market pays the draw. Treat them as one market and your best-price scan will match a two-way price against a three-way price, and your grader will settle overtime games wrong.
import os, requests, statistics, datetime as dt
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
SPORT_ID, TOURNAMENT_ID = 15, 234 # ice hockey, NHL
WINNER, REGULATION = 151, 153 # incl. overtime and shootout (2-way) / 60 minutes (3-way)
today = dt.date.today()
fixtures = requests.get(f"{BASE}/fixtures", params={
"apiKey": API_KEY, "sportId": SPORT_ID, "tournamentId": TOURNAMENT_ID,
"from": today.isoformat(), "to": (today + dt.timedelta(days=3)).isoformat(),
}).json()
fx = sorted((f for f in fixtures if f["hasOdds"]), key=lambda f: f["startTime"])[0]
odds = requests.get(f"{BASE}/odds", params={"apiKey": API_KEY, "fixtureId": fx["fixtureId"]}).json()
def prices(book, market_id, n):
mk = book["markets"].get(str(market_id))
if not mk:
return None
p = [v["players"]["0"]["price"] for v in mk["outcomes"].values()
if v["players"].get("0", {}).get("active") and v["players"]["0"]["price"] > 1]
return p if len(p) == n else None
two, three = {}, {}
for slug, book in odds["bookmakerOdds"].items():
if "+" in slug or slug == "demo" or book.get("suspended") or not book.get("bookmakerIsActive", True):
continue
if (p := prices(book, WINNER, 2)):
two[slug] = p
if (p := prices(book, REGULATION, 3)):
three[slug] = p
vig = lambda ps: statistics.median(sum(1 / x for x in p) - 1 for p in ps.values())
print(fx["participant1Name"], "v", fx["participant2Name"])
print(f"{WINNER} two-way winner: {len(two):>3} books, median margin {vig(two):.2%}")
print(f"{REGULATION} regulation 1X2: {len(three):>3} books, median margin {vig(three):.2%}")
only = sorted(set(three) - set(two))
print(len(only), "books quote the 60-minute market and no two-way winner:", only[:6])
Output:
Washington Capitals v Montreal Canadiens
151 two-way winner: 236 books, median margin 5.13%
153 regulation 1X2: 229 books, median margin 7.93%
19 books quote the 60-minute market and no two-way winner: ['31bet', 'admiralbet.ba', 'admiralbet.me', 'admiralbet.rs', 'betcenter.be', 'cashpoint']
Hockey is where this matters most. The 60-minute market drew 229 books against 234.5 on the moneyline, and 19 books, from Admiralbet’s Balkan skins to Winamax Spain and Germany, price the regulation market without a two-way winner. If your app shows a European audience the NHL “match odds”, those books are the ones they know, and their number is the three-way price. The NHL regulation and overtime guide walks all 118 hockey twin pairs, and the same split runs through baseball’s first five innings.
Soccer: Pick the Moneyline You Mean
On the four Premier League boards, the three-way 1X2 (market 101) drew a median 254.5 books at a 6.02% median margin. The two-way Draw No Bet (10214) drew 212.5 books at 7.02%. About 43 books on each board quoted 1X2 with no Draw No Bet, Betfair Exchange among them. So if your users expect a two-way soccer price, read 10214. If they expect “match odds”, read 101 and keep all three legs, and reject any book that ships only two of them.
Grade and Backtest the Moneyline
The same IDs work on /v4/historical-odds once a fixture has finished. On the Jaguars v Eagles game of 11 October, market 141 held 1,442 DraftKings snapshots going back to 3 July, 662 from FanDuel and 642 from Pinnacle, each with a timestamp and Pinnacle’s stake limit. Take the last active snapshot before kick-off as the closing line, and you have a moneyline closing-line dataset for every sport in the table above. The backtesting guide shows the full loop. For in-play work, the WebSocket feed pushes each moneyline change instead of making you poll.
Tennis has its own wrinkles below the main tour, such as a second fixture ID scheme for ITF matches. The tennis moneyline guide covers market 121 from the WTA Tour down to ITF.
Pull Your First Moneyline Board
One free OddsPapi key gives you the moneyline market ID for 59 sports, up to 252 bookmakers on the boards we pulled, Pinnacle and the prediction venues on the same board, American odds on every quote, and the history to grade it all afterwards. Get a free API key, set SPORT_ID, and run Step 2 on tonight’s slate.
FAQ
What is a moneyline odds API?
A moneyline odds API returns bookmaker prices on which participant wins a fixture, with no point spread. OddsPapi calls this market Winner, defines it for 59 sports and returns it from one endpoint, /v4/odds, with decimal, American and fractional prices on every quote. On 14 October 2026 each NFL board we pulled carried it from 229 to 252 bookmakers.
Which market ID is the moneyline?
Multiply the sport ID by 10 and add 1. NFL (sport 14) is market 141, NHL (15) is 151, MLB (13) is 131, NBA (11) is 111, tennis (12) is 121 and UFC (20) is 201. The outcome IDs follow the same rule: 141 is participant 1 and 142 is participant 2. Confirm the name against /v4/markets before you hardcode it.
Does the NHL moneyline include overtime?
Yes. Market 151 is Winner (incl. overtime and penalties) and settles on the final result. Market 153, Regular Time Result, settles on the 60-minute score and has a draw leg. On 14 October 2026, 229 books priced 153 on a Capitals v Canadiens board, at a 7.80% median margin against 5.22% on 151.
Is the soccer moneyline two-way or three-way?
The default soccer match market is three-way: market 101, Full Time Result, with home, draw and away. The two-way version is Draw No Bet, market 10214. On four Premier League boards on 14 October 2026, 101 drew a median 254.5 books and 10214 drew 212.5.
Does the API return American moneyline odds?
Yes. Every quote carries priceAmerican next to the decimal price and a fractional price, so a US app needs no conversion step. On the 28 boards we pulled, all 11,046 moneyline legs had an American price. The field is a string such as “-140” or “124”.