# Novig market data in Google Sheets

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

Canonical: https://novigapi.com/google-sheets
Facts checked: 2026-11-12

To get Novig data in Google Sheets, add an Apps Script custom function, NOVIG_MARKETS(league), that calls novigapi.com with UrlFetchApp and a bearer key stored in Script Properties. IMPORTDATA cannot send an Authorization header, so it cannot call the API. On 12 Nov 2026, Novig's catalog listed 39 leagues you can pass in. (Source: [Novig API reference: List leagues](https://docs.novig.com/api-reference/catalog/list-leagues.md))

A sheet is the quickest way to watch a slate of Novig prices, compare them with other books or log bets for closing line value. Google Sheets can call novigapi.com through Apps Script; this guide adds one custom function, `=NOVIG_MARKETS("NBA")`, that fills a table.

## 1. Store the key in Script Properties

1. In your sheet, open Extensions, then Apps Script.
2. Click Project Settings (the gear), scroll to Script Properties and click Add script property.
3. Set the property to `NOVIGAPI_API_KEY` and the value to your `nv_` key. Save.

Script Properties keep the key out of the code and out of cells. Anyone with edit access to the script can still read it, so do not share edit access to a sheet that holds a paid key.

## 2. Add the function

In the Apps Script editor, replace the contents of `Code.gs` with this and save:

```javascript
/**
 * Novig markets for a league, from novigapi.com.
 * @param {string} league League code, such as "NBA" or "NFL".
 * @param {string} marketType Optional Novig market type, such as "STEALS".
 * @return Market, line, Over bid, Over ask, fair Over, fair Under, volume, start.
 * @customfunction
 */
function NOVIG_MARKETS(league, marketType) {
  if (!league) return [["Pass a league code, such as NBA"]];
  var key = PropertiesService.getScriptProperties().getProperty("NOVIGAPI_API_KEY");
  if (!key) return [["Set NOVIGAPI_API_KEY in Project Settings, Script Properties"]];

  var url = "https://novigapi.com/api/v1/markets?limit=200&league=" + encodeURIComponent(league);
  if (marketType) url += "&market_type=" + encodeURIComponent(marketType);

  var cache = CacheService.getScriptCache();
  var body = cache.get(url);
  if (!body) {
    var res = UrlFetchApp.fetch(url, { headers: { Authorization: "Bearer " + key }, muteHttpExceptions: true });
    if (res.getResponseCode() !== 200) {
      var err = JSON.parse(res.getContentText()).error || {};
      return [["Error " + res.getResponseCode(), err.message || ""]];
    }
    body = res.getContentText();
    if (body.length < 100000) cache.put(url, body, 60); // reuse for 60 s, saves credits
  }

  var page = JSON.parse(body);
  var rows = [["Market", "Line", "Over bid", "Over ask", "Fair Over", "Fair Under", "Volume", "Starts (UTC)"]];
  page.data.forEach(function (m) {
    var over = m.outcomes[0] || {}, under = m.outcomes[1] || {};
    rows.push([m.description, m.strike, over.bid, over.ask, over.fair, under.fair, m.volume, m.starts_at]);
  });
  rows.push(["Observed " + page.meta.observed_at + ", " + page.meta.source_age_seconds + " s old"]);
  return rows;
}
```

## 3. Use it

In any cell type `=NOVIG_MARKETS("NBA")`, or `=NOVIG_MARKETS("NBA", "STEALS")` for one market type. The first time, Sheets may ask you to authorize the script to connect to an external service. The table spills down and right from the cell.

Columns returned, with a real row from 12 Nov 2026

| Column | Example |
| --- | --- |
| Market | Cooper Flagg 1.5 STEALS |
| Line | 1.5 |
| Over bid | 0.39 |
| Over ask | 0.43 |
| Fair Over | 0.41 |
| Fair Under | 0.59 |
| Volume | 198265 |
| Starts (UTC) | 2026-11-13T01:05:00Z |


Prices are probabilities. To show American odds next to a fair price in cell E2, use `=IF(E2>=0.5, -100*E2/(1-E2), 100*(1-E2)/E2)`.

## Refresh and credits

Sheets recalculates a custom function when its arguments change and when the sheet reopens, not on a timer. The function caches each response for 60 seconds, so a recalculation inside a minute costs nothing. Each fresh call costs 1 credit per 100 rows requested, so a 200-row board costs 2 credits (Free pages stop at 100 rows). For a scheduled refresh, add a time-driven trigger that rewrites the cell (Triggers, then Add Trigger, every 5 or 10 minutes), and keep the interval slow: Free has 100 credits a month.

Custom functions must finish within 30 seconds and return at most what fits in the sheet, so filter by league and market type rather than fetching everything.

## Why not IMPORTDATA?

`IMPORTDATA(url)` fetches a CSV or TSV from a URL, but it cannot send request headers. novigapi.com requires the key in the `Authorization` header, so IMPORTDATA cannot call it, and putting a key in a URL inside a shared sheet would expose it anyway. IMPORTDATA can fetch public files with no key, such as Novig's own daily CSVs at data.novig.com, but those carry no player names, and trades.csv was about 209 MB for 11 Nov 2026, far beyond what a sheet should load. For a daily file in probabilities, call the [bulk CSV endpoint](https://novigapi.com/docs/endpoints/bulk-csv) from Apps Script with the same header pattern.

## Next steps

- Log your bets with the price you took and use the [CLV calculator](https://novigapi.com/clv-calculator) or [GET /v1/clv](https://novigapi.com/docs/endpoints/clv) to grade them against the close.
- Compare a sportsbook's price with Novig's fair price in the [EV calculator](https://novigapi.com/ev-calculator).
- Add a second function for [player props](https://novigapi.com/docs/endpoints/player-props) by copying this one and changing the path.

## Questions

### Does this work in Excel?

Not as written: Apps Script runs only in Google Sheets. Excel can call the API with Power Query, which can send an Authorization header.

### Why does the function show an error about NOVIGAPI_API_KEY?

The Script Property is missing or misspelled. It must be named exactly NOVIGAPI_API_KEY.

### How often does the data update?

Each fresh call returns data at most about 10 seconds old on paid plans. The sheet itself only refreshes when it recalculates or a trigger runs.

### Can I use the free plan?

Yes, with care: Free has 100 credits a month and 20 requests a minute, so keep refreshes infrequent.

## Sources

- [Google Apps Script: Custom functions in Google Sheets](https://developers.google.com/apps-script/guides/sheets/functions)
- [Google Apps Script: UrlFetchApp](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)
- [Google Apps Script: Properties service](https://developers.google.com/apps-script/guides/properties)
- [Google Docs Editors Help: IMPORTDATA](https://support.google.com/docs/answer/3093335)
- [Novig API reference: List leagues](https://docs.novig.com/api-reference/catalog/list-leagues.md)
- [Novig exchange data files (data.novig.com)](https://data.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.
