Build a Python Telegram Bot That Monitors Any Website and Alerts You Instantly

Learn How to Create a Python Telegram Bot That Monitors Any Website and Alerts You Instantly: Step-by-Step Guide for Beginners:

Build a Python Telegram Bot That Monitors Any Website and Alerts You Instantly

Imagine you run a website, an online shop, or a personal blog. What if your site goes down at 3 AM and you don't know until your customers start complaining? That's a nightmare scenario — and it's completely avoidable.

In this tutorial, you'll learn how to build a Python Telegram Bot that watches your websites 24/7 and sends you an instant Telegram alert the moment something goes wrong — whether the site is down, too slow, or missing critical content.

This is a real-world, production-ready Python project that teaches you:

  • Asynchronous Python programming (async/await).
  • HTTP request handling with aiohttp.
  • Telegram Bot API integration.
  • JSON-based data persistence.
  • Background task loops.

Who Is This Tutorial For?

  • Beginners who know basic Python and want a real project to build.
  • Developers who want to monitor their own websites for free.
  • Bug bounty hunters who need to track when target sites change.
  • Freelancers who manage client websites and need uptime alerts.
  • Students learning Python automation, bots, and async programming.

Table of Contents

  1. Get Your Telegram Bot Token
  2. Install Dependencies
  3. Create Your .env File
  4. Settings & Defaults (config.py)
  5. JSON Database (Persistent State)
  6. HTTP Check Engine
  7. The Background Loop
  8. Main Bot + All Commands
  9. Conclusion

Introduction

Never miss a downtime again. In this tutorial, you'll learn how to build a Python Telegram bot that automatically monitors any website 24/7 and sends you an instant Telegram alert the moment something changes — whether your site goes down, recovers, or loses critical content.

Whether you're a web developer protecting your live site, an ethical hacker tracking page changes for bug bounty hunting, or a Python beginner looking for a real-world automation project — this guide is built for you.

By the end, you'll have a fully working website monitoring Python script with commands like /add, /status, and /pause — all controlled directly from your Telegram app.

Project Architecture — How the Components Fit Together

Before diving into code, understand how the five Python files work together:

Build a Python Telegram Bot That Monitors Any Website and Alerts You Instantly

In Simple Terms:

  • config.py — reads your secret setting
  • storage.py — saves and loads site data from a JSON file.
  • checker.py — visits each website and checks if it's alive.
  • monitor.py — runs forever in the background, checking sites on a schedule.
  • bot.py — the brain: handles your Telegram commands and starts everything.

Project File Structure

website-monitor-bot/
├── bot.py           ← Main bot: all Telegram commands
├── monitor.py       ← Background loop: checks sites automatically
├── checker.py       ← HTTP engine: visits websites, checks status
├── storage.py       ← Database: reads/writes sites.json
├── config.py        ← Settings: loads .env variables
├── sites.json       ← Auto-created: stores all your site data
├── .env             ← Secret file: bot token and chat ID
└── requirements.txt ← Python packages needed

Step 1: Get Your Telegram Bot Token

Before writing any code, you need a Telegram bot token.

  1. Open Telegram → search for "BotFather"
  2. Send: /newbot
  3. Follow the prompts → give your bot a name
  4. Copy the token BotFather gives you (looks like: 123456789:ABCdef...)
  5. Start your bot, then visit: https://api.telegram.org/bot/getUpdates
  6. Send any message to your bot
  7. Note your "chat_id" from the JSON response

Beginner Tip: Never share your bot token publicly. Treat it like a password. Anyone with your token can control your bot.


Step 2: Install Dependencies

Bash

pip install python-telegram-bot requests beautifulsoup4 python-dotenv aiohttp

What each package does:

Package Purpose
python-telegram-bot Connects your Python code to Telegram's API
aiohttp Makes fast, asynchronous HTTP requests to websites
beautifulsoup4 Parses HTML (used for content checking)
python-dotenv Loads secret values from your .env file
requests Standard HTTP library (fallback utility)

Step 3: Create Your .env File

Create a file named .env in your project folder:

BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrstUVwxyz
CHAT_ID=987654321
CHECK_INTERVAL=60

What each variable means:

  • BOT_TOKEN: Your Telegram bot's unique token for authentication.

  • CHAT_ID: The ID of the chat where you want to receive notifications.

  • CHECK_INTERVAL: How often (in seconds) the bot should check the website for changes.

Beginner Tip: Never commit your .env file to GitHub. Add it to .gitignore to keep your secrets safe.


Step 4: Settings & Defaults (config.py)

config.py

                            
import os
from dotenv import load_dotenv

load_dotenv()

BOT_TOKEN      = os.getenv("BOT_TOKEN")
CHAT_ID        = os.getenv("CHAT_ID")
CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", 60))  # default: 60 seconds
STORAGE_FILE   = "sites.json"
REQUEST_TIMEOUT = 10   # max seconds to wait for a site response
MAX_SITES      = 50    # maximum number of sites per bot instance
                        

What This Does

  • load_dotenv() reads your .env file and makes those values available as environment variables.
  • os.getenv("BOT_TOKEN") fetches the value of BOT_TOKEN from the environment.
  • int(os.getenv("CHECK_INTERVAL", 60)) if CHECK_INTERVAL is not set in .env, it defaults to 60.
  • STORAGE_FILE the name of the JSON file used as a simple database.
  • REQUEST_TIMEOUT if a website doesn't respond within 10 seconds, it's marked as down.
  • MAX_SITES prevents abuse by limiting to 50 monitored sites.

Centralizing all settings in one file means you only need to change one place when adjusting bot behavior. This is a professional practice called configuration management.


Step 5: JSON Database (Persistent State) [storage.py]

