Tutorials

Build a Telegram Football Betting Bot with UK Odds (Python)

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 Arsenal in any chat to show odds inline
  • Player props: Add /props Arsenal goalscorer using package=full on 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