# Novig API in Python

> Independent. Not affiliated with, endorsed by or sponsored by Novig.

Canonical: https://novigapi.com/novig-api-python
Facts checked: 2026-11-12

To use Novig data in Python, call https://novigapi.com/api/v1 with requests and an Authorization: Bearer nv_ header; no Novig account or request signing is needed. Each market comes back with bids, asks and a fair price. On 12 Nov 2026, Cooper Flagg 1.5 steals had bids of 0.39 and 0.57, a fair Over of 0.41. (Source: [Novig API reference: List markets](https://docs.novig.com/api-reference/catalog/list-markets.md))

There are two ways to reach Novig from Python. Novig's official API at docs.novig.com is built for trading: it needs a Novig account, identity and location checks, and every request signed with Ed25519 or P-256. If you only need to read prices, novigapi.com is an independent read-only layer: one bearer key, plain JSON, names resolved, fair prices computed. This guide uses novigapi.com.

## Install and authenticate

Get a key from your dashboard (keys start with `nv_`) and keep it in an environment variable, never in code. The Free plan gives 100 credits a month and 20 requests a minute, enough to follow this guide.

```bash
pip install requests pandas
export NOVIGAPI_API_KEY=nv_...
```

```python
import os
import requests

BASE = "https://novigapi.com/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['NOVIGAPI_API_KEY']}"

def get(path, **params):
    r = session.get(BASE + path, params=params, timeout=10)
    if r.status_code == 429:
        raise RuntimeError(r.json()["error"]["message"])
    r.raise_for_status()
    return r.json()
```

## Pull a board

List endpoints return `data` and a `meta` object with `next_cursor` for paging, three honesty fields (`observed_at`, `source_age_seconds` and `edge_cached`) and what the call cost (`credits_charged`). On paid plans the data is at most about 10 seconds old. Page until `meta.next_cursor` is null:

```python
def all_markets(**filters):
    cursor = None
    while True:
        page = get("/markets", cursor=cursor, limit=500, **filters)
        yield from page["data"]
        cursor = page["meta"]["next_cursor"]
        if not cursor:
            return

board = list(all_markets(league="NBA", market_type="STEALS"))
print(len(board), "markets")
```

Each credit covers up to 100 rows requested, so a 500-row page costs 5 credits (Free pages hold at most 100 rows). Filter by league and market type instead of reading the whole catalog, which held about 38,400 open markets on 12 Nov 2026.

## Devig a board

Novig is an exchange, so there is no bookmaker margin to strip. Its order book holds bids only: the price to buy one outcome is 1 minus the best bid on the other. The fair price is the midpoint of bid and ask:

```
fair_yes = (bid_yes + (1 - bid_no)) / 2
```
fair_no = 1 - fair_yes. Needs a bid on both sides.

Real Novig markets on 12 Nov 2026, devigged

| Market | League | Bid Over | Bid Under | Ask Over | Fair Over |
| --- | --- | --- | --- | --- | --- |
| Cooper Flagg 1.5 STEALS | NBA | 0.39 | 0.57 | 0.43 | 0.41 |
| Daniss Jenkins 23.5 POINTS_REBOUNDS_ASSISTS | NBA | 0.49 | 0.47 | 0.53 | 0.51 |
| TOR @ PHI t107.5 1H | NBA | 0.48 | 0.49 | 0.51 | 0.495 |
| Jonathan Taylor 68.5 RUSHING_YARDS | NFL | 0.455 | 0.465 | 0.535 | 0.495 |


The API already returns `fair` on every outcome. Computing it yourself is a good check, and it lets you devig prices from other books the same way:

```python
def fair(bid_yes, bid_no):
    if bid_yes is None or bid_no is None:
        return None  # one-sided market, no fair price
    return round((bid_yes + (1 - bid_no)) / 2, 4)

def devig_two_way(p_a, p_b):
    """Multiplicative devig for a sportsbook's two implied probabilities."""
    total = p_a + p_b
    return p_a / total, p_b / total

for m in board:
    over, under = m["outcomes"]
    mine = fair(over["bid"], under["bid"])
    assert mine == over["fair"], (m["market_id"], mine, over["fair"])

print(fair(0.39, 0.57))  # 0.41, Cooper Flagg 1.5 steals
```

Fees are separate from the fair price. Novig's taker fee is 0.06 x P x (1 - P) x contracts x $0.01 (0.06 x P x (1 - P) per $1 of payout), charged only on game markets while they are live, so pregame prices on game markets are fee-free to take. The [fee calculator](https://novigapi.com/novig-fee-calculator) shows the cost at any price.

## A pandas DataFrame of player props

[GET /v1/player-props](https://novigapi.com/docs/endpoints/player-props) returns every line for one player. Flatten it into one row per market:

```python
import pandas as pd

props = get("/player-props", league="NBA", player="cooper-flagg")["data"]
rows = []
for m in props:
    over, under = m["outcomes"]
    rows.append({
        "stat": m["stat"],
        "line": m["strike"],
        "protected": m["market_type"].endswith("_PROTECTED"),
        "over_bid": over["bid"],
        "over_ask": over["ask"],
        "fair_over": over["fair"],
        "volume": m["volume"],
    })

df = pd.DataFrame(rows).dropna(subset=["fair_over"]).sort_values(["stat", "line"])
df["spread"] = (df["over_ask"] - df["over_bid"]).round(3)
print(df.to_string(index=False))
```

Drop rows with no fair price first: many ladder rungs have a bid on one side only. A wide `spread` means a soft fair price, so weight those rows less.

## Standard library only

No dependencies, for a cron job or a locked-down server:

```python
import json, os, urllib.parse, urllib.request

def novig(path, **params):
    url = "https://novigapi.com/api/v1" + path + "?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, headers={"Authorization": "Bearer " + os.environ["NOVIGAPI_API_KEY"]})
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.load(resp)

for m in novig("/markets", league="NFL", market_type="RUSHING_YARDS", limit=20)["data"]:
    print(m["description"], m["outcomes"][0]["fair"])
```

## Limits and errors

A 429 with `code: RATE_LIMITED` means you passed your per-minute ceiling; sleep for the `Retry-After` header and retry. A 429 with `code: QUOTA_EXCEEDED` means the month's credits are spent; retrying will not help, and nothing is billed automatically. Errors cost no credits. The [errors reference](https://novigapi.com/docs/endpoints/errors) lists every code.

## Questions

### Is there an official Novig Python package?

Novig's docs describe a signed REST and WebSocket API for trading rather than a read-only Python package. novigapi.com works with plain requests or urllib, so you do not need a package.

### Do I need a Novig account to use this from Python?

No. You need a novigapi.com key. It reads Novig's public market data and cannot place orders.

### How fresh is the data?

On paid plans REST data is at most about 10 seconds old, and each response says its own age in `source_age_seconds`. For pushed changes, the stream delivers within 5 to 30 seconds.

### Why is fair None on some markets?

The fair price needs a bid on both outcomes. On 12 Nov 2026, Cooper Flagg 11.5 rebounds plus assists had only an Under bid, so it had no fair price.

## Sources

- [Novig API reference: List markets](https://docs.novig.com/api-reference/catalog/list-markets.md)
- [Novig API docs: Trading fees](https://docs.novig.com/api/concepts/fees.md)
- [Novig docs: Sign a request](https://docs.novig.com/api/signing.md)
- [Novig API docs](https://docs.novig.com)

---

Get a free API key: https://novigapi.com/sign-up . Docs: https://novigapi.com/docs . MCP: https://novigapi.com/api/mcp . Operated by Stunning Couches LLC. Independent. Not affiliated with, endorsed by or sponsored by Novig.
