NPB Odds API: Live Japanese Baseball Odds in Python (Free Tier)

NPB Odds API - OddsPapi API Blog
How To Guides August 3, 2026

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.