Odds API Excel and Google Sheets Guide: Power Query and Apps Script

Pull sportsbook odds into Excel with Power Query and into Google Sheets with Apps Script, with an Authorization header, refresh schedules, helpful formulas, a credit budget for refreshes and ways to keep your API key out of shared files.

A spreadsheet is still where a lot of betting analysis happens: a model in Excel, a shared tracker in Google Sheets, a quick comparison of a few books before a slate. This odds API Excel guide shows how to pull live sportsbook prices into both, using tools that are already built in: Power Query in Excel and Apps Script in Google Sheets.

To be clear up front: PreciseAPI has no native add-on for Excel or Google Sheets. There is nothing to install from an add-in store. Both routes below call the REST API directly, which takes about ten minutes to set up and gives you full control over which leagues, books and markets land in your sheet.

Before you start

You need three things:

That last point rules out the shortcuts. Excel's WEBSERVICE function and Google Sheets' IMPORTDATA fetch a URL but can't send headers, and the API refuses keys placed in the URL with 401 api_key_in_url, because URLs end up in logs and browser history. Both methods below send the header properly.

Odds API Excel setup with Power Query

These steps are written for Excel for Microsoft 365 and Excel 2016 or later on Windows, where the query editor is built in.

Step 1: Store the key in a parameter

Open Data → Get Data → Launch Power Query Editor, then Home → Manage Parameters → New Parameter. Name it ApiKey, set the type to Text, and paste your key as the current value. A parameter keeps the key out of every cell, and you'll clear it before sharing the file (see the last section).

Step 2: Create the query

In the editor, choose Home → New Source → Other Sources → Blank Query, open Advanced Editor, and replace its contents with:

let
    Response = Json.Document(
        Web.Contents(
            "https://api.preciseapi.com/v1/odds",
            [
                Query = [league = "nba", markets = "moneyline,spread,total", books = "pinnacle,draftkings,fanduel,betmgm"],
                Headers = [Authorization = "Bearer " & ApiKey]
            ]
        )
    ),
    Events = Table.FromList(Response[data], Splitter.SplitByNothing(), {"event"}),
    Games = Table.ExpandRecordColumn(Events, "event", {"id", "start_time", "status", "home", "away", "bookmakers"},
        {"event_id", "start_time", "status", "home", "away", "bookmakers"}),
    Named = Table.AddColumn(Games, "game", each [away][name] & " @ " & [home][name], type text),
    Books = Table.ExpandListColumn(Named, "bookmakers"),
    BookCols = Table.ExpandRecordColumn(Books, "bookmakers", {"id", "markets"}, {"book", "markets"}),
    Markets = Table.ExpandListColumn(BookCols, "markets"),
    MarketCols = Table.ExpandRecordColumn(Markets, "markets", {"key", "period", "updated_at", "outcomes"},
        {"market", "period", "updated_at", "outcomes"}),
    Outcomes = Table.ExpandListColumn(MarketCols, "outcomes"),
    OutcomeCols = Table.ExpandRecordColumn(Outcomes, "outcomes", {"id", "name", "side", "point", "price"},
        {"selection_id", "outcome", "side", "point", "price"}),
    Result = Table.SelectColumns(OutcomeCols, {"game", "start_time", "book", "market", "period", "outcome", "side",
        "point", "price", "updated_at", "selection_id", "event_id"}),
    Typed = Table.TransformColumnTypes(Result, {{"point", type number}, {"price", type number}})
in
    Typed

The query flattens the response into one row per outcome per book: the event, then each bookmaker, each market and each outcome. Change league, markets and books in the Query record to suit your sheet. league accepts several leagues separated by commas, and adding format = "decimal" returns decimal odds instead of American.

Step 3: Connect anonymously

The first time the query runs, Excel asks how to connect to api.preciseapi.com. Choose Anonymous. The key travels in the header you wrote, so no other credential is needed.

Most odds API Excel problems show up as an error code when the query refreshes:

Step 4: Load and schedule refreshes

Choose Close & Load to put the table on a sheet. Then open Data → Queries & Connections, right-click the query, choose Properties, and set Refresh every N minutes and, if you like, Refresh data when opening the file. Excel only refreshes while the workbook is open, which keeps the credit cost down.

Sports odds in Excel: formulas that help

With the table loaded (named Odds here), a few columns turn raw prices into something you can compare. Add them as columns of the loaded table so they survive refreshes. They assume the default American prices:

Column nameFormula
decimal=IF([@price]>0, 1+[@price]/100, 1+100/-[@price])
implied=1/[@decimal]
best=MAXIFS([decimal], [selection_id], [@selection_id])
is_best=[@decimal]=[@best]

The best-price formula works because the same outcome at the same number has the same selection_id at every book, so a spread of -3 is never compared with -3.5. Conditional formatting on the last column highlights the best price in each group. To check fair prices by hand, the no vig calculator and implied probability calculator use the same math. If you only want best prices, point the query at /v1/best-lines instead; the odds comparison API page shows its response shape.

Odds API Google Sheets setup with Apps Script

Google Sheets has no equivalent of Excel's query editor, but Apps Script can call any HTTP API with UrlFetchApp. In your spreadsheet, open Extensions → Apps Script, replace the editor's contents with the code below and save.

const BASE = 'https://api.preciseapi.com/v1';

