Guide
Betting odds in Google Sheets
An Apps Script that fills a sheet with upcoming football odds and refreshes it on a timer. The API key stays in Script Properties, not in the sheet or the code.
Before you start
You need an API key; see Authentication. Anyone with edit access to the spreadsheet can open its script project and read Script Properties, so use a key only for this sheet and share the file with care. A restricted REST-only key limits the damage if it leaks.
Which bookmakers your key can read depends on your plan and bookmaker entitlements; see Bookmakers for the list your key returns. No bookmaker is in verified customer coverage right now. The examples use bookmaker=tipico and football. Examples use Tipico for illustration only. Tipico ingestion is offline for a source review and is not part of current customer coverage.
Store the key in Script Properties
- In the spreadsheet, open Extensions → Apps Script.
- Open Project Settings (gear icon) and scroll to Script Properties.
- Add the property
BETSPREAD_API_KEYwith your key (YOUR_API_KEY) as value and save.
Add the script
Replace the contents of Code.gs with the script below. refreshOdds pages through GET /api/v1/events from now on, reads GET /api/v1/events/{eventId}/odds for the first 5 events and writes one row per outcome to a sheet named Odds.
UrlFetchApp runs with muteHttpExceptions so the script can read the error code. 429 and 503 are retried after Retry-After seconds when that wait is short; longer waits, such as a used-up monthly quota, are left to the next run. Other errors stop the run with the error code in the execution log.
const API_BASE = "https://api.betspread.io/api/v1";
const SHEET_NAME = "Odds";
const MAX_EVENTS = 5;
const MAX_WAIT_SECONDS = 30;
function refreshOdds() {
const events = [];
const params = { sport: "football", dateFrom: new Date().toISOString(), limit: 100, offset: 0 };
while (events.length < MAX_EVENTS) {
const page = betspreadGet_("/events", params);
events.push(...page.data);
params.offset += page.meta.limit;
if (page.data.length === 0 || params.offset >= page.meta.total) break;
}
const rows = [["Kickoff (UTC)", "League", "Home", "Away", "Market", "Line", "Bookmaker", "Outcome", "Odds", "Last updated"]];
for (const event of events.slice(0, MAX_EVENTS)) {
const oddsParams = { bookmaker: "tipico" };
const odds = betspreadGet_(`/events/${event.id}/odds`, oddsParams).data;
for (const market of odds.markets) {
for (const bookmaker of market.bookmakers) {
for (const outcome of bookmaker.outcomes) {
rows.push([
event.kickoffAt, event.leagueName, event.homeTeam, event.awayTeam,
market.type, market.line ?? "", bookmaker.displayName,
outcome.outcome, outcome.odds, outcome.lastUpdatedAt,
]);
}
}
}
}
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getSheetByName(SHEET_NAME) || spreadsheet.insertSheet(SHEET_NAME);
sheet.clearContents();
sheet.getRange(1, 1, rows.length, rows[0].length).setValues(rows);
}
function betspreadGet_(path, params) {
const apiKey = PropertiesService.getScriptProperties().getProperty("BETSPREAD_API_KEY");
if (!apiKey) throw new Error("Add the BETSPREAD_API_KEY script property first.");
const query = Object.keys(params)
.map((name) => `${encodeURIComponent(name)}=${encodeURIComponent(params[name])}`)
.join("&");
for (let attempt = 1; attempt <= 3; attempt++) {
const response = UrlFetchApp.fetch(`${API_BASE}${path}?${query}`, {
headers: { Authorization: `Bearer ${apiKey}` },
muteHttpExceptions: true,
});
const status = response.getResponseCode();
if (status === 200) return JSON.parse(response.getContentText());
let code = "unknown";
try {
code = JSON.parse(response.getContentText()).error.code;
} catch (parseError) {
// Not a betspread error body.
}
const retryAfter = Number(header_(response, "Retry-After") || 2 ** attempt);
if ((status !== 429 && status !== 503) || retryAfter > MAX_WAIT_SECONDS) {
// Long waits are left to the next trigger run.
throw new Error(`betspread ${status} ${code}`);
}
Utilities.sleep(retryAfter * 1000);
}
throw new Error("betspread: retries exhausted");
}
function header_(response, name) {
const headers = response.getHeaders();
const match = Object.keys(headers).find((key) => key.toLowerCase() === name.toLowerCase());
return match ? headers[match] : null;
}Run refreshOdds once from the editor. Google asks you to authorize external requests and access to the spreadsheet.
Refresh on a trigger
Add this function to the same file and run it once. It replaces any earlier refreshOdds trigger with one that runs every 6 hours. You can also create the trigger by hand under Triggers (clock icon) as a time-driven trigger.
function installTrigger() {
for (const trigger of ScriptApp.getProjectTriggers()) {
if (trigger.getHandlerFunction() === "refreshOdds") ScriptApp.deleteTrigger(trigger);
}
ScriptApp.newTrigger("refreshOdds").timeBased().everyHours(6).create();
}Budget your quota
Every run makes one /events request plus one odds request per event: 6 requests. Every 6 hours that is 720 requests in a 30-day month, within the Free plan's 1,000 monthly REST requests. Raising MAX_EVENTS or the trigger frequency multiplies that number; check your plan on Rate limits first.
Troubleshooting
401 invalid_api_key: the key in Script Properties is mistyped, expired or revoked. A missing property stops the script before any request is sent.403 forbiddenor403 api_key_scope_denied: your plan or key does not cover this sport or bookmaker; see Bookmakers.- An empty sheet below the header: no upcoming events with odds from your entitled bookmakers right now.