storage.py

                            
import json, os
from config import STORAGE_FILE

# Default data structure when no sites.json exists yet
DEFAULT = {"sites": {}, "paused": False}

def load() -> dict:
    """Read sites.json from disk. Return default if file doesn't exist."""
    if os.path.exists(STORAGE_FILE):
        with open(STORAGE_FILE, "r") as f:
            return json.load(f)
    return dict(DEFAULT)

def save(data: dict):
    """Write updated data back to sites.json."""
    with open(STORAGE_FILE, "w") as f:
        json.dump(data, f, indent=2)

def get_sites() -> dict:
    """Return the dictionary of all monitored sites."""
    return load().get("sites", {})

def add_site(url: str, name: str, keyword: str = None):
    """Add a new site to the watchlist."""
    data = load()
    data["sites"][url] = {
        "name": name,
        "keyword": keyword,
        "status": "unknown",
        "last_checked": None,
        "response_time": None,
        "failures": 0
    }
    save(data)

def remove_site(url: str) -> bool:
    """Remove a site from the watchlist. Returns True if found."""
    data = load()
    if url in data["sites"]:
        del data["sites"][url]
        save(data)
        return True
    return False

def update_site(url: str, **kwargs):
    """Update specific fields for a site (status, response time, etc.)."""
    data = load()
    if url in data["sites"]:
        data["sites"][url].update(kwargs)
        save(data)

def is_paused() -> bool:
    """Check if monitoring is currently paused."""
    return load().get("paused", False)

def set_paused(val: bool):
    """Set the global monitoring pause state."""
    data = load()
    data["paused"] = val
    save(data)

                        

Code Explanation

Imports & Config

                            
import json, os
from config import STORAGE_FILE
                        
  • json — reads/writes JSON files.
  • os — checks if the file exists on disk.
  • STORAGE_FILE — a file path constant imported from your config (e.g., "storage.json" or "data/sites.json")

Default Data Structure

                            
DEFAULT = {
    "sites": {},   # url -> { name, keyword, status, last_seen, response_time, added_by }
    "paused": False
}
                        

This defines the skeleton of your JSON file when it doesn't exist yet:

Key Type Purpose
"sites" dict All monitored websites, keyed by URL
"paused" bool Global toggle to pause all monitoring

Each site entry (nested inside "sites") stores:

                            
{
  "https://example.com": {
    "name": "Example Site",
    "keyword": "Welcome",
    "status": "unknown",
    "last_checked": null,
    "response_time": null,
    "failures": 0
  }
}
                        

Core Read/Write Functions

➤ load() — Read from disk

                            
def load() -> dict:
    if os.path.exists(STORAGE_FILE):
        with open(STORAGE_FILE, "r") as f:
            return json.load(f)
    return dict(DEFAULT)
                        

Flow:

  1. Checks if the JSON file exists on disk.
  2. If yes → opens it and parses it into a Python dict.
  3. If no → returns a fresh copy of DEFAULT.

Site Management Functions

➤ get_sites() — Fetch all monitored sites

                            
def get_sites() -> dict:
    return load().get("sites", {})
                        
  • Loads the file and returns only the "sites" dictionary.
  • Falls back to {} if "sites" key is somehow missing.

➤ add_site() — Register a new site

                            
def add_site(url: str, name: str, keyword: str = None):
    data = load()
    data["sites"][url] = {
        "name": name,
        "keyword": keyword,
        "status": "unknown",
        "last_checked": None,
        "response_time": None,
        "failures": 0,
    }
    save(data)
                        
  • url is the dictionary key — acts as a unique ID for each site.
  • keyword is optional (= None) — used to check if a specific word appears in the response body.
  • All monitoring fields start at neutral defaults:
    • status: "unknown" — not yet checked
    • last_checked: None — never checked
    • response_time: None — not measured yet
    • failures: 0 — no failures recorded

➤ remove_site() — Delete a site

                            
def remove_site(url: str) -> bool:
    data = load()
    if url in data["sites"]:
        del data["sites"][url]
        save(data)
        return True
    return False
                        
  1. Returns True if the site was found and deleted.
  2. Returns False if the URL didn't exist — lets the caller handle the "not found" case gracefully.
  3. Safe — won't crash if URL is missing.

➤ update_site() — Updates site fields

                            
def update_site(url: str, **kwargs):
    data = load()
    if url in data["sites"]:
        data["sites"][url].update(kwargs)
        save(data)
                        
  1. **kwargs accepts any number of keyword arguments dynamically.
  2. .update(kwargs) merges them into the existing site dict — only updating the fields you pass.
  3. Used by the monitoring engine to write results back, e.g.:
  4.                             
    # Called after checking a site
    update_site(
        "https://example.com",
        status="up",
        last_checked="2026-04-08T12:00:00",
        response_time=0.342,
        failures=0
    )
                            

Pause Control Functions

➤ is_paused() — Check if monitoring is paused

                            
def is_paused() -> bool:
    return load().get("paused", False)
                        
  1. Returns True or False.
  2. The monitoring loop checks this before each check cycle — if True, it skips all checks.

➤ set_paused() — Pause or resume monitoring

                            
def set_paused(val: bool):
    data = load()
    data["paused"] = val
    save(data)
                        

Usage:

set_paused(True)   # Pause all monitoring
set_paused(False)  # Resume monitoring

Step 6: HTTP Check Engine [checker.py]

                            
import aiohttp
import asyncio
from datetime import datetime


