Build a Website Monitoring Bot with a Screenshot API and Python

2026-04-24 | Tags: [screenshot-api, monitoring, python, automation]

Want to know when a website changes? Not just whether it's up or down — but whether the content actually looks different? Screenshot-based monitoring catches visual changes that HTTP status checks miss: layout breaks, missing images, content swaps, and accidental deployments.

Here's how to build a simple visual monitoring bot in Python using a free screenshot API.

The Concept

  1. Take a screenshot of a URL at regular intervals
  2. Compare each new screenshot to the previous one
  3. Alert when the difference exceeds a threshold

This catches things that traditional monitoring misses: - CSS breaks that don't produce HTTP errors - A/B test variants accidentally shown to everyone - CDN serving stale or broken assets - Third-party widget failures (chat widgets, analytics banners) - Content management mistakes (wrong image, broken layout)

The Code

import requests
import hashlib
import os
import time
from datetime import datetime

SCREENSHOT_API = "https://hermesforge.dev/api/screenshot"
WATCH_DIR = "screenshots"
URLS = [
    "https://yoursite.com",
    "https://yoursite.com/pricing",
    "https://yoursite.com/docs",
]

def take_screenshot(url, width=1280):
    """Capture a screenshot and return the image bytes."""
    resp = requests.get(SCREENSHOT_API, params={
        "url": url,
        "width": width,
        "format": "png",
        "block_ads": "true",
    }, timeout=30)
    resp.raise_for_status()
    return resp.content

def get_image_hash(image_bytes):
    """Get a hash of the image for quick comparison."""
    return hashlib.sha256(image_bytes).hexdigest()

def monitor_url(url):
    """Take a screenshot and compare to the last known state."""
    slug = url.replace("https://", "").replace("/", "_").rstrip("_")
    current_path = os.path.join(WATCH_DIR, f"{slug}_current.png")
    previous_path = os.path.join(WATCH_DIR, f"{slug}_previous.png")
    hash_path = os.path.join(WATCH_DIR, f"{slug}.hash")

    # Take new screenshot
    image_bytes = take_screenshot(url)
    new_hash = get_image_hash(image_bytes)

    # Load previous hash
    old_hash = None
    if os.path.exists(hash_path):
        with open(hash_path) as f:
            old_hash = f.read().strip()

    # Compare
    changed = old_hash is not None and new_hash != old_hash

    # Save current state
    if os.path.exists(current_path):
        os.rename(current_path, previous_path)
    with open(current_path, "wb") as f:
        f.write(image_bytes)
    with open(hash_path, "w") as f:
        f.write(new_hash)

    return changed

def send_alert(url):
    """Send an alert when a change is detected."""
    timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
    print(f"[ALERT] {timestamp}: Visual change detected on {url}")
    # Add your preferred notification here:
    # - Slack webhook
    # - Email via SMTP
    # - Discord webhook
    # - PagerDuty event

def main():
    os.makedirs(WATCH_DIR, exist_ok=True)

    for url in URLS:
        try:
            changed = monitor_url(url)
            if changed:
                send_alert(url)
            else:
                print(f"[OK] No change: {url}")
        except Exception as e:
            print(f"[ERROR] {url}: {e}")

if __name__ == "__main__":
    main()

Running It on a Schedule

Save the script as monitor.py and add a cron job:

# Check every 15 minutes
*/15 * * * * cd /path/to/monitor && python3 monitor.py >> monitor.log 2>&1

Or every hour if you don't need frequent checks:

0 * * * * cd /path/to/monitor && python3 monitor.py >> monitor.log 2>&1

Adding Slack Notifications

Replace the send_alert function:

import json

SLACK_WEBHOOK = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

def send_alert(url):
    timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC")
    payload = {
        "text": f":warning: Visual change detected on <{url}|{url}> at {timestamp}"
    }
    requests.post(SLACK_WEBHOOK, json=payload)

Pixel-Level Comparison (Advanced)

The hash-based approach detects any change, even a single pixel. For more nuanced comparison, use Pillow to calculate the percentage of changed pixels:

from PIL import Image
import io

def compare_images(img1_bytes, img2_bytes, threshold=0.01):
    """Return True if images differ by more than threshold (0.01 = 1%)."""
    img1 = Image.open(io.BytesIO(img1_bytes)).convert("RGB")
    img2 = Image.open(io.BytesIO(img2_bytes)).convert("RGB")

    if img1.size != img2.size:
        return True  # Different dimensions = definitely changed

    pixels1 = list(img1.getdata())
    pixels2 = list(img2.getdata())
    total = len(pixels1)
    diff = sum(1 for p1, p2 in zip(pixels1, pixels2) if p1 != p2)

    return (diff / total) > threshold

This lets you ignore minor rendering differences (anti-aliasing, font smoothing) and only alert on significant visual changes.

What This Catches That Uptime Monitors Don't

Scenario HTTP Status Visual Monitor
Site is down Catches it Catches it
500 error page Catches it Catches it
CSS file 404 (broken layout) 200 OK Catches it
Wrong image deployed 200 OK Catches it
JavaScript error breaks rendering 200 OK Catches it
Third-party widget disappears 200 OK Catches it
Content accidentally deleted 200 OK Catches it

The HTTP status code is a necessary but not sufficient check. Visual monitoring adds the layer that catches everything the status code misses.

Cost

The screenshot API is free for basic usage — no API key required. For monitoring 3 URLs every 15 minutes, that's 288 requests per day. If you need more, get a free API key for higher rate limits.