Build a Telegram Football Betting Bot with UK Odds (Python)
Telegram betting bots are everywhere in UK football communities — sending odds alerts, tracking line movement, and notifying users when a specific bookmaker drops below a target price. Most are built on scraped data that breaks weekly. In this tutorial, you'll build one powered by a proper API with live odds from 25+ UK bookmakers.
By the end, you'll have a Telegram bot that responds to commands like /odds Arsenal vs Chelsea, /best Liverpool, /alert Bet365 Arsenal under 2.0, and sends automatic notifications when odds move.
What you'll build
A Telegram bot that:
- Fetches live football odds from all UK bookmakers on command
- Shows the best odds across Bet365, Sky Bet, Paddy Power, William Hill, and 20+ more
- Lets users set price alerts ("tell me when Bet365 odds on Arsenal drop below 1.80")
- Sends push notifications when alerts trigger
- Compares bookmaker margins so users know who's offering fair prices
Prerequisites
- Python 3.8+
- A UKOddsApi Starter plan (ukoddsapi.com)
- A Telegram Bot Token (from @BotFather)
pip install requests python-telegram-bot
Step 1: Set up the bot skeleton
"""
uk_odds_telegram_bot.py
A Telegram bot that delivers live UK football odds from 25+ bookmakers.
Powered by UKOddsApi.
Usage:
pip install requests python-telegram-bot
python uk_odds_telegram_bot.py
"""
import logging
import requests
from datetime import date, datetime
from telegram import Update
from telegram.ext import (
Application, CommandHandler, ContextTypes, CallbackContext
)
# ─── Configuration ───────────────────────────────────────
TELEGRAM_TOKEN = "your_telegram_bot_token"
UKODDS_API_KEY = "your_ukoddsapi_key"
UKODDS_BASE = "https://api.ukoddsapi.com"
# ─────────────────────────────────────────────────────────
ukodds_headers = {"X-Api-Key": UKODDS_API_KEY}
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# In-memory alert storage (use a database in production)
alerts = {} # chat_id -> [alert_configs]
Step 2: Add the /matches command
List today's fixtures so users know what's available:
async def matches_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""List today's football fixtures."""
league = " ".join(context.args) if context.args else "premier-league"
response = requests.get(
f"{UKODDS_BASE}/v1/football/events",
headers=ukodds_headers,
params={
"schedule_date": date.today().isoformat(),
"league": league,
}
)
if response.status_code != 200:
await update.message.reply_text("❌ Could not fetch fixtures. Try again.")
return
events = response.json().get("events", [])
if not events:
await update.message.reply_text(
f"No {league} matches today.\n\n"
f"Try: /matches championship\n"
f"Or: /matches league-one"
)
return
msg = f"⚽ *{league.replace('-', ' ').title()}* — {date.today().strftime('%d %b %Y')}\n\n"
for event in events:
kickoff = event["kickoff_utc"][11:16] # HH:MM
msg += f"🕐 {kickoff} — *{event['event_title']}*\n"
msg += f"\n📊 Use /odds [team name] for prices"
await update.message.reply_text(msg, parse_mode="Markdown")
Step 3: Add the /odds command
The core feature — pull live odds from all UK bookmakers for a match:
async def odds_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Get odds for a match. Usage: /odds Arsenal"""
if not context.args:
await update.message.reply_text(
"Usage: /odds [team name]\n"
"Example: /odds Arsenal\n"
"Example: /odds Liverpool"
)
return
search_term = " ".join(context.args).lower()
# Find matching fixture
response = requests.get(
f"{UKODDS_BASE}/v1/football/events",
headers=ukodds_headers,
params={"schedule_date": date.today().isoformat()}
)
events = response.json().get("events", [])
matching = [
e for e in events
if search_term in e["event_title"].lower()
]
if not matching:
# Try upcoming fixtures if nothing today
response = requests.get(
f"{UKODDS_BASE}/v1/football/events",
headers=ukodds_headers,
)
events = response.json().get("events", [])
matching = [
e for e in events
if search_term in e["event_title"].lower()
]
if not matching:
await update.message.reply_text(f"❌ No fixtures found matching '{search_term}'")
return
event = matching[0]
event_id = event["event_id"]
# Get best odds
response = requests.get(
f"{UKODDS_BASE}/v1/football/events/{event_id}/odds/best",
headers=ukodds_headers,
params={"package": "core", "odds_format": "decimal"}
)
best_data = response.json()
# Get all odds for the Win Market
response = requests.get(
f"{UKODDS_BASE}/v1/football/events/{event_id}/odds",
headers=ukodds_headers,
params={"package": "core", "odds_format": "decimal"}
)
all_data = response.json()
# Build message
kickoff = event["kickoff_utc"][11:16]
msg = f"⚽ *{event['event_title']}*\n"
msg += f"🕐 Kick-off: {kickoff} UTC\n\n"
# Show Win Market odds from all bookmakers
for market in all_data["markets"]:
if market["market_name"] != "Win Market":
continue
msg += f"📈 *{market['market_name']}*\n\n"
# Group by selection
selections = {}
for sel in market["selections"]:
sname = sel["selection_name"]
if sname not in selections:
selections[sname] = []
selections[sname].append({
"bookie": sel["bookmaker_name"],
"odds": sel["odds"]
})
for sname, bookies in selections.items():
bookies.sort(key=lambda x: -x["odds"])
best = bookies[0]
msg += f"*{sname}*\n"
for bm in bookies[:6]: # Top 6 bookmakers
icon = "🏆" if bm == best else " "
msg += f"{icon} {bm['odds']:.2f} — {bm['bookie']}\n"
if len(bookies) > 6:
msg += f" _+{len(bookies)-6} more bookmakers_\n"
msg += "\n"
# Add other markets summary
for market in best_data["markets"]:
if market["market_name"] == "Win Market":
continue
msg += f"📊 *{market['market_name']}*\n"
for sel in market["selections"]:
msg += f" {sel['selection_name']}: {sel['odds']} @ {sel['bookmaker_name']}\n"
msg += "\n"
msg += f"_Updated: {all_data['captured_at'][:19]}_\n"
msg += f"_Data from {all_data['summary']['bookmakers_count']} UK bookmakers_"
await update.message.reply_text(msg, parse_mode="Markdown")
Step 4: Add the /best command
Quick lookup — show just the best odds per outcome:
async def best_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Quick best odds lookup. Usage: /best Arsenal"""
if not context.args:
await update.message.reply_text("Usage: /best [team name]")
return
search_term = " ".join(context.args).lower()
# Find fixture
response = requests.get(
f"{UKODDS_BASE}/v1/football/events",
headers=ukodds_headers,
)
events = response.json().get("events", [])
matching = [e for e in events if search_term in e["event_title"].lower()]
if not matching:
await update.message.reply_text(f"❌ No match found for '{search_term}'")
return
event = matching[0]
response = requests.get(
f"{UKODDS_BASE}/v1/football/events/{event['event_id']}/odds/best",
headers=ukodds_headers,
params={"package": "core", "odds_format": "decimal"}
)
best = response.json()
msg = f"🏆 *Best odds — {event['event_title']}*\n\n"
for market in best["markets"]:
msg += f"*{market['market_name']}*\n"
for sel in market["selections"]:
msg += f" {sel['selection_name']}: *{sel['odds']}* @ {sel['bookmaker_name']}\n"
msg += "\n"
await update.message.reply_text(msg, parse_mode="Markdown")
Step 5: Add price alerts
Let users set alerts that trigger when a bookmaker's odds cross a threshold:
async def alert_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
Set a price alert.
Usage: /alert [bookmaker] [team] [direction] [price]
Example: /alert Bet365 Arsenal under 1.80
Example: /alert SkyBet Liverpool over 3.50
"""
if len(context.args) < 4:
await update.message.reply_text(
"Usage: /alert [bookmaker] [team] [under/over] [price]\n\n"
"Examples:\n"
"/alert Bet365 Arsenal under 1.80\n"
"/alert SkyBet Liverpool over 3.50\n"
"/alert PaddyPower Chelsea under 2.50"
)
return
bookmaker = context.args[0]
team = context.args[1].lower()
direction = context.args[2].lower()
target_price = float(context.args[3])
chat_id = update.effective_chat.id
if chat_id not in alerts:
alerts[chat_id] = []
alert_config = {
"bookmaker": bookmaker,
"team": team,
"direction": direction,
"target": target_price,
"created": datetime.now().isoformat(),
"triggered": False,
}
alerts[chat_id].append(alert_config)
emoji = "📉" if direction == "under" else "📈"
await update.message.reply_text(
f"{emoji} Alert set!\n\n"
f"I'll notify you when *{bookmaker}* odds on *{team.title()}* "
f"go *{direction} {target_price}*\n\n"
f"Use /myalerts to see all active alerts",
parse_mode="Markdown"
)
async def myalerts_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Show active alerts."""
chat_id = update.effective_chat.id
user_alerts = alerts.get(chat_id, [])
if not user_alerts:
await update.message.reply_text("No active alerts. Use /alert to set one.")
return
msg = "🔔 *Your active alerts*\n\n"
for i, a in enumerate(user_alerts, 1):
emoji = "📉" if a["direction"] == "under" else "📈"
status = "✅ Triggered" if a["triggered"] else "⏳ Watching"
msg += f"{i}. {emoji} {a['bookmaker']} — {a['team'].title()} {a['direction']} {a['target']} [{status}]\n"
msg += "\nUse /clearalerts to remove all alerts"
await update.message.reply_text(msg, parse_mode="Markdown")
async def clearalerts_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Clear all alerts."""
chat_id = update.effective_chat.id
alerts[chat_id] = []
await update.message.reply_text("🗑️ All alerts cleared.")
Step 6: Background alert checker
Set up a recurring job that polls odds and fires alerts when thresholds are crossed:
# Bookmaker name to code mapping (partial — extend as needed)
BOOKIE_CODES = {
"bet365": "UO004", "skybet": "UO020", "paddypower": "UO018",
"williamhill": "UO027", "ladbrokes": "UO015", "coral": "UO013",
"betfred": "UO007", "betvictor": "UO005", "888sport": "UO002",
"unibet": "UO024", "boylesports": "UO012", "betway": "UO008",
"betfair": "UO006", "matchbook": "UO017",
}
async def check_alerts(context: CallbackContext):
"""Background job to check price alerts."""
if not alerts:
return
# Get current fixtures
response = requests.get(
f"{UKODDS_BASE}/v1/football/events",
headers=ukodds_headers,
)
events = response.json().get("events", [])
for chat_id, user_alerts in alerts.items():
for alert in user_alerts:
if alert["triggered"]:
continue
# Find matching fixture
matching = [
e for e in events
if alert["team"] in e["event_title"].lower()
]
if not matching:
continue
event = matching[0]
bookie_code = BOOKIE_CODES.get(alert["bookmaker"].lower())
if not bookie_code:
continue
# Get current odds
response = requests.get(
f"{UKODDS_BASE}/v1/football/events/{event['event_id']}/odds",
headers=ukodds_headers,
params={
"package": "core",
"bookmaker_codes": bookie_code,
"odds_format": "decimal",
}
)
if response.status_code != 200:
continue
odds_data = response.json()
# Check Win Market for the team
for market in odds_data["markets"]:
if market["market_name"] != "Win Market":
continue
for sel in market["selections"]:
if alert["team"] not in sel["selection_name"].lower():
continue
current_odds = sel["odds"]
triggered = False
if alert["direction"] == "under" and current_odds <= alert["target"]:
triggered = True
elif alert["direction"] == "over" and current_odds >= alert["target"]:
triggered = True
if triggered:
alert["triggered"] = True
emoji = "📉" if alert["direction"] == "under" else "📈"
msg = (
f"🔔 *ALERT TRIGGERED!*\n\n"
f"{emoji} *{alert['bookmaker']}* odds on "
f"*{sel['selection_name']}* are now *{current_odds}*\n\n"
f"Target was: {alert['direction']} {alert['target']}\n"
f"Match: {event['event_title']}\n\n"
f"_Checked at {datetime.now().strftime('%H:%M:%S')}_"
)
await context.bot.send_message(
chat_id=chat_id,
text=msg,
parse_mode="Markdown"
)
Step 7: Add the /help command and wire everything up
async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Welcome message."""
msg = (
"🇬🇧 *UK Football Odds Bot*\n\n"
"Live odds from 25+ UK bookmakers including Bet365, Sky Bet, "
"Paddy Power, William Hill, Ladbrokes, and more.\n\n"
"*Commands:*\n"
"/matches — Today's fixtures\n"
"/matches championship — Other leagues\n"
"/odds Arsenal — Full odds comparison\n"
"/best Liverpool — Quick best odds\n"
"/alert Bet365 Arsenal under 1.80 — Price alert\n"
"/myalerts — View active alerts\n"
"/clearalerts — Remove all alerts\n\n"
"_Powered by UKOddsApi — ukoddsapi.com_"
)
await update.message.reply_text(msg, parse_mode="Markdown")
def main():
# Verify UKOddsApi key
r = requests.get(f"{UKODDS_BASE}/v1/auth/verify", headers=ukodds_headers)
if not r.json().get("ok"):
raise SystemExit("❌ Invalid UKOddsApi key. Get yours at ukoddsapi.com")
print("✅ UKOddsApi key verified")
# Build the bot
app = Application.builder().token(TELEGRAM_TOKEN).build()
# Commands
app.add_handler(CommandHandler("start", start_command))
app.add_handler(CommandHandler("help", start_command))
app.add_handler(CommandHandler("matches", matches_command))
app.add_handler(CommandHandler("odds", odds_command))
app.add_handler(CommandHandler("best", best_command))
app.add_handler(CommandHandler("alert", alert_command))
app.add_handler(CommandHandler("myalerts", myalerts_command))
app.add_handler(CommandHandler("clearalerts", clearalerts_command))
# Background alert checker — runs every 60 seconds
app.job_queue.run_repeating(check_alerts, interval=60, first=10)
print("🤖 Bot is running...")
app.run_polling()
if __name__ == "__main__":
main()
Deploying the bot
For the bot to run 24/7 and check alerts continuously, deploy it to a server:
Railway/Render (free tier): Push to GitHub, connect Railway or Render, set environment variables for TELEGRAM_TOKEN and UKODDS_API_KEY. The bot runs as a worker process.
VPS (DigitalOcean/Hetzner): SSH in, clone the repo, run in a screen or tmux session, or use systemd for auto-restart.
Docker:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY uk_odds_telegram_bot.py .
CMD ["python", "uk_odds_telegram_bot.py"]
Extending the bot
Some ideas for v2:
- Inline queries: Let users type
@yourbotname Arsenalin any chat to show odds inline - Player props: Add
/props Arsenal goalscorerusingpackage=fullon the Pro plan - Group mode: Post automatic odds updates to a group chat before every match
- Multiple leagues: Add buttons for Premier League, Championship, Champions League
- Bet tracking: Let users log bets and track P&L over time
API endpoints used
| Endpoint | Bot command |
|---|---|
GET /v1/football/events |
/matches |
GET /v1/football/events/{id}/odds |
/odds, alert checker |
GET /v1/football/events/{id}/odds/best |
/best |
GET /v1/bookmakers |
Bookmaker code lookups |
This bot is powered by UKOddsApi — live odds from 25+ UK bookmakers via a single REST API. Get your API key at ukoddsapi.com