async def check_site(url: str, keyword: str = None, timeout: int = 10) -> dict:
    """
    Returns a result dict:
      status     : "up" | "down" | "keyword_missing"
      status_code: int or None
      response_ms: int (milliseconds)
      error      : str or None
      keyword_found: bool or None
    """
    result = {
        "status": "down",
        "status_code": None,
        "response_ms": None,
        "error": None,
        "keyword_found": None,
        "checked_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    }

    headers = {
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 Chrome/122.0 Safari/537.36"
        )
    }

    try:
        start = asyncio.get_event_loop().time()
        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers,
                                   timeout=aiohttp.ClientTimeout(total=timeout),
                                   allow_redirects=True, ssl=False) as resp:

                elapsed_ms = int((asyncio.get_event_loop().time() - start) * 1000)
                result["status_code"] = resp.status
                result["response_ms"] = elapsed_ms

                if resp.status == 200:
                    if keyword:
                        body = await resp.text()
                        if keyword.lower() in body.lower():
                            result["status"] = "up"
                            result["keyword_found"] = True
                        else:
                            result["status"] = "keyword_missing"
                            result["keyword_found"] = False
                    else:
                        result["status"] = "up"
                else:
                    result["status"] = "down"
                    result["error"] = f"HTTP {resp.status}"

    except aiohttp.ClientConnectorError:
        result["error"] = "Connection refused / DNS failure"
    except asyncio.TimeoutError:
        result["error"] = f"Timeout after {timeout}s"
    except Exception as e:
        result["error"] = str(e)

    return result
                        

Code Breakdown

Imports

                            
import aiohttp       # Async HTTP client (non-blocking requests)
import asyncio       # Python's async event loop framework
from datetime import datetime  # For timestamping the check
                        
  • aiohttp — like requests but async; can check hundreds of sites simultaneously without waiting for each one.
  • asyncio — manages the event loop that runs async tasks concurrently.

Function Signature

                            
async def check_site(url: str, keyword: str = None, timeout: int = 10) -> dict:
                        
Parameter Type Purpose
url str The website URL to check
keyword str (optional) A word to search in the page body
timeout int Max seconds to wait (default: 10)

async def — marks this as a coroutine; must be await-ed when called.

Result Dictionary (Default State)

result = {
    "status": "down",          # Assume down until proven otherwise
    "status_code": None,       # HTTP status code (200, 404, 500...)
    "response_ms": None,       # Response time in milliseconds
    "error": None,             # Error message if something fails
    "keyword_found": None,     # True/False if keyword was searched
    "checked_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}

This is initialized as pessimistic — everything starts as "down" and gets updated only if the request succeeds.

Fake Browser Headers

headers = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 Chrome/122.0 Safari/537.36"
    )
}

Disguises the HTTP request as a real Chrome browser on Windows. Without this, many sites block bots or return different responses.

The Core Request (Step by Step)

                            
start = asyncio.get_event_loop().time()
                        

Records the start time using the event loop's high-precision clock (more accurate than time.time() for async code).

                            
async with aiohttp.ClientSession() as session:
                        

Opens an HTTP session — like a browser session. The async with ensures it's properly closed afterward even if an error occurs.

                            
async with session.get(
    url,
    headers=headers,
    timeout=aiohttp.ClientTimeout(total=timeout),
    allow_redirects=True,
    ssl=False
) as resp:
                        
Argument / Parameter Type Purpose
url str Target website URL
headers dict Sends fake browser identity (User-Agent)
timeout int Kills request after N seconds (Default: 10)
allow_redirects=True bool Follows HTTP 301/302 redirects automatically
ssl=False bool Skips SSL verification (useful for self-signed or broken HTTPS)

Measuring Response Time

                            
elapsed_ms = int((asyncio.get_event_loop().time() - start) * 1000)
result["status_code"] = resp.status
result["response_ms"] = elapsed_ms
                        

Status Logic (The Decision Tree)

HTTP 200 received?
├── YES → keyword provided?
│         ├── YES → keyword in body?
│         │         ├── YES → status = "up", keyword_found = True
│         │         └── NO  → status = "keyword_missing", keyword_found = False
│         └── NO  → status = "up"
└── NO  → status = "down", error = "HTTP {code}"
                            
if resp.status == 200:
    if keyword:
        body = await resp.text()          # Read full HTML body
        if keyword.lower() in body.lower():  # Case-insensitive search
            result["status"] = "up"
            result["keyword_found"] = True
        else:
            result["status"] = "keyword_missing"
            result["keyword_found"] = False
    else:
        result["status"] = "up"
else:
    result["status"] = "down"
    result["error"] = f"HTTP {resp.status}"
                        

The 3 possible outcomes:

Status Meaning
"up" Site responded 200, keyword found (or not required)
"keyword_missing" Site is online but expected content not found
"down" Non-200 response (404, 500, etc.)

Error Handling

                            
except aiohttp.ClientConnectorError:
    result["error"] = "Connection refused / DNS failure"
                        

Site doesn't exist, server is down, or DNS can't resolve the domain.

                            
except asyncio.TimeoutError:
    result["error"] = f"Timeout after {timeout}s"
                        

Server took too long to respond — likely overloaded or dead.

                            
except Exception as e:
    result["error"] = str(e)
                        

Catches any other unexpected error (SSL errors if ssl=True, malformed URL, etc.).


Step 7: The Background Loop [monitor.py]

This is the brain of the monitoring system. It runs silently in the background, checks every website on your list at regular intervals, and fires a Telegram alert instantly when a site's status changes.

                            
import asyncio
from datetime import datetime
from checker import check_site
from storage import get_sites, update_site, is_paused
from config import CHECK_INTERVAL, CHAT_ID


def status_emoji(status: str) -> str:
    return {"up": "✅", "down": "🔴", "keyword_missing": "⚠️", "unknown": "❓"}.get(status, "❓")


