Pull Live UK Football Odds into Google Sheets — No Code Required (2026)
You don't need to be a developer to use live UK bookmaker odds data. If you can use Google Sheets, you can pull live odds from Bet365, Sky Bet, Paddy Power, William Hill, and 20+ other UK bookmakers directly into a spreadsheet using Google Apps Script and UKOddsApi.
This is perfect for tipsters tracking odds movement, content creators building "best odds" tables for blog posts, analysts comparing bookmaker margins, or anyone who works in spreadsheets and needs live football odds.
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 25+ 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 25+ 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 25+ 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