// Run once from the editor: stores the key in your user properties, never in a cell.
function setKey() {
  const ui = SpreadsheetApp.getUi();
  const res = ui.prompt('PreciseAPI API key', ui.ButtonSet.OK_CANCEL);
  if (res.getSelectedButton() === ui.Button.OK) {
    PropertiesService.getUserProperties().setProperty('PRECISEAPI_KEY', res.getResponseText().trim());
  }
}

function api_(path, params) {
  const key = PropertiesService.getUserProperties().getProperty('PRECISEAPI_KEY');
  if (!key) throw new Error('No API key: run setKey() first.');
  const qs = Object.keys(params).map((k) => k + '=' + encodeURIComponent(params[k])).join('&');
  const res = UrlFetchApp.fetch(BASE + path + '?' + qs, {
    headers: { Authorization: 'Bearer ' + key },
    muteHttpExceptions: true,
  });
  let body = {};
  try { body = JSON.parse(res.getContentText()); } catch (e) {}
  if (res.getResponseCode() !== 200) {
    throw new Error('PreciseAPI ' + res.getResponseCode() + (body.error ? ' ' + body.error.code + ': ' + body.error.message : ''));
  }
  return body;
}

function refreshOdds() {
  const body = api_('/odds', { league: 'nba', markets: 'moneyline,spread,total', books: 'pinnacle,draftkings,fanduel,betmgm' });
  const rows = [['Game', 'Start (UTC)', 'Book', 'Market', 'Outcome', 'Point', 'Price', 'Updated', 'Selection id']];
  body.data.forEach((ev) => {
    const game = ev.away.name + ' @ ' + ev.home.name;
    ev.bookmakers.forEach((b) => b.markets.forEach((m) => m.outcomes.forEach((o) => {
      rows.push([game, ev.start_time, b.name, m.key, o.name, o.point === undefined ? '' : o.point, o.price, m.updated_at, o.id]);
    })));
  });
  const ss = SpreadsheetApp.getActive();
  const sheet = ss.getSheetByName('Odds') || ss.insertSheet('Odds');
  sheet.clearContents();
  sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
  const left = body.meta.credits_remaining === null ? 'unlimited' : body.meta.credits_remaining;
  sheet.getRange(1, 11).setValue('Refreshed ' + new Date().toISOString() + ', credits left: ' + left);
}

// Run once: refresh every 15 minutes (Apps Script allows 1, 5, 10, 15 or 30).
function installTrigger() {
  ScriptApp.newTrigger('refreshOdds').timeBased().everyMinutes(15).create();
}

Run setKey once and paste your key, then run refreshOdds and approve the permissions Google asks for. When the Odds sheet fills, run installTrigger to refresh on a schedule. The trigger runs as you, with your stored key, even when the spreadsheet is closed. Apps Script has its own daily quotas for URL fetches and trigger run time; a refresh every few minutes is well within them for a typical account.

Budgeting credits for refreshes

Every /v1/odds request costs 2 credits, however many books, markets and games it returns, and one request can cover several leagues. So the cost of a sheet depends only on how often it refreshes:

Refresh intervalRequests per dayCredits per 30 days
Every 60 minutes241,440
Every 15 minutes965,760
Every 5 minutes28817,280
Every minute1,44086,400

Edge includes 150,000 credits a month, so a single sheet refreshing every minute around the clock fits, with room for a second sheet on a slower schedule. Adding a /v1/props request (3 credits, 25 events per request) to each refresh changes the math quickly, so give props their own, slower trigger. A Google Sheets trigger runs day and night whether anyone is looking; an Excel workbook refreshes only while open. Sharp and Syndicate have unlimited credits. The odds API pricing page has the full plan table, and the credits and rate limits post goes deeper.

One limit that budgeting can't change: Edge and Sharp serve pre-game odds, so games leave the response once they start. In-play prices are on Syndicate, and a spreadsheet refreshing once a minute is too slow for in-play betting markets anyway.

Keep your key out of shared sheets

An API key in a shared file is a key anyone with the file can use against your credits. A few habits prevent that:

When a spreadsheet stops being enough

An odds API Excel workbook or a Google Sheet is good for pre-game comparison, model inputs and tracking. When you need every price move as it happens, a history of thousands of snapshots, or alerts, move the fetching to a small script. Our guide to how to get sports odds data ends with a Python script that writes CSV files on a schedule, and the betting odds API page describes the endpoints you'd use.

Frequently asked questions

Is there an odds API Excel add-in?

Not for PreciseAPI: there is no native add-on for Excel or Google Sheets. Excel connects through Power Query with an Authorization header, and Google Sheets through an Apps Script function using UrlFetchApp, as shown in this guide.

Why doesn't WEBSERVICE or IMPORTDATA work with the API?

Both functions fetch a URL but can't send request headers, and the API only accepts keys in a header. Keys placed in the URL are refused with 401 api_key_in_url, because URLs are written to logs.

How often can my sheet refresh?

As often as your credits allow. A /v1/odds refresh every minute uses 86,400 credits a month, within Edge's 150,000. Excel's scheduled refresh and Apps Script's time-driven triggers both go down to one minute.

Can I pull player props into a spreadsheet?

Yes. Point the query at /v1/props with a league and optional player filter. It costs 3 credits per request and returns up to 25 events at a time, so give props a slower refresh than game lines.

Will people I share the sheet with see my API key?

Not if you follow the steps above: the key never sits in a cell. In Excel, clear the parameter before sharing; in Google Sheets, keep the script in a file only you can edit, and use a dedicated key with a monthly credit cap.

Keep reading