async def run_monitor(app):
    """Runs forever in the background — checks all sites on interval."""
    print(f"🔍 Monitor started — checking every {CHECK_INTERVAL}s")

    while True:
        if not is_paused():
            sites = get_sites()

            for url, info in sites.items():
                prev_status = info.get("status", "unknown")
                result = await check_site(url, info.get("keyword"))
                new_status = result["status"]

                update_site(url,
                    status=new_status,
                    last_checked=result["checked_at"],
                    response_time=result["response_ms"],
                    failures=info.get("failures", 0) + (1 if new_status != "up" else 0)
                )

                # Only alert on STATUS CHANGE (no spam)
                if new_status != prev_status:
                    await send_alert(app, url, info["name"], prev_status,
                                     new_status, result)

        await asyncio.sleep(CHECK_INTERVAL)


async def send_alert(app, url: str, name: str, prev: str, curr: str, result: dict):
    """Fire an instant Telegram alert on status change."""
    emoji = status_emoji(curr)
    prev_emoji = status_emoji(prev)

    if curr == "up":
        header = f"*SITE RECOVERED*"
        color_word = "is back ONLINE"
    elif curr == "down":
        header = f"*SITE DOWN ALERT*"
        color_word = "went OFFLINE"
    else:
        header = f"*CONTENT CHANGED*"
        color_word = "is missing expected content"

    msg = (
        f"{header}\n"
        f"{'─'*30}\n"
        f"*Site:* {name}\n"
        f"*URL:* `{url}`\n"
        f"*Status:* {prev_emoji} → {emoji}  _{color_word}_\n"
        f"*Response:* {result['response_ms']} ms\n"
        f"*Time:* {result['checked_at']}\n"
    )

    if result.get("error"):
        msg += f"*Error:* `{result['error']}`\n"

    if curr != "up":
        msg += f"\nRechecking every {CHECK_INTERVAL}s until recovery..."

    await app.bot.send_message(
        chat_id=CHAT_ID,
        text=msg,
        parse_mode="Markdown",
        disable_web_page_preview=True
    )
    print(f"  Alert sent: {name} → {curr}")
                        

Code Breakdown

Imports

                            
import asyncio
from datetime import datetime
from checker import check_site
from storage import get_sites, update_site, is_paused
from config import CHECK_INTERVAL, CHAT_ID
                        
Import / Function / Constant Purpose
asyncio Runs multiple tasks concurrently without blocking
datetime Handles timestamps for last-checked time
check_site Sends HTTP request and returns site status
get_sites Loads all monitored URLs from sites.json
update_site Saves updated status back to sites.json
is_paused Checks if monitoring is paused by user
CHECK_INTERVAL How often to check (e.g., every 60 seconds)
CHAT_ID Your Telegram user ID to send alerts to

Function 1 — status_emoji()

                            
def status_emoji(status: str) -> str:
    return {"up": "✅", "down": "🔴", "keyword_missing": "⚠️", "unknown": "❓"}.get(status, "❓")
                        

What it does: Converts a plain text status into a visual emoji for Telegram messages.

How it works:

Status Text Returns
"up" ✅
"down" 🔴
"keyword_missing" ⚠️
"unknown" ❓
Anything else ❓

Function 2 — run_monitor() — The Main Loop

                            
async def run_monitor(app):
    print(f"🔍 Monitor started — checking every {CHECK_INTERVAL}s")

    while True:
        if not is_paused():
            sites = get_sites()

            for url, info in sites.items():
                ...

        await asyncio.sleep(CHECK_INTERVAL)
                        

What it does: Runs an infinite loop that never stops. Every {CHECK_INTERVAL} seconds, it wakes up and checks all your monitored sites — unless monitoring is paused.

Bot starts
    │
    ▼
while True (runs forever)
    │
    ├── is_paused? ──► YES → skip, sleep, repeat
    │
    └── NO → get all sites → check each one → update → alert if changed
                                    │
                                    ▼
                             sleep(CHECK_INTERVAL)
                                    │
                                    ▼
                              repeat forever ↺

Inside the Loop — Checking Each Site

                            
for url, info in sites.items():
    prev_status = info.get("status", "unknown")
    result = await check_site(url, info.get("keyword"))
    new_status = result["status"]
                        

What it does:

  • prev_status → Remembers the old status before checking (e.g., "up").
  • await check_site(...) → Actually visits the URL and gets back the result.
  • new_status → The fresh status just returned (e.g., "down").

await means: wait for the HTTP check to finish before moving on — but don't block other tasks while waiting.

Updating the Site Data

                            
update_site(url,
    status=new_status,
    last_checked=result["checked_at"],
    response_time=result["response_ms"],
    failures=info.get("failures", 0) + (1 if new_status != "up" else 0)
)
                        

What it does: Saves the latest check results back to sites.json.

Field What Gets Saved
status Latest status: "up", "down", etc.
last_checked Timestamp of this check
response_time How fast the site responded (ms)
failures Increments by 1 if site is NOT up

Failure counter logic:

failures + (1 if new_status != "up" else 0)

→ If up: keep the same count

→ If down/missing: add 1 to the failure count

The Smart Alert Trigger

                            
if new_status != prev_status:
    await send_alert(app, url, info["name"], prev_status, new_status, result)
                        

What is does: Only sends an alert when the status actually changed.

Why this is smart: Without this check, you'd get a Telegram message every 60 seconds saying "site is up" — which is annoying spam. This way you only get notified when something actually happens.

Scenario Alert Sent?
Was up → Still up ❌ No
Was up → Now down ✅ Yes
Was down → Now up ✅ Yes
Was up → Keyword missing ✅ Yes

Function 3 — send_alert() — The Telegram Notification

                            
async def send_alert(app, url, name, prev, curr, result):
                        

What is does: Builds a nicely formatted Telegram message and sends it to your chat when a status change is detected.

