Novig API: Read Both Sides of a 191-Market NFL Board
Does Novig have an API? Yes. Novig publishes a trading API at docs.novig.com: you create a management key in your Novig profile, open a subaccount, sign each REST request with Ed25519 or P-256, and follow the order book and your fills on a websocket. It is built to place orders on the Novig exchange.
If you want to read Novig’s prices next to Pinnacle, Kalshi, ProphetX and 350+ other bookmakers, with closing lines for backtests, OddsPapi carries the book under one slug: novig.us. It ships three levels of back and lay depth on every outcome. On 16 NFL Week 3 fixtures (24-29 September 2026), novig.us closed the moneyline at a 0.51% median overround against Pinnacle’s 2.63%, and its no-vig price sat a median 0.67 points from Pinnacle’s. This guide shows you how to find the slug, read both sides of the order book, pull the player props and check the close yourself.
What the Novig trading API covers, and what a price pipeline needs
A trading API answers one question well: what can I fill on this venue right now? A pricing model asks more. It wants the Novig price next to the sharp books, in one schema, plus every snapshot after the fixture is played. If you already read Kalshi or ProphetX, you know the cost: one auth scheme per venue, one market format per venue, and your own recorder for history.
OddsPapi gives Novig the same JSON shape as every other book on the board. A Novig outcome looks like a Pinnacle outcome, plus an exchangeMeta block that holds the ladder. Our NFL prediction market liquidity study ranked seven peer-to-peer venues on depth. This post goes one level down: how to read the novig.us block itself.
| Novig trading API | OddsPapi novig.us |
|
|---|---|---|
| Built for | Placing and managing orders on Novig | Reading Novig prices next to other books |
| Auth | Management key from your Novig profile, signed requests (Ed25519 or P-256) | One apiKey query parameter |
| Venues | Novig | Novig plus 350+ bookmakers and exchanges, one schema |
| Depth | Order book and fills on a websocket | Three back and three lay levels per outcome, REST and WebSocket |
| Played fixtures | See Novig’s docs | /v4/historical-odds on the free tier, back to 20+ days before an NFL kickoff |
Where novig.us prices today
We probed up to five upcoming fixtures with odds in each of 35 tournaments across 17 sports on 2 October 2026 at 13:08 UTC, with bookmakers=novig.us. Novig priced six tournaments:
| Tournament | Fixtures priced | Markets per fixture | Player prop markets |
|---|---|---|---|
| NFL | 5 of 5 | 175 to 197 (median 191) | 121 to 140 |
| NCAA football | 5 of 5 | 55 to 77 | 0 |
| MLB (postseason) | 4 of 5 | 22 to 45 | 3 to 18 |
| NHL | 5 of 5 | 23 to 25 | 7 to 9 |
| WTA Beijing singles | 5 of 5 | 13 to 14 | 0 |
| UFC 332 | 5 of 5 | 5 to 6 | 0 |
The fifth MLB fixture was the next day’s game in the same series, and Novig had not opened it yet. On the soccer, basketball, CFL, AHL, Japanese and Korean baseball, darts, rugby and esports fixtures we sampled, the slug returned no prices. For those, Kalshi, polymarket.us and Pinnacle cover most of the gap. NBA preseason had no fixtures in the window, so we did not test it.
Step 1: find the Novig slug
Authentication is one query parameter, apiKey. The helper waits out a 429 for as long as the error body says, backs off on a 5xx, and returns None on a 404.
import os, time, requests
from datetime import datetime, timedelta, timezone
from statistics import median
API_KEY = os.environ["ODDSPAPI_API_KEY"]
BASE = "https://api.oddspapi.io/v4"
def get(path, **params):
params["apiKey"] = API_KEY
for _ in range(5):
r = requests.get(f"{BASE}/{path}", params=params, timeout=180)
if r.status_code == 429: # rate limited: wait as told
time.sleep(r.json().get("error", {}).get("retryMs", 2000) / 1000 + 0.5)
continue
if r.status_code >= 500: # transient: back off and retry
time.sleep(2)
continue
if r.status_code == 404: # empty window
return None
r.raise_for_status()
return r.json()
raise RuntimeError(f"gave up on {path}")
books = get("bookmakers")
novig = next(b for b in books if b["slug"] == "novig.us")
print(novig)
print(f"{len(books)} bookmakers in the catalogue")
{'bookmakerName': 'Novig US', 'slug': 'novig.us', 'liveOdds': True, 'cloneOf': None}
363 bookmakers in the catalogue
One slug, liveOdds: True, no clone. Pass it alone in bookmakers= when you probe coverage. A 200 with the slug missing means Novig has not priced that fixture; a 403 would mean your key cannot read the book.
Step 2: count Novig’s markets per fixture
This step pulls the next two fixtures in four tournaments and counts Novig’s markets. It reads the global /v4/markets catalogue once to flag player props.
# Reuses get() and the imports from Step 1.
MARKETS = {m["marketId"]: m for m in get("markets")} # name lookup for every market ID
now = datetime.now(timezone.utc)
window = {"from": now.strftime("%Y-%m-%d"), "to": (now + timedelta(days=4)).strftime("%Y-%m-%d")}
TOURNAMENTS = {31: "NFL", 27653: "NCAAF", 109: "MLB", 234: "NHL"}
for tid, name in TOURNAMENTS.items():
fixtures = get("fixtures", tournamentId=tid, **window) or []
upcoming = sorted((f for f in fixtures if f["hasOdds"] and f["startTime"] > now.isoformat()),
key=lambda f: f["startTime"])[:2]
for fx in upcoming:
data = get("odds", fixtureId=fx["fixtureId"], bookmakers="novig.us")
book = data["bookmakerOdds"].get("novig.us")
time.sleep(1.0)
if not book:
print(f"{name:<6} {fx['participant1Name'][:22]:<22} not priced yet")
continue
props = sum(MARKETS.get(int(mid), {}).get("playerProp", False) for mid in book["markets"])
print(f"{name:<6} {fx['participant1Name'][:22]:<22} v {fx['participant2Name'][:22]:<22} "
f"{len(book['markets']):>3} markets, {props:>3} player props")
NFL Washington Commanders v Indianapolis Colts 193 markets, 136 player props
NFL Baltimore Ravens v Tennessee Titans 197 markets, 140 player props
NCAAF Delaware Blue Hens v Liberty Flames 72 markets, 0 player props
NCAAF Virginia Tech Hokies v Pittsburgh Panthers 77 markets, 0 player props
MLB Cleveland Guardians v Chicago White Sox 43 markets, 18 player props
MLB Los Angeles Dodgers v Atlanta Braves 22 markets, 3 player props
NHL Detroit Red Wings v New York Rangers 24 markets, 7 player props
NHL Carolina Hurricanes v Washington Capitals 25 markets, 9 player props
The NFL board is the deep one. On the five Week 4 Sunday fixtures we pulled, Novig listed 26 to 31 players with props each, across receiving yards, rushing yards, passing yards, receptions, touchdowns and first touchdown scorer. College football carries spreads, totals, team totals and first-half lines, with no props. The NHL board adds shots on goal, points, assists, goals and saves.
Step 3: read both sides of the Novig order book
Every Novig price ships an exchangeMeta block with up to three back levels and three lay levels. Each level carries four numbers: price (decimal odds), cents (the contract price, 0 to 1), size (contracts) and limit (the stake at that level). This step resolves the moneyline by name, then prints the ladder for both outcomes.
# Reuses get() and the imports from Step 1.
MARKETS = {m["marketId"]: m for m in get("markets")}
now = datetime.now(timezone.utc)
fixtures = get("fixtures", tournamentId=31, # NFL
**{"from": now.strftime("%Y-%m-%d"), "to": (now + timedelta(days=4)).strftime("%Y-%m-%d")})
fx = min((f for f in fixtures if f["startTime"] > now.isoformat()), key=lambda f: f["startTime"])
book = get("odds", fixtureId=fx["fixtureId"], bookmakers="novig.us")["bookmakerOdds"]["novig.us"]
# Resolve the moneyline by name, never by a hardcoded ID
mid = next(k for k in book["markets"]
if MARKETS[int(k)]["marketName"].startswith("Winner") and MARKETS[int(k)]["period"] == "result")
names = {o["outcomeId"]: o["outcomeName"] for o in MARKETS[int(mid)]["outcomes"]}
legs = {names[int(oid)]: o["players"]["0"] for oid, o in book["markets"][mid]["outcomes"].items()}
legs = dict(sorted(legs.items())) # "1" first, then "2"
team = {"1": fx["participant1Name"], "2": fx["participant2Name"]}
print(f"{fx['participant1Name']} v {fx['participant2Name']} market {mid}\n")
for name, p in legs.items():
print(f"Outcome {name} ({team[name]})")
for side in ("back", "lay"):
for lvl in p["exchangeMeta"][side]:
ok = abs(lvl["limit"] - lvl["size"] * lvl["cents"]) < 0.02
print(f" {side:<4} {lvl['price']:>6.3f} cents {lvl['cents']:.3f} size {lvl['size']:>9.2f} "
f"limit {lvl['limit']:>8.2f} limit == size x cents: {ok}")
a, b = legs.values()
mirror = all(abs(x["limit"] - y["limit"]) <= 0.02 and x["price"] == y["price"]
for x, y in zip(a["exchangeMeta"]["lay"], b["exchangeMeta"]["back"]))
over = 1 / a["price"] + 1 / b["price"] - 1
print(f"\nOutcome 1 lay ladder == outcome 2 back ladder: {mirror}")
print(f"Best-back overround: {over:.2%} top-of-book stake: {min(a['limit'], b['limit']):.2f}")
Washington Commanders v Indianapolis Colts market 141
Outcome 1 (Washington Commanders)
back 2.778 cents 0.360 size 67211.32 limit 24196.08 limit == size x cents: True
back 2.740 cents 0.365 size 14071.56 limit 5136.12 limit == size x cents: True
back 2.703 cents 0.370 size 47064.12 limit 17413.72 limit == size x cents: True
lay 1.550 cents 0.645 size 351.63 limit 226.80 limit == size x cents: True
lay 1.538 cents 0.650 size 19536.17 limit 12698.51 limit == size x cents: True
lay 1.527 cents 0.655 size 16487.45 limit 10799.28 limit == size x cents: True
Outcome 2 (Indianapolis Colts)
back 1.550 cents 0.645 size 351.63 limit 226.80 limit == size x cents: True
back 1.538 cents 0.650 size 19536.17 limit 12698.51 limit == size x cents: True
back 1.527 cents 0.655 size 16487.45 limit 10799.28 limit == size x cents: True
lay 2.778 cents 0.360 size 67211.32 limit 24196.08 limit == size x cents: True
lay 2.740 cents 0.365 size 14071.56 limit 5136.12 limit == size x cents: True
lay 2.703 cents 0.370 size 47064.12 limit 17413.72 limit == size x cents: True
Outcome 1 lay ladder == outcome 2 back ladder: True
Best-back overround: 0.51% top-of-book stake: 226.80
Read three things off that output.
- One book, two views. Outcome 1’s lay ladder is outcome 2’s back ladder, level for level. Across 2,211 active two-way pairs on the 29 boards in our census, the mirror held on 2,200, to a 0.02 rounding on
limit. So you can read one outcome and know both sides of the market. Do not count Novig twice in a consensus by treating the lay ladder as a second quote. limitissizetimescents. That held on 13,499 of 13,499 ladder levels. Size counts contracts. Limit is what it costs to take them.- The top level can be thin while level two is deep. The Colts back at 1.550 carried 226.80 of stake. One tick worse, at 1.538, it carried 12,698.51. A fill model that reads only the best price undercounts this book.
The best-back prices on that moneyline add up to a 0.51% overround: the two contract prices, 0.360 and 0.645, sum to 1.005, one half-cent tick. Novig also publishes bookmakerChangedAt on 4,497 of 6,674 prices in our census, so you can measure how old a quote is on most of the board.
On the same five NFL boards, with the same filter, the other venues priced the moneyline like this:
| Venue | Median best-back overround, NFL moneyline (5 fixtures, 2 Oct) |
|---|---|
polymarket.us |
0.25% |
novig.us |
0.50% |
prophetx |
0.66% |
kalshi |
1.01% |
pinnacle |
3.61% |
On an exchange, check depth before you trust a tight price. polymarket.us shipped limit: 0 on these prices, so check its size before you size against it. The ProphetX guide covers that venue’s ladder, which has no size field.
Step 4: pull Novig player props by player
On player props, the players dict is keyed by player ID, and each entry carries a playerName in “Last, First” form. A parser that hardcodes players["0"] sees an empty prop board. This step reuses the fixture and book from Step 3.
# Reuses MARKETS, fx and book from Step 3.
from collections import defaultdict
by_player = defaultdict(list)
stake = {"player props": [], "game lines": []}
for mid, m in book["markets"].items():
meta = MARKETS[int(mid)]
outcome_name = {o["outcomeId"]: o["outcomeName"] for o in meta["outcomes"]}
for oid, o in m["outcomes"].items():
for pid, p in o["players"].items(): # props are keyed by player ID, not "0"
if not p.get("active"):
continue
stake["player props" if meta["playerProp"] else "game lines"].append(p["limit"])
if meta["playerProp"]:
by_player[p["playerName"]].append((meta["marketName"], meta["handicap"],
outcome_name[int(oid)], p))
print(f"{len(by_player)} players with an active prop\n")
for name, rows in sorted(by_player.items(), key=lambda kv: -len(kv[1]))[:5]:
families = sorted({r[0].replace(" (incl. overtime)", "").replace("Over Under ", "") for r in rows})
print(f"{name:<22} {len(rows):>3} prices {', '.join(families)}")
for kind, limits in stake.items():
print(f"\nMedian top-of-book stake, {kind}: {median(limits):,.2f} over {len(limits)} prices", end="")
name, rows = max(by_player.items(), key=lambda kv: len(kv[1]))
rungs = defaultdict(dict)
for market, line, side, p in rows:
if market.startswith("Over Under Rush Yards"):
rungs[line][side] = p
print(f"\n\n{name}, Rush Yards rungs with both sides posted")
for line, sides in sorted(rungs.items()):
if len(sides) == 2:
o, u = sides["Over"], sides["Under"]
print(f" {line:>6} over {o['price']:>6.3f} under {u['price']:>6.3f} "
f"overround {1/o['price'] + 1/u['price'] - 1:>6.2%} stake {min(o['limit'], u['limit']):>7.2f}")
31 players with an active prop
Taylor, Jonathan 61 prices Longest Rush Yards, Player Receiving Yards, Player Receptions, Player TD, Player To Score First TD, Rush Attempts, Rush Yards
Mariota, Marcus 59 prices Longest Rush Yards, Pass Attempts, Pass Completions, Pass Yards, Player Interceptions, Player TD, Player TD Passes, Player To Score First TD, Rush Attempts, Rush Yards
Jones, Daniel 52 prices Longest Rush Yards, Pass Attempts, Pass Completions, Pass Yards, Player Interceptions, Player TD, Player TD Passes, Player To Score First TD, Rush Attempts, Rush Yards
McLaurin, Terry 48 prices Player Receiving Yards, Player Receptions, Player TD, Player To Score First TD
Downs, Josh 45 prices Player Receiving Yards, Player Receptions, Player TD, Player To Score First TD
Median top-of-book stake, player props: 17.08 over 523 prices
Median top-of-book stake, game lines: 525.67 over 114 prices
Taylor, Jonathan, Rush Yards rungs with both sides posted
49.5 over 1.111 under 2.564 overround 29.01% stake 19.80
59.5 over 1.220 under 2.222 overround 26.97% stake 9.84
69.5 over 1.351 under 3.030 overround 7.02% stake 0.99
79.5 over 1.562 under 2.326 overround 7.01% stake 1.72
89.5 over 1.835 under 1.942 overround 5.99% stake 26.55
90.5 over 2.000 under 1.887 overround 2.99% stake 12.00
92.5 over 2.151 under 1.449 overround 15.50% stake 445.16
99.5 over 2.198 under 1.667 overround 5.48% stake 141.13
109.5 over 2.740 under 1.449 overround 5.51% stake 2.76
139.5 over 5.882 under 1.053 overround 11.97% stake 14.45
Novig’s prop book runs wider and thinner than its game lines. On the five NFL boards, 962 prop rungs had both sides posted. Their median overround was 5.98%, and the median top-of-book stake was 10.30. The 279 two-sided game-line rungs, alt spreads and totals included, ran at 2.03% with a median stake of 312.50. Within one player, the spread moves from rung to rung: Jonathan Taylor’s tightest rushing line, 90.5, priced at 2.99%, and the rungs either side of it were two to five times wider. Scan for the tight rung before you compare Novig with a sportsbook prop. Our NFL player props guide shows the same markets across the retail books.
Step 5: check Novig’s NFL closing line against Pinnacle
OddsPapi stores every Novig snapshot. On the two Week 1 fixtures we checked, novig.us history began 20 and 28 days before kickoff. This step pulls Novig and Pinnacle from /v4/historical-odds for every played Week 3 fixture, takes the last active snapshot before kickoff on each side, and compares the two closes.
# Reuses get() and the imports from Step 1.
MARKETS = {m["marketId"]: m for m in get("markets")}
ts = lambda s: datetime.fromisoformat(s.replace("Z", "+00:00"))
def close(book, start):
"""Last ACTIVE pre-kickoff snapshot per outcome on the moneyline, or None."""
if not book:
return None
mid = next((k for k in book["markets"] if MARKETS[int(k)]["marketName"].startswith("Winner")
and MARKETS[int(k)]["period"] == "result"), None)
if mid is None:
return None
out = {}
for oid, o in book["markets"][mid]["outcomes"].items():
snaps = [s for s in o["players"]["0"] if s["active"] and ts(s["createdAt"]) < start]
if snaps:
out[oid] = snaps[-1]["price"]
return out if len(out) == 2 else None
played = get("fixtures", tournamentId=31, **{"from": "2026-09-24", "to": "2026-09-30"}) # NFL Week 3
played = [f for f in played if f["statusName"] == "Finished"]
rows = []
for f in played:
hist = get("historical-odds", fixtureId=f["fixtureId"], bookmakers="novig.us,pinnacle")
time.sleep(4.6) # /historical-odds needs the longer pause
start = ts(f["startTime"])
n, p = close(hist["bookmakers"].get("novig.us"), start), close(hist["bookmakers"].get("pinnacle"), start)
if not (n and p):
continue
n_over, p_over = sum(1 / v for v in n.values()), sum(1 / v for v in p.values())
k = sorted(n)[0] # participant 1's outcome
gap = abs((1 / n[k]) / n_over - (1 / p[k]) / p_over) # no-vig probability gap
better = sum(n[o] >= p[o] for o in n)
rows.append((n_over - 1, p_over - 1, gap, better))
print(f"{f['participant1Name'][:20]:<20} v {f['participant2Name'][:20]:<20} "
f"novig {n_over - 1:>5.2%} pinnacle {p_over - 1:>5.2%} no-vig gap {gap * 100:.2f} pts")
print(f"\n{len(rows)} fixtures. Median close overround: novig.us {median(r[0] for r in rows):.2%}, "
f"pinnacle {median(r[1] for r in rows):.2%}")
print(f"Median no-vig gap {median(r[2] for r in rows) * 100:.2f} pts, max {max(r[2] for r in rows) * 100:.2f}")
print(f"novig.us close at or above pinnacle on {sum(r[3] for r in rows)} of {2 * len(rows)} sides")
Green Bay Packers v Atlanta Falcons novig 0.49% pinnacle 2.71% no-vig gap 0.83 pts
Buffalo Bills v Los Angeles Chargers novig 0.97% pinnacle 3.04% no-vig gap 0.49 pts
Cleveland Browns v Carolina Panthers novig 0.49% pinnacle 2.54% no-vig gap 0.26 pts
Detroit Lions v New York Jets novig 0.52% pinnacle 3.01% no-vig gap 0.09 pts
Indianapolis Colts v Houston Texans novig 0.50% pinnacle 2.52% no-vig gap 0.44 pts
Jacksonville Jaguars v New England Patriots novig 0.49% pinnacle 2.58% no-vig gap 0.19 pts
Miami Dolphins v Kansas City Chiefs novig 1.03% pinnacle 3.24% no-vig gap 1.38 pts
New York Giants v Tennessee Titans novig 1.01% pinnacle 2.36% no-vig gap 0.65 pts
Pittsburgh Steelers v Cincinnati Bengals novig 0.51% pinnacle 2.52% no-vig gap 0.68 pts
Washington Commander v Seattle Seahawks novig 0.52% pinnacle 3.06% no-vig gap 1.38 pts
San Francisco 49ers v Arizona Cardinals novig 0.51% pinnacle 3.07% no-vig gap 1.33 pts
Tampa Bay Buccaneers v Minnesota Vikings novig 0.49% pinnacle 2.45% no-vig gap 0.54 pts
Dallas Cowboys v Baltimore Ravens novig 1.01% pinnacle 2.59% no-vig gap 1.18 pts
New Orleans Saints v Las Vegas Raiders novig 0.99% pinnacle 2.68% no-vig gap 0.74 pts
Denver Broncos v Los Angeles Rams novig 0.51% pinnacle 2.45% no-vig gap 0.96 pts
Chicago Bears v Philadelphia Eagles novig 0.49% pinnacle 2.77% no-vig gap 0.37 pts
16 fixtures. Median close overround: novig.us 0.51%, pinnacle 2.63%
Median no-vig gap 0.67 pts, max 1.38
novig.us close at or above pinnacle on 30 of 32 sides
Novig closed 11 of the 16 moneylines at one half-cent tick (0.49% to 0.52%) and the other five at two ticks. Its no-vig probability landed within 1.38 points of Pinnacle’s on all 16, with a median gap of 0.67. The quoted Novig price was at or above Pinnacle’s on 30 of 32 sides. Novig’s fee schedule lists no fee on pre-game straight trades, which is what a closing price is. Live and futures trades carry a taker fee, so net those out before you compare an in-play Novig price.
Two things to keep in mind when you build on this history. Snapshots record polling, so collapse identical consecutive prices before you count line moves: the Week 3 moneylines held a median of 1,589 snapshots and 295 price changes each. And historical snapshots carry exchangeMeta: null, so the three-level ladder exists live only; the history keeps the top-level price and limit. To turn these closes into a strategy test, start from our backtesting guide. For live fills, swap the polling loop for the WebSocket feed.
For a one-page view of the slug, see the Novig US sportsbook page. To compare Novig with the two biggest prediction markets, read Kalshi API vs Polymarket API.
Novig API FAQ
Does Novig have an API?
Yes. Novig documents a trading API at docs.novig.com, with signed REST requests and a websocket for the order book and your fills. To read Novig prices next to 350+ other bookmakers, with history, use the novig.us slug on OddsPapi.
How do I get Novig odds in Python?
Call /v4/odds with a fixtureId and bookmakers=novig.us, passing your key as the apiKey query parameter. Each outcome returns a decimal price, a limit and an exchangeMeta block with three back and three lay levels.
Which sports does novig.us cover on OddsPapi?
On 2 October 2026, novig.us priced the NFL (175 to 197 markets per fixture, including player props), NCAA football, MLB, the NHL, WTA singles and the UFC.
What do size, cents and limit mean in Novig’s exchangeMeta?
cents is the contract price between 0 and 1, size is the number of contracts at that level, and limit is the stake, equal to size times cents. That identity held on 13,499 of 13,499 ladder levels we measured.
Can I get historical Novig odds?
Yes. /v4/historical-odds returns every novig.us snapshot on a played fixture, on the free tier. On 16 NFL Week 3 fixtures, Novig closed the moneyline at a 0.51% median overround, against 2.63% at Pinnacle.
Read the Novig book next to the board
Every call in this guide runs on the free key: 350+ bookmakers in the catalogue, free historical odds for the closing-line check, and a WebSocket feed when polling gets too slow. Get a free API key and run Step 3 on Sunday’s NFL slate.