You don't need to be a developer to use live UK bookmaker odds. If you can use Google Sheets, you can pull prices from Bet365, Sky Bet, Paddy Power, William Hill, and other UK books into a spreadsheet with Google Apps Script — no Python app, no scrapers.
What is UK Odds API?
UK Odds API is a UK-first REST API for pre-match football odds from major UK bookmakers and exchanges — Bet365, Sky Bet, Paddy Power, William Hill, Coral, Ladbrokes, Betfair, and more — in one OpenAPI contract. This tutorial uses that API from Google Apps Script so tipsters, analysts, and content teams can keep a live odds sheet without building a backend.
Coverage: bookmaker coverage · Docs: api.ukoddsapi.com/docs · Get a key: sign up.
This is ideal for tipsters tracking line movement, writers building “best odds” tables, analysts comparing bookmaker margins, or anyone who lives in spreadsheets.
What you'll build
A Google Sheets workbook that:
- Pulls live odds from all UK bookmakers into your spreadsheet
- Auto-refreshes on a schedule (every 5, 15, or 30 minutes)
- Highlights the best and worst odds per selection
- Calculates bookmaker margins (overround)
- Works for any league — Premier League, Championship, Champions League, and more
Step 1: Set up your Google Sheet
- Create a new Google Sheet
- Rename the first tab to "Config"
- In cell A1, type
API Keyand in B1, paste your UKOddsApi key - In cell A2, type
Leagueand in B2, typepremier-league - Create a second tab called "Odds"
Step 2: Add the Apps Script
Go to Extensions → Apps Script, delete any existing code, and paste this:
/**
* UK Football Odds Tracker
* Pulls live odds from 34 UK bookmakers into Google Sheets.
* Powered by UKOddsApi — https://ukoddsapi.com
*
* Setup:
* 1. Get a free API key at ukoddsapi.com
* 2. Paste it in the Config tab, cell B1
* 3. Run fetchOdds() or set up a trigger for auto-refresh
*/
const BASE_URL = "https://api.ukoddsapi.com";
function getConfig() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Config");
return {
apiKey: sheet.getRange("B1").getValue(),
league: sheet.getRange("B2").getValue() || "premier-league",
};
}
function apiCall(endpoint, params) {
const config = getConfig();
const queryString = Object.entries(params || {})
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
.join("&");
const url = `${BASE_URL}${endpoint}${queryString ? "?" + queryString : ""}`;
const response = UrlFetchApp.fetch(url, {
headers: { "X-Api-Key": config.apiKey },
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error(`API error ${response.getResponseCode()}: ${response.getContentText()}`);
}
return JSON.parse(response.getContentText());
}
/**
* Main function — fetches odds and writes to the Odds tab.
* Run this manually or set up a time-based trigger.
*/
function fetchOdds() {
const config = getConfig();
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Odds");
// Clear existing data
sheet.clearContents();
// Get today's date
const today = Utilities.formatDate(new Date(), "UTC", "yyyy-MM-dd");
// Fetch fixtures
let events;
try {
const data = apiCall("/v1/football/events", {
league: config.league,
schedule_date: today,
});
events = data.events || [];
} catch (e) {
sheet.getRange("A1").setValue("Error: " + e.message);
return;
}
if (events.length === 0) {
// Try upcoming if nothing today
const data = apiCall("/v1/football/events", { league: config.league });
events = (data.events || []).slice(0, 10);
}
// Headers
let row = 1;
sheet.getRange(row, 1).setValue("UK Football Odds — " + config.league.replace(/-/g, " ").toUpperCase());
sheet.getRange(row, 1).setFontSize(14).setFontWeight("bold");
row++;
sheet.getRange(row, 1).setValue("Last updated: " + new Date().toLocaleString());
sheet.getRange(row, 1).setFontColor("#888888");
row += 2;
// Process each fixture
for (const event of events.slice(0, 10)) {
try {
// Get best odds
const bestData = apiCall(`/v1/football/events/${event.event_id}/odds/best`, {
package: "core",
odds_format: "decimal",
});
// Get all odds
const allData = apiCall(`/v1/football/events/${event.event_id}/odds`, {
package: "core",
odds_format: "decimal",
});
// Match header
sheet.getRange(row, 1).setValue("⚽ " + event.event_title);
sheet.getRange(row, 1).setFontSize(12).setFontWeight("bold");
sheet.getRange(row, 3).setValue("Kick-off: " + event.kickoff_utc);
row++;
// Process Win Market
for (const market of allData.markets) {
if (market.market_name !== "Win Market") continue;
// Get unique bookmaker list
const bookmakers = [];
const seen = new Set();
for (const sel of market.selections) {
if (!seen.has(sel.bookmaker_name)) {
seen.add(sel.bookmaker_name);
bookmakers.push(sel.bookmaker_name);
}
}
// Column headers: Selection | Bookmaker1 | Bookmaker2 | ... | Best | Best Bookie
sheet.getRange(row, 1).setValue(market.market_name).setFontWeight("bold");
row++;
const headerRow = row;
sheet.getRange(row, 1).setValue("Selection");
for (let i = 0; i < bookmakers.length; i++) {
sheet.getRange(row, i + 2).setValue(bookmakers[i]);
}
sheet.getRange(row, bookmakers.length + 2).setValue("BEST");
sheet.getRange(row, bookmakers.length + 3).setValue("Best Bookie");
// Bold and colour the header row
sheet.getRange(row, 1, 1, bookmakers.length + 3)
.setFontWeight("bold")
.setBackground("#f0f0f0");
row++;
// Group selections
const selections = {};
for (const sel of market.selections) {
if (!selections[sel.selection_name]) {
selections[sel.selection_name] = {};
}
selections[sel.selection_name][sel.bookmaker_name] = sel.odds;
}
// Find best odds per selection from the best endpoint
const bestLookup = {};
for (const bmarket of bestData.markets) {
if (bmarket.market_name !== "Win Market") continue;
for (const sel of bmarket.selections) {
bestLookup[sel.selection_name] = {
odds: sel.odds,
bookmaker: sel.bookmaker_name,
};
}
}
// Write data rows
for (const [selName, bookieOdds] of Object.entries(selections)) {
sheet.getRange(row, 1).setValue(selName);
const allOdds = [];
for (let i = 0; i < bookmakers.length; i++) {
const bname = bookmakers[i];
const odds = bookieOdds[bname];
if (odds) {
sheet.getRange(row, i + 2).setValue(odds);
allOdds.push({ odds, col: i + 2 });
}
}
// Best odds
const best = bestLookup[selName];
if (best) {
sheet.getRange(row, bookmakers.length + 2)
.setValue(best.odds)
.setFontWeight("bold")
.setFontColor("#059669");
sheet.getRange(row, bookmakers.length + 3)
.setValue(best.bookmaker)
.setFontColor("#059669");
}
// Highlight best and worst in the row
if (allOdds.length > 1) {
const maxOdds = Math.max(...allOdds.map((o) => o.odds));
const minOdds = Math.min(...allOdds.map((o) => o.odds));
for (const o of allOdds) {
if (o.odds === maxOdds) {
sheet.getRange(row, o.col).setBackground("#dcfce7"); // Green
} else if (o.odds === minOdds) {
sheet.getRange(row, o.col).setBackground("#fef2f2"); // Red
}
}
}
row++;
}
// Margin row
sheet.getRange(row, 1).setValue("Margin").setFontStyle("italic");
for (let i = 0; i < bookmakers.length; i++) {
const bname = bookmakers[i];
let totalProb = 0;
let count = 0;
for (const selOdds of Object.values(selections)) {
if (selOdds[bname]) {
totalProb += 1 / selOdds[bname];
count++;
}
}
if (count >= 3) {
const margin = ((totalProb - 1) * 100).toFixed(1) + "%";
const cell = sheet.getRange(row, i + 2);
cell.setValue(margin).setFontStyle("italic");
const marginVal = (totalProb - 1) * 100;
if (marginVal < 3) cell.setFontColor("#059669");
else if (marginVal < 5) cell.setFontColor("#d97706");
else cell.setFontColor("#dc2626");
}
}
row++;
}
row += 2; // Space between fixtures
} catch (e) {
sheet.getRange(row, 1).setValue("Error loading " + event.event_title + ": " + e.message);
row += 2;
}
}
// Auto-resize columns
for (let i = 1; i <= 20; i++) {
sheet.autoResizeColumn(i);
}
SpreadsheetApp.getActiveSpreadsheet().toast("Odds updated!", "✅ Success", 3);
}
/**
* Set up auto-refresh. Run this once.
* Creates a trigger that refreshes odds every 15 minutes.
*/
function setupAutoRefresh() {
// Remove existing triggers
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === "fetchOdds") {
ScriptApp.deleteTrigger(trigger);
}
}
// Create new trigger — every 15 minutes
ScriptApp.newTrigger("fetchOdds")
.timeBased()
.everyMinutes(15)
.create();
SpreadsheetApp.getActiveSpreadsheet().toast(
"Auto-refresh set to every 15 minutes",
"✅ Trigger Created",
5
);
}
/**
* Stop auto-refresh.
*/
function stopAutoRefresh() {
const triggers = ScriptApp.getProjectTriggers();
for (const trigger of triggers) {
if (trigger.getHandlerFunction() === "fetchOdds") {
ScriptApp.deleteTrigger(trigger);
}
}
SpreadsheetApp.getActiveSpreadsheet().toast("Auto-refresh stopped", "🛑 Stopped", 3);
}
/**
* Add a custom menu to the spreadsheet.
*/
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("🇬🇧 UK Odds")
.addItem("🔄 Refresh Odds Now", "fetchOdds")
.addItem("⏰ Start Auto-Refresh (15 min)", "setupAutoRefresh")
.addItem("🛑 Stop Auto-Refresh", "stopAutoRefresh")
.addToUi();
}
Step 3: Run it
- Save the script (Ctrl+S)
- Go back to your spreadsheet
- Reload the page — you'll see a new "🇬🇧 UK Odds" menu
- Click 🇬🇧 UK Odds → 🔄 Refresh Odds Now
- Authorise the script when prompted (it needs permission to make API calls)
Your spreadsheet will fill with live odds from every UK bookmaker, colour-coded with green (best odds) and red (worst odds) highlighting, plus bookmaker margin calculations.
Step 4: Set up auto-refresh
Click 🇬🇧 UK Odds → ⏰ Start Auto-Refresh to automatically update every 15 minutes. The script uses Google's time-based triggers, so it runs even when you're not looking at the sheet.
To stop auto-refresh: 🇬🇧 UK Odds → 🛑 Stop Auto-Refresh
Customisation
Change the league: Edit cell B2 in the Config tab. Options include premier-league, championship, league-one, league-two, champions-league, fa-cup, and more.
Change refresh interval: Edit the setupAutoRefresh function — change everyMinutes(15) to everyMinutes(5) or everyMinutes(30).
Add more markets: The script currently shows the Win Market. To add BTTS and Over/Under, remove the if (market.market_name !== "Win Market") continue; filter in the fetchOdds function.
Show fractional odds: Change odds_format: "decimal" to odds_format: "fractional" in the API calls.
Use cases
Tipsters: Build "best odds" tables for your tips and embed them in blog posts or Twitter threads. The spreadsheet auto-updates, so your published odds stay current.
Content creators: Generate odds comparison data for articles, YouTube videos, or social media posts. Export as CSV or copy-paste formatted tables.
Analysts: Track bookmaker margins over time. Copy the Odds tab to a new sheet every matchday to build a historical record of which bookmakers consistently offer the best value.
Personal betting: Before placing a bet, check all 34 UK bookmakers in one glance. The green highlighting instantly shows you where to place your bet for the best return.
API endpoints used
| Endpoint | Purpose |
|---|---|
GET /v1/football/events |
Fetch fixtures by league and date |
GET /v1/football/events/{id}/odds |
All bookmaker odds |
GET /v1/football/events/{id}/odds/best |
Best odds per selection |
UKOddsApi delivers odds from 34 UK bookmakers into any tool — Python, JavaScript, Google Sheets, or anything that can make an HTTP request. Get your free API key at ukoddsapi.com