Step 1 — Decide the Header and Message

                            
if curr == "up":
    header = f"*SITE RECOVERED*"
    color_word = "is back ONLINE"
elif curr == "down":
    header = f"*SITE DOWN ALERT*"
    color_word = "went OFFLINE"
else:
    header = f"*CONTENT CHANGED*"
    color_word = "is missing expected content"
                        

Three possible alert types:

Current Status Alert Type
"up" Site Recovered
"down" Site Down Alert
"keyword_missing" Content Changed

Step 2 — Build the Message

                            
msg = (
    f"{header}\n"
    f"{'─'*30}\n"
    f"*Site:* {name}\n"
    f"*URL:* `{url}`\n"
    f"*Status:* {prev_emoji} → {emoji}  _{color_word}_\n"
    f"*Response:* {result['response_ms']} ms\n"
    f"*Time:* {result['checked_at']}\n"
)
                        

Step 3 — Add Error Info (If Any)

                            
if result.get("error"):
    msg += f"*Error:* `{result['error']}`\n"
                        

Only adds the error line if the check returned an error (e.g., "Connection timeout"). If the site is just slow or missing keyword, there may be no error.

Step 4 — Add Recheck Reminder (If Down)

                            
if curr != "up":
    msg += f"\nRechecking every {CHECK_INTERVAL}s until recovery..."
                        

Adds a helpful footer so you know the bot is still watching and will alert you again when it recovers.

Step 5 — Send the Message

                            
await app.bot.send_message(
    chat_id=CHAT_ID,
    text=msg,
    parse_mode="Markdown",
    disable_web_page_preview=True
)
print(f"  Alert sent: {name} → {curr}")
                        
Parameter Purpose
chat_id=CHAT_ID Sends to YOUR Telegram account only
text=msg The formatted message built above
parse_mode="Markdown" Enables bold, italic, code formatting
disable_web_page_preview=True Stops Telegram from generating a URL preview card
Build a Python Telegram Bot That Monitors Any Website and Alerts You Instantly

Step 8: Main Bot + All Commands [bot.py]

                            
import asyncio
import logging
from telegram import Update, BotCommand
from telegram.ext import (
    Application, CommandHandler, ContextTypes
)
from config import BOT_TOKEN, CHECK_INTERVAL
from storage import (add_site, remove_site, get_sites,
                     update_site, is_paused, set_paused)
from checker import check_site
from monitor import run_monitor, status_emoji

logging.basicConfig(
    format="%(asctime)s [%(levelname)s] %(message)s",
    level=logging.INFO
)


# ─── /start ──────────────────────────────────────────────────
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        " *Website Monitor Bot*\n"
        "━━━━━━━━━━━━━━━━━━━━━━━\n"
        "I'll watch your websites and alert you the *instant* anything changes.\n\n"
        " *Commands:*\n"
        "`/add   [keyword]` — Add a site\n"
        "`/remove ` — Remove a site\n"
        "`/list` — Show all monitored sites\n"
        "`/status ` — Check a site right now\n"
        "`/statusall` — Check all sites immediately\n"
        "`/pause` — Pause monitoring\n"
        "`/resume` — Resume monitoring\n"
        "`/help` — Show this menu",
        parse_mode="Markdown"
    )


# ─── /add ────────────────────────────────────────────────────
async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    args = ctx.args
    if len(args) < 2:
        await update.message.reply_text(
            " Usage: `/add   [keyword]`\n\n"
            "Example:\n"
            "`/add https://devspirehub.com DevspireHub`\n"
            "`/add https://mysite.com MyShop \"Add to Cart\"`",
            parse_mode="Markdown"
        )
        return

    url = args[0]
    if not url.startswith("http"):
        url = "https://" + url

    name = args[1]
    keyword = " ".join(args[2:]) if len(args) > 2 else None

    if len(get_sites()) >= 50:
        await update.message.reply_text(" Max 50 sites reached. Remove one first.")
        return

    add_site(url, name, keyword)

    msg = (
        f" *Site added to watchlist!*\n"
        f" *Name:* {name}\n"
        f" *URL:* `{url}`\n"
    )
    if keyword:
        msg += f" *Monitoring keyword:* `{keyword}`\n"
    msg += f"⏱ *Check interval:* every {CHECK_INTERVAL}s"

    await update.message.reply_text(msg, parse_mode="Markdown")


