NPB Odds API: Live Japanese Baseball Odds in Python (Free Tier)
Nippon Professional Baseball is the strongest baseball league outside the United States, it runs from late March to October, and it fills the dead hours before the MLB slate starts. Most odds APIs either skip it or bury it under a generic baseball endpoint with two bookmakers attached. If you already scrape MLB prices, NPB is the obvious next league, and your existing code almost works on it.
This guide pulls live NPB odds in Python: moneylines, run lines, totals and first-inning markets from 8 bookmakers including Pinnacle. It also covers the one thing that trips people up when they point MLB code at Japan, which is that the scoring environment is completely different and your thresholds will be wrong.
NPB uses the same endpoints as MLB
Good news first. NPB sits under the same sport as MLB, sportId 13, and uses the same market IDs. Winner is market 131, the run line is the Handicap (incl. extra innings) family, and totals are Over Under (incl. extra innings). Anything you built for the MLB odds API ports across by changing one filter.
import requests
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.oddspapi.io/v4"
def api_get(path, **params):
params["apiKey"] = API_KEY # query param, not a header
r = requests.get(f"{BASE_URL}{path}", params=params, timeout=30)
r.raise_for_status()
return r.json()
fixtures = api_get("/fixtures", sportId=13,
**{"from": "2026-07-18", "to": "2026-07-27"})
npb = [f for f in fixtures
if f.get("tournamentName") == "NPB" and f.get("hasOdds")]
print(len(npb), "NPB fixtures with odds")
for f in npb[:5]:
print(f["fixtureId"], f["participant1Name"], "v", f["participant2Name"])
That returned 36 NPB fixtures with odds across a ten day window. Team names come back in English (Orix Buffaloes, Yomiuri Giants, Hokkaido Nippon-Ham Fighters), so you do not need to handle Japanese text to match fixtures.
Pull the moneyline
The worked example is Orix Buffaloes against Hokkaido Nippon-Ham Fighters, fixture id1300103666777590. Market 131 is Winner (incl. extra innings), with outcome 131 for the first participant and 132 for the second.
FID = "id1300103666777590"
books = api_get("/odds", fixtureId=FID)["bookmakerOdds"]
def price(slug, market_id, outcome_id):
node = (books.get(slug, {}).get("markets", {})
.get(market_id, {}).get("outcomes", {}).get(outcome_id, {}))
player = node.get("players", {}).get("0")
if not player or node.get("active") is False:
return None
return player.get("price")
for slug in sorted(books):
home, away = price(slug, "131", "131"), price(slug, "131", "132")
if home:
print(f"{slug:20} {home:>7} {away:>7}")
| Bookmaker | Orix | Nippon-Ham |
|---|---|---|
| pinnacle | 2.10 | 1.806 |
| polymarket | 2.128 | 1.852 |
| pointsbet.com.au | 2.06 | 1.769 |
| draftkings | 2.05 | 1.76 |
| caesars / williamhill | 2.05 | 1.741 |
| kalshi | 1.852 | 1.613 |
Eight bookmakers were on the fixture and 121 markets were on the board across them. Filter on active is False rather than a truthy active check, because the feed sometimes ships active: null alongside a valid price.
Two things to notice before you trust the board
First, caesars and williamhill quote 2.05 and 1.741 to the decimal, because they run the same pricing. Eight slugs collapse to six independent quotes here. Deduplicate before averaging or that one opinion gets double weight in your consensus number.
from collections import defaultdict
groups = defaultdict(list)
for slug in books:
quote = (price(slug, "131", "131"), price(slug, "131", "132"))
if None not in quote:
groups[quote].append(slug)
print(len(books), "slugs ->", len(groups), "distinct quotes")
for quote, slugs in groups.items():
if len(slugs) > 1:
print(" duplicate:", quote, slugs)
# 8 slugs -> 6 distinct quotes
# duplicate: (2.05, 1.741) ['caesars', 'williamhill']
Second, look at Kalshi. Its two prices imply 54.0% and 62.0%, which sums to 116%. That is not a competitive quote, it is a thin market on a league the exchange has little volume in. The two exchanges split hard here: Polymarket was the tightest book on the fixture at 100.99%, Kalshi the widest by a distance. Screen by implied total before you let any book into a consensus calculation.
def book_percent(slug):
h, a = price(slug, "131", "131"), price(slug, "131", "132")
return (1/h + 1/a) if h and a else None
for slug in sorted(books):
bp = book_percent(slug)
if bp:
print(f"{slug:20} {bp*100:6.2f}%")
# polymarket 100.99%
# pinnacle 102.99%
# draftkings 105.60%
# pointsbet.com.au 105.07%
# caesars 106.22%
# williamhill 106.22%
# kalshi 115.99%
De-vig the sharp price
Pinnacle carried a 2.99% margin on this game, which is tight for a non-US league.
quote = {o: price("pinnacle", "131", o) for o in ("131", "132")}
implied = {k: 1 / v for k, v in quote.items()}
total = sum(implied.values())
print(f"overround {(total - 1) * 100:.2f}%")
for k, label in [("131", "Orix"), ("132", "Nippon-Ham")]:
print(f"{label:12} fair prob {implied[k]/total:.4f} fair odds {total/implied[k]:.4f}")
# overround 2.99%
# Orix fair prob 0.4624 fair odds 2.1628
# Nippon-Ham fair prob 0.5376 fair odds 1.8600
Best available prices were 2.128 on Orix and 1.852 on Nippon-Ham, both at Polymarket, which beat Pinnacle on each side of the same game. Both still sit inside Pinnacle’s fair numbers of 2.1628 and 1.8600, so that is a line shopping result rather than a value claim. The three de-vig methods are compared in the no-vig odds guide.
NPB scores about two runs lower than MLB
This is the part that breaks ported MLB code. Japanese baseball plays in a lower scoring environment, and any total threshold tuned on MLB will be wrong. Rather than assert it, here are the main totals pulled from the same feed on the same days.
| NPB fixture | Main total | MLB fixture | Main total |
|---|---|---|---|
| Orix Buffaloes | 7.0 | Cleveland Guardians | 7.5 |
| Yomiuri Giants | 6.5 | Chicago Cubs | 8.5 |
| Chiba Lotte Marines | 7.5 | Toronto Blue Jays | 9.5 |
| Yokohama DeNA BayStars | 7.5 | Colorado Rockies | 12.5 |
| Hiroshima Toyo Carp | 6.0 | Philadelphia Phillies | 8.5 |
| Tohoku Rakuten Golden Eagles | 6.5 | Houston Astros | 9.0 |
| Median | 6.75 | Median | 8.75 |
Two full runs of difference on the median. If your model flags anything under 7.5 as a low-total game, every NPB fixture triggers it. The Rockies at 12.5 show the other end of the range, which is Coors Field doing what Coors Field does.
Totals resolve through the Over Under (incl. extra innings) family. Each line is a separate market ID, so look them up instead of hardcoding.
catalog = api_get("/markets", sportId=13)
totals = {m["marketId"]: m.get("handicap") for m in catalog
if m.get("marketName") == "Over Under (incl. extra innings)"
and m.get("period") == "result"}
for mid, market in books["pinnacle"]["markets"].items():
if int(mid) in totals:
for oid, outcome in market["outcomes"].items():
node = outcome.get("players", {}).get("0")
if node and outcome.get("mainLine"):
print("main total", totals[int(mid)], oid, node["price"])
# main total 7 ...
Read the mainLine flag rather than guessing which rung of the ladder is the headline number. On this fixture Pinnacle’s 6.5 line priced at 1.813 over and 2.05 under, and the run line at 1.5 came in at 1.595 and 2.44.
Settlement: NPB games can end in a tie
NPB regular season games are capped at 12 innings, so a game level at that point is recorded as a tie. MLB has no such rule, which means a two-way moneyline behaves differently across the two leagues.
The feed labels the market Winner (incl. extra innings) and ships it with two outcomes, no draw selection. How a tie settles is a bookmaker rules question rather than something the odds payload answers, and it varies by book. Check the settlement terms at whichever book you are pricing against before you grade positions automatically, and do not assume a stake is returned. This is the single most common way an MLB-shaped grading script produces wrong results on Japanese baseball.
Free historical prices
hist = api_get("/historical-odds", fixtureId=FID,
bookmakers="pinnacle,bet365,draftkings") # max 3 per call
snaps = (hist["bookmakers"]["pinnacle"]["markets"]["131"]
["outcomes"]["131"]["players"]["0"]) # a LIST of snapshots
print(len(snaps), snaps[0]["price"], "->", snaps[-1]["price"])
# 5 2.07 -> 2.1
Pinnacle recorded 5 price points on this fixture, drifting 2.07 to 2.10. NPB books post later and move less than MLB books, so the history is shorter. The historical response nests under bookmakers rather than bookmakerOdds, and players["0"] is a list instead of one price. Capture it into your own store and the sparse history stops being a problem, which the odds database guide covers.
What NPB coverage actually looks like
Being straight about the depth: NPB gets 8 bookmakers on a fixture where a marquee MLB game gets 17, and the run line and totals sit on 4 books rather than 10. Pinnacle, Polymarket and the US retail books carry it. First-inning markets are there but on two books.
What you do get is a sharp reference price on every fixture, a full handicap and total ladder from Pinnacle, and 121 markets on a league most feeds ignore. For a league playing daily while MLB sleeps, that is enough to build on. Widen to the best price across the board with line shopping in Python.
Frequently asked questions
Is there an NPB odds API?
Yes. NPB fixtures sit under baseball, sportId 13, with tournamentName NPB. A ten day window returned 36 NPB fixtures with odds, priced by up to 8 bookmakers including Pinnacle, with 121 markets on a single game.
Does NPB use the same market IDs as MLB?
Yes. Both sit under sportId 13, so Winner is market 131, run lines are the Handicap (incl. extra innings) family and totals are Over Under (incl. extra innings). MLB code ports over by changing the tournament filter.
Why are NPB totals lower than MLB totals?
Japanese baseball plays in a lower scoring environment. Across six fixtures from each league on the same days, the median main total was 6.75 for NPB and 8.75 for MLB. Any threshold tuned on MLB totals needs retuning before it runs on NPB.
How do NPB ties affect settlement?
NPB regular season games are capped at 12 innings and can end in a tie. The feed ships Winner (incl. extra innings) with two outcomes and no draw selection, so how a tie settles depends on the bookmaker’s rules rather than the odds payload. Check settlement terms before grading positions automatically.
Can I get free historical NPB odds?
Yes, on the free tier, capped at three bookmakers per call. Pinnacle recorded 5 price points on the sample fixture. NPB books post later and move less than MLB books, so histories are shorter than you would see on a US game.
Start pulling NPB prices
One free key covers 381 bookmakers across 69 sports, with sharp books and prediction markets in the same response and free price history. NPB is one filter away from code you have already written. Get your free API key and point it at tonight’s Japanese slate.