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:
- An API key. Keys come with the paid plans, starting with Edge at $49.99 a month. The free Sandbox is a dashboard chat for asking about pre-game lines and has no API access.
- An endpoint.
/v1/odds(2 credits) returns every game in a league with each book's markets./v1/best-lines(3 credits) returns only the best price per outcome, and/v1/futures(2 credits) returns championship and award prices. - A header. The key must be sent in a request header, either
Authorization: Bearer <key>orX-API-Key: <key>.
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:
- 401: the key is missing or wrong. Check the
ApiKeyparameter value. - 403
sandbox_only: the key belongs to an account without a paid plan. - 400
invalid_parameter: a league, book or market id is misspelled. The free/v1/leagues,/v1/sportsbooksand/v1/marketsendpoints list the valid ids. - An empty table: no games are scheduled in that league right now, or they have all started (Edge and Sharp serve pre-game odds).
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 name | Formula |
|---|---|
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 interval | Requests per day | Credits per 30 days |
|---|---|---|
| Every 60 minutes | 24 | 1,440 |
| Every 15 minutes | 96 | 5,760 |
| Every 5 minutes | 288 | 17,280 |
| Every minute | 1,440 | 86,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:
- Never put the key in a cell. Both setups above keep it out of the grid.
- Clear the Excel parameter before sharing. The parameter value is saved in the workbook. Blank it before sending the file, or send a values-only copy: copy the table and paste values into a new workbook.
- Keep the Sheets script in a file only you edit. User properties are private to you, but anyone who can edit the spreadsheet can edit the script your trigger runs, which then runs with your key. Share the data as view-only, or keep the script in your own spreadsheet and let others read the results through a separate sheet that uses
IMPORTRANGE. - Use a dedicated key with limits. Create a key just for spreadsheets and give it a monthly credit cap and REST-only access in the key controls. If it leaks, roll it from the dashboard; the old key stops working immediately.
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.