# ─── /remove ─────────────────────────────────────────────────
async def cmd_remove(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    if not ctx.args:
        await update.message.reply_text("Usage: `/remove `", parse_mode="Markdown")
        return

    url = ctx.args[^0]
    if remove_site(url):
        await update.message.reply_text(f" Removed: `{url}`", parse_mode="Markdown")
    else:
        await update.message.reply_text(" URL not found in watchlist.")


# ─── /list ───────────────────────────────────────────────────
async def cmd_list(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    sites = get_sites()
    if not sites:
        await update.message.reply_text(" No sites monitored yet. Use /add to start.")
        return

    paused_tag = "  ⏸ *PAUSED*" if is_paused() else ""
    msg = f" *Monitored Sites*{paused_tag}\n{'━'*30}\n"

    for url, info in sites.items():
        emoji = status_emoji(info.get("status", "unknown"))
        rt = f"{info['response_time']}ms" if info.get("response_time") else "N/A"
        kw = f" |  `{info['keyword']}`" if info.get("keyword") else ""
        msg += (
            f"{emoji} *{info['name']}*{kw}\n"
            f"   └ `{url}`\n"
            f"   └  {rt} |  {info.get('last_checked') or 'Never'}\n\n"
        )

    await update.message.reply_text(msg, parse_mode="Markdown",
                                    disable_web_page_preview=True)


# ─── /status ─────────────────────────────────────────────────
async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    if not ctx.args:
        await update.message.reply_text("Usage: `/status `", parse_mode="Markdown")
        return

    url = ctx.args[^0]
    sites = get_sites()
    keyword = sites.get(url, {}).get("keyword")

    await update.message.reply_text(f" Checking `{url}`...", parse_mode="Markdown")
    result = await check_site(url, keyword)

    emoji = status_emoji(result["status"])
    msg = (
        f"{emoji} *Live Check Result*\n"
        f"{'─'*28}\n"
        f"`{url}`\n"
        f" Status: *{result['status'].upper()}*\n"
        f" HTTP Code: `{result['status_code'] or 'N/A'}`\n"
        f" Response: `{result['response_ms']} ms`\n"
        f" Checked: {result['checked_at']}\n"
    )
    if result.get("error"):
        msg += f"Error: `{result['error']}`\n"
    if result.get("keyword") and result.get("keyword_found") is not None:
        found = "Found" if result["keyword_found"] else "Not Found"
        msg += f"Keyword: {found}\n"

    await update.message.reply_text(msg, parse_mode="Markdown",
                                    disable_web_page_preview=True)


# ─── /statusall ──────────────────────────────────────────────
async def cmd_statusall(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    sites = get_sites()
    if not sites:
        await update.message.reply_text("No sites to check.")
        return

    await update.message.reply_text(f"Checking {len(sites)} site(s)...")

    msg = "*Live Status — All Sites*\n" + "━" * 30 + "\n"
    for url, info in sites.items():
        result = await check_site(url, info.get("keyword"))
        emoji = status_emoji(result["status"])
        update_site(url, status=result["status"],
                    response_time=result["response_ms"],
                    last_checked=result["checked_at"])
        rt = f"{result['response_ms']}ms" if result["response_ms"] else "N/A"
        msg += f"{emoji} *{info['name']}* — `{rt}`\n   `{url}`\n\n"

    await update.message.reply_text(msg, parse_mode="Markdown",
                                    disable_web_page_preview=True)


# ─── /pause & /resume ────────────────────────────────────────
async def cmd_pause(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    set_paused(True)
    await update.message.reply_text("⏸ Monitoring *paused*. Use /resume to restart.",
                                    parse_mode="Markdown")


async def cmd_resume(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    set_paused(False)
    await update.message.reply_text("▶Monitoring *resumed*!", parse_mode="Markdown")


# ─── Boot ────────────────────────────────────────────────────
async def post_init(app: Application):
    await app.bot.set_my_commands([
        BotCommand("start",     "Show welcome & help"),
        BotCommand("add",       "Add a site to monitor"),
        BotCommand("remove",    "Remove a site"),
        BotCommand("list",      "List all monitored sites"),
        BotCommand("status",    "Live check one site"),
        BotCommand("statusall", "Live check all sites"),
        BotCommand("pause",     "Pause monitoring"),
        BotCommand("resume",    "Resume monitoring"),
    ])
    asyncio.create_task(run_monitor(app))
    print("Bot is live and monitoring!")


def main():
    app = (
        Application.builder()
        .token(BOT_TOKEN)
        .post_init(post_init)
        .build()
    )

    app.add_handler(CommandHandler("start",     cmd_start))
    app.add_handler(CommandHandler("help",      cmd_start))
    app.add_handler(CommandHandler("add",       cmd_add))
    app.add_handler(CommandHandler("remove",    cmd_remove))
    app.add_handler(CommandHandler("list",      cmd_list))
    app.add_handler(CommandHandler("status",    cmd_status))
    app.add_handler(CommandHandler("statusall", cmd_statusall))
    app.add_handler(CommandHandler("pause",     cmd_pause))
    app.add_handler(CommandHandler("resume",    cmd_resume))

    print("Starting Website Monitor Bot...")
    app.run_polling(drop_pending_updates=True)


if __name__ == "__main__":
    main()
                        

Code Breakdown

Imports

                            
import asyncio
import logging
from telegram import Update, BotCommand
from telegram.ext import Application, CommandHandler, ContextTypes
from config import BOT_TOKEN, CHECK_INTERVAL
from storage import (add_site, remove_site, get_sites,
                     update_site, is_paused, set_paused)
from checker import check_site
from monitor import run_monitor, status_emoji
                        
Import / Function / Constant What It Does
asyncio Runs async tasks (background monitor loop)
logging Prints log messages with timestamps
Update Represents an incoming Telegram message/command
BotCommand Defines slash commands shown in Telegram menu
Application The main bot app object from python-telegram-bot
CommandHandler Links a /command to a Python function
BOT_TOKEN Your secret bot token from .env
CHECK_INTERVAL How often (seconds) to check sites
storage functions Load, save, update site data
check_site Performs the actual HTTP request
run_monitor The background loop that checks sites
status_emoji Returns ✅🔴⚠️ based on site status

Logging Setup

                            
logging.basicConfig(
    format="%(asctime)s [%(levelname)s] %(message)s",
    level=logging.INFO
)
                        

What it does: Configures Python's built-in logger to print timestamped messages in your terminal.

2026-04-11 10:00:01 [INFO] Bot started polling...
2026-04-11 10:01:00 [INFO] Checking https://mysite.com

Why it's used: Helps you debug and monitor the bot's activity in real time without adding print() everywhere.

/start Command

                            
async def cmd_start(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text(
        " *Website Monitor Bot*\n..."
        parse_mode="Markdown"
    )
                        

What it does: Sends a welcome message with the full list of available commands when the user sends /start or /help.

/add Command

                            
async def cmd_add(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    args = ctx.args
    if len(args) < 2:
        await update.message.reply_text(" Usage: ...")
        return

    url = args[0]
    if not url.startswith("http"):
        url = "https://" + url

    name    = args[1]
    keyword = " ".join(args[2:]) if len(args) > 2 else None

    if len(get_sites()) >= 50:
        await update.message.reply_text(" Max 50 sites reached.")
        return

    add_site(url, name, keyword)
    await update.message.reply_text(msg, parse_mode="Markdown")
                        

What it does: Adds a new website to the monitoring watchlist.

User sends: /add https://mysite.com MySite "Add to Cart"
                   ↓           ↓         ↓
               args[0]      args[1]   args[2:]
                 url          name     keyword
Step What Happens
Check len(args) < 2 Show usage hint if URL or name is missing
Check if not url.startswith("http") Auto-adds https:// if user forgot it
" ".join(args[2:]) Joins multi-word keywords like "Add to Cart"
len(get_sites()) >= 50 Hard limit of 50 sites to prevent abuse
add_site(url, name, keyword) Saves the site to sites.json

/remove Command

                            
async def cmd_remove(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    if not ctx.args:
        await update.message.reply_text("Usage: `/remove `")
        return

    url = ctx.args[0]
    if remove_site(url):
        await update.message.reply_text(f" Removed: `{url}`")
    else:
        await update.message.reply_text(" URL not found in watchlist.")
                        

What it does: Removes a website from the watchlist by its URL.

  • remove_site(url) returns True if found and deleted, False if not found.
  • The bot replies with a different message depending on whether deletion succeeded.

/list Command

                            
async def cmd_list(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    sites = get_sites()
    if not sites:
        await update.message.reply_text(" No sites monitored yet.")
        return

    paused_tag = "  ⏸ *PAUSED*" if is_paused() else ""
    msg = f" *Monitored Sites*{paused_tag}\n{'━'*30}\n"

    for url, info in sites.items():
        emoji = status_emoji(info.get("status", "unknown"))
        rt    = f"{info['response_time']}ms" if info.get("response_time") else "N/A"
        kw    = f" |  `{info['keyword']}`" if info.get("keyword") else ""
        msg  += f"{emoji} *{info['name']}*{kw}\n└ `{url}`\n└ ⏱ {rt}\n\n"

    await update.message.reply_text(msg, parse_mode="Markdown",
                                    disable_web_page_preview=True)
                        

What it does: Shows all monitored websites in a formatted list with their status, response time, and last checked time.

Sample output in Telegram:

 Monitored Sites  ⏸ PAUSED
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 DevspireHub |  `login`
   └ https://devspirehub.com
   └  320ms |  2026-04-11 09:55:00

 MyShop
   └ https://myshop.com
   └  N/A |  Never

why disable_web_page_preview=True? Prevents Telegram from generating link previews for every URL in the list, which would clutter the message.

/status Command

                            
async def cmd_status(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    url     = ctx.args[0]
    keyword = sites.get(url, {}).get("keyword")

    await update.message.reply_text(f" Checking `{url}`...")
    result  = await check_site(url, keyword)

    # Build and send result message
    await update.message.reply_text(msg, parse_mode="Markdown")
                        

What it does: Instantly checks a single site right now (outside the regular interval) and reports the live result.

Flow:

 /status https://mysite.com
       ↓
Send " Checking..." message
       ↓
await check_site() → HTTP request
       ↓
Reply with live result:
   Status: UP
   HTTP Code: 200
   Response: 245ms
   Keyword:  Found

why " Checking..." first? Because check_site() takes time (network request). The immediate reply lets the user know the bot received the command and is working on it — prevents them thinking the bot is frozen.

/statusall Command

                            
async def cmd_statusall(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    sites = get_sites()
    await update.message.reply_text(f" Checking {len(sites)} site(s)...")

    for url, info in sites.items():
        result = await check_site(url, info.get("keyword"))
        update_site(url, status=result["status"], ...)
        msg += f"{emoji} *{info['name']}* — `{rt}`\n`{url}`\n\n"

    await update.message.reply_text(msg, parse_mode="Markdown")
                        

What it does: Checks ALL monitored sites immediately and sends one combined status report.

Bonus Bebavior: It also calls update_site() to save the fresh status back to sites.json — so /list will show updated data after /statusall runs.

/pause and /resume Commands

                            
async def cmd_pause(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    set_paused(True)
    await update.message.reply_text("⏸ Monitoring *paused*.")

async def cmd_resume(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
    set_paused(False)
    await update.message.reply_text("▶ Monitoring *resumed*!")
                        

What they do:

  • /pause → Sets "paused": true in sites.json → monitor.py skips all checks.
  • /resume → Sets "paused": false → monitoring continues normally

These are the simplest functions — just one storage call and one reply each.

post_init() Boot Hook

                            
async def post_init(app: Application):
    await app.bot.set_my_commands([
        BotCommand("start",     "Show welcome & help"),
        BotCommand("add",       "Add a site to monitor"),
        # ... more commands
    ])
    asyncio.create_task(run_monitor(app))
    print(" Bot is live and monitoring!")
                        

What it does: Runs automatically right after the bot connects to Telegram — before it starts listening for messages.

It does two critical things:

Task What It Does
set_my_commands([...]) Registers all slash commands in Telegram's UI menu (the / popup)
asyncio.create_task(run_monitor(app)) Launches the background monitoring loop as a parallel async task

Why asyncio.create_task()? It runs run_monitor() in the background without blocking the bot. The bot can still respond to commands while monitoring runs simultaneously.

post_init() fires on startup
     ├──► set_my_commands()  → Registers /add, /list etc. in Telegram UI
     └──► create_task()      → Launches background monitor loop
                                      ↓
                          Runs forever every 60s
                          (while bot handles commands in parallel)

main() — The Entry Point

                            
def main():
    app = (
        Application.builder()
        .token(BOT_TOKEN)
        .post_init(post_init)
        .build()
    )

    app.add_handler(CommandHandler("start",     cmd_start))
    app.add_handler(CommandHandler("help",      cmd_start))
    app.add_handler(CommandHandler("add",       cmd_add))
    app.add_handler(CommandHandler("remove",    cmd_remove))
    app.add_handler(CommandHandler("list",      cmd_list))
    app.add_handler(CommandHandler("status",    cmd_status))
    app.add_handler(CommandHandler("statusall", cmd_statusall))
    app.add_handler(CommandHandler("pause",     cmd_pause))
    app.add_handler(CommandHandler("resume",    cmd_resume))

    app.run_polling(drop_pending_updates=True)

if __name__ == "__main__":
    main()
                        

What it does: Builds the bot, registers all command handlers, and starts it.

Line by line:

                            
Application.builder().token(BOT_TOKEN).post_init(post_init).build()
                        
  • Application.builder() → Creates a bot builder object.
  • .token(BOT_TOKEN) → Authenticates with Telegram using your secret token.
  • .post_init(post_init) → Registers the boot hook to run after connection.
  • .build() → Finalizes and returns the bot app/
                            
app.add_handler(CommandHandler("add", cmd_add))
                        
  • Tells the bot: "When someone sends /add, call the cmd_add() function".
  • Note: Both "start" and "help" are mapped to the same cmd_start function.
                            
app.run_polling(drop_pending_updates=True)
                        
  • Starts the bot and continuously polls Telegram for new messages.
  • drop_pending_updates=True → Ignores any commands sent while the bot was offline (prevents old command flooding on restart).
                            
if __name__ == "__main__":
    main()
                        

Ensures main() only runs when you directly execute python bot.py, not when imported as a module.

Full Flow — How It All Works Together

python bot.py
     ↓
main() runs
     ↓
Application built with BOT_TOKEN
     ↓
post_init() fires:
  ├── Registers /commands in Telegram UI
  └── Starts run_monitor() as background task
            ↓
Bot starts polling Telegram
            │
  ┌─────────┴──────────┐
  │                    │
User sends /add     Background loop
  ↓                    ↓
cmd_add()          Every 60s:
  ↓                check_site()
add_site() saves     ↓
to sites.json      Status changed?
                     ↓
                   send_alert() → Telegram message to you 📱)

Running the Bot

                            
# Run locally
python bot.py

# Run on VPS in background (persistent)
nohup python bot.py > bot.log 2>&1 &

# Or with screen
screen -S monitor
python bot.py
# Ctrl+A, D to detach
                        

Quick Reference — All Commands

Command Function What It Does
/start or /help cmd_start Shows welcome + all commands
/add <url> <name> cmd_add Adds site to watchlist
/remove <url> cmd_remove Removes site from watchlist
/list cmd_list Shows all sites + status
/status <url> cmd_status Live checks one site now
/statusall cmd_statusall Live checks all sites now
/pause cmd_pause Pauses background monitoring
/resume cmd_resume Resumes background monitoring

Project Conclusion

Congratulations! You've just built a fully functional Python Telegram bot that monitors any website 24/7, detects downtime, tracks keyword changes, and delivers instant Telegram alerts — all from a single lightweight Python script.

This project is more than just a beginner exercise. It's a real-world automation tool you can deploy on a VPS today and use to protect your live websites, monitor competitors, or supercharge your bug bounty workflow.

Take It Further — What to Build Next

This bot is a strong foundation. Here are some powerful ways to extend it:

Next Step What to Add
📧 Email Alerts Add smtplib for dual Telegram + Email notifications
📊 Response Time Chart Use matplotlib to visualize uptime history
🔐 SSL Expiry Checker Alert when an SSL certificate is about to expire
☁️ Cloudflare Integration Monitor your Cloudflare Pages deployments by checking for your latest Git commit hash in the HTML
👥 Multi-User Support Let a team manage their own separate watchlists
📦 Docker Deployment Containerize the bot for easy cloud deployment

Why This Project Matters

Whether you're a web developer guarding your production site, a bug bounty hunter tracking target page changes, or a Python beginner building your first automation tool — this Python Telegram bot project delivers practical, real-world skills that directly apply to modern development, DevOps, and cybersecurity.

Found this tutorial helpful? Share it with your developer community and follow along for more real-world Python automation, website monitoring, and ethical hacking projects.

Other Projects

Space Shooter Game Python Pygame Tutorial

Shooter Game

This is a beginner-friendly guide for building a Space Shooter game with Python and Pygame, covering coding concepts and project structure.

Python Pygame
View Project →
ATM Management System Python Tutorial

ATM Management System

This Python application implements a multi-user ATM system with SQLite-backed persistence, featuring account management, financial transactions, and administrative controls.

Python SQLite
View Project →
Weather App HTML CSS JavaScript Tutorial

Weather App

Responsive weather app with real-time API data, feature comparison, and intuitive design for global city forecasts.

HTML CSS JavaScript
View Project →
Team Card App HTML CSS JavaScript Tutorial

Team Card App

Interactive team card application for cricket, featuring dynamic team selection, player filters, and customizable light/dark themes.

HTML CSS JavaScript
View Project →
Password Strength Checker C++ Tutorial

Password Strength Checker

Multi-Password Batch Strength Checker (C++), designed to check multiple passwords at once, show individual strength, and provide a summary report.

C++
View Project →
VPN Connectivity verification in C Tutorial

VPN Connectivity verification in C

Efficient C program to verify VPN status, routing, and DNS configurations through comprehensive public IP and network adapter analysis.

C
View Project →