Novig API: Read Both Sides of a 191-Market NFL Board

Novig API header image
How To Guides October 13, 2026

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.
  • limit is size times cents. 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.