Build a Competitor Intelligence Dashboard with WebShot API
Learn how to automate competitor website monitoring and build a visual intelligence dashboard using WebShot API in Python — tracking pricing, feature changes, and UI updates.

Product managers spend hours every week manually visiting competitor websites, taking screenshots, and comparing them to last week's versions. Pricing pages change. Feature lists get updated. New integrations appear. A competitor quietly launches a feature you were planning to build — and you find out three weeks later when a prospect mentions it on a sales call.
This guide shows you how to build a fully automated competitor intelligence dashboard using the WebShot API — capturing, storing, and visually diffing competitor pages on a schedule, so your team always has current intelligence without the manual work.
What We'll Build
By the end of this guide, you'll have:
- Scheduled screenshot capture — competitors' key pages photographed daily
- Visual diff detection — automated detection of changes since last capture
- Change alerts — Slack notifications when significant changes are detected
- Historical archive — timestamped screenshot history for trend analysis
- Simple dashboard — HTML report showing latest state and detected changes
Prerequisites
pip install requests schedule Pillow slack-sdk jinja2You'll need a WebShot API key from apivult.com.
Step 1: Define Your Competitor Monitoring List
import requests
import os
from datetime import datetime
from pathlib import Path
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://apivult.com/webshot/v1"
# Define the pages you want to track
COMPETITOR_PAGES = [
{
"id": "competitor_a_pricing",
"company": "Competitor A",
"url": "https://competitor-a.com/pricing",
"description": "Pricing page",
"importance": "critical" # critical | high | medium
},
{
"id": "competitor_a_features",
"company": "Competitor A",
"url": "https://competitor-a.com/features",
"description": "Features overview",
"importance": "high"
},
{
"id": "competitor_b_pricing",
"company": "Competitor B",
"url": "https://competitor-b.com/pricing",
"description": "Pricing page",
"importance": "critical"
},
{
"id": "competitor_b_integrations",
"company": "Competitor B",
"url": "https://competitor-b.com/integrations",
"description": "Integrations list",
"importance": "high"
},
{
"id": "competitor_c_homepage",
"company": "Competitor C",
"url": "https://competitor-c.com",
"description": "Homepage hero",
"importance": "medium"
}
]Step 2: Capture Screenshots with WebShot API
def capture_screenshot(
url: str,
width: int = 1440,
full_page: bool = True,
wait_ms: int = 2000
) -> bytes:
"""
Capture a full-page screenshot via WebShot API.
Returns raw PNG bytes.
"""
response = requests.post(
f"{BASE_URL}/capture",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
json={
"url": url,
"viewport": {"width": width, "height": 900},
"full_page": full_page,
"format": "png",
"wait_ms": wait_ms,
"block_ads": True,
"block_trackers": True,
"quality": 90
},
timeout=60
)
if response.status_code != 200:
raise ValueError(f"WebShot error {response.status_code}: {response.text}")
return response.content
def save_screenshot(page_id: str, png_bytes: bytes, base_dir: str = "./screenshots") -> str:
"""Save screenshot with timestamp, return file path."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
page_dir = Path(base_dir) / page_id
page_dir.mkdir(parents=True, exist_ok=True)
filename = page_dir / f"{timestamp}.png"
filename.write_bytes(png_bytes)
return str(filename)
def get_latest_screenshot(page_id: str, base_dir: str = "./screenshots") -> str | None:
"""Return the most recent screenshot path for a page, or None."""
page_dir = Path(base_dir) / page_id
if not page_dir.exists():
return None
screenshots = sorted(page_dir.glob("*.png"), reverse=True)
return str(screenshots[0]) if screenshots else NoneStep 3: Detect Visual Changes with Image Diff
from PIL import Image, ImageChops
import math
import struct
def calculate_image_diff(path_a: str, path_b: str) -> dict:
"""
Compare two screenshots and calculate visual difference.
Returns diff percentage and a diff image.
"""
img_a = Image.open(path_a).convert("RGB")
img_b = Image.open(path_b).convert("RGB")
# Resize to same dimensions for comparison
if img_a.size != img_b.size:
# Use the older image's dimensions as reference
img_b = img_b.resize(img_a.size, Image.LANCZOS)
diff = ImageChops.difference(img_a, img_b)
# Calculate percentage of pixels that changed
pixels = list(diff.getdata())
total_pixels = len(pixels)
changed_pixels = sum(
1 for r, g, b in pixels
if math.sqrt(r**2 + g**2 + b**2) > 30 # threshold for "significant" change
)
diff_percentage = (changed_pixels / total_pixels) * 100
# Save diff image
diff_path = path_b.replace(".png", "_diff.png")
diff.save(diff_path)
return {
"diff_percentage": round(diff_percentage, 2),
"changed_pixels": changed_pixels,
"total_pixels": total_pixels,
"diff_image_path": diff_path,
"change_level": classify_change(diff_percentage)
}
def classify_change(diff_pct: float) -> str:
"""Classify the magnitude of change."""
if diff_pct > 20:
return "major" # Major redesign or new content block
elif diff_pct > 5:
return "significant" # Content changes, new features, pricing updates
elif diff_pct > 1:
return "minor" # Small copy or styling changes
else:
return "negligible"Step 4: Run a Full Monitoring Cycle
from concurrent.futures import ThreadPoolExecutor
def run_monitoring_cycle(pages: list[dict], base_dir: str = "./screenshots") -> list[dict]:
"""
Capture screenshots for all pages and detect changes from previous run.
"""
results = []
def monitor_one(page: dict) -> dict:
result = {
"id": page["id"],
"company": page["company"],
"url": page["url"],
"description": page["description"],
"importance": page["importance"],
"timestamp": datetime.now().isoformat(),
"status": "ok",
"change": None,
"new_screenshot": None,
"error": None
}
try:
# Get previous screenshot for comparison
previous = get_latest_screenshot(page["id"], base_dir)
# Capture new screenshot
png_bytes = capture_screenshot(page["url"])
new_path = save_screenshot(page["id"], png_bytes, base_dir)
result["new_screenshot"] = new_path
# Compare if we have a previous one
if previous and previous != new_path:
diff = calculate_image_diff(previous, new_path)
result["change"] = diff
print(f" {page['company']} / {page['description']}: "
f"{diff['diff_percentage']:.1f}% changed ({diff['change_level']})")
else:
print(f" {page['company']} / {page['description']}: baseline captured")
except Exception as e:
result["status"] = "error"
result["error"] = str(e)
print(f" ERROR {page['id']}: {e}")
return result
print(f"Running monitoring cycle for {len(pages)} pages...")
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(monitor_one, pages))
return resultsStep 5: Slack Notifications for Significant Changes
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
def send_change_alert(results: list[dict], slack_token: str, channel: str = "#competitor-intel"):
"""
Send a Slack alert for significant changes detected in the monitoring run.
"""
client = WebClient(token=slack_token)
significant = [
r for r in results
if r.get("change") and r["change"]["change_level"] in ("major", "significant")
]
if not significant:
return # No significant changes, no alert needed
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"🔍 Competitor Changes Detected ({len(significant)} pages)"
}
},
{
"type": "context",
"elements": [{"type": "mrkdwn", "text": f"Monitored {len(results)} pages • {datetime.now().strftime('%Y-%m-%d %H:%M')}"}]
}
]
for r in significant:
change = r["change"]
level_emoji = "🚨" if change["change_level"] == "major" else "⚠️"
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"{level_emoji} *{r['company']}* — {r['description']}\n"
f"Change: *{change['diff_percentage']}%* pixels modified ({change['change_level']})\n"
f"URL: {r['url']}"
)
}
})
try:
client.chat_postMessage(channel=channel, blocks=blocks)
print(f"Slack alert sent for {len(significant)} changes")
except SlackApiError as e:
print(f"Slack error: {e.response['error']}")Step 6: Generate an HTML Intelligence Report
from jinja2 import Template
REPORT_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Competitor Intelligence Report — {{ date }}</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; padding: 2rem; }
.page-card { border: 1px solid #e0e0e0; border-radius: 8px; margin: 1rem 0; padding: 1rem; }
.change-major { border-left: 4px solid #ef4444; }
.change-significant { border-left: 4px solid #f59e0b; }
.change-minor { border-left: 4px solid #3b82f6; }
.change-negligible { border-left: 4px solid #22c55e; }
img { max-width: 100%; border: 1px solid #ccc; margin: 0.5rem 0; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
</style>
</head>
<body>
<h1>Competitor Intelligence Report</h1>
<p>Generated: {{ date }} | Pages monitored: {{ total }}</p>
{% for r in results %}
<div class="page-card change-{{ r.change.change_level if r.change else 'negligible' }}">
<h3>{{ r.company }} — {{ r.description }}</h3>
<p><a href="{{ r.url }}">{{ r.url }}</a></p>
{% if r.change %}
<p><strong>Change detected: {{ r.change.diff_percentage }}% ({{ r.change.change_level }})</strong></p>
<div class="grid">
<div>
<p>Latest Screenshot:</p>
<img src="{{ r.new_screenshot }}" alt="Latest">
</div>
<div>
<p>Diff View:</p>
<img src="{{ r.change.diff_image_path }}" alt="Diff">
</div>
</div>
{% else %}
<p>No change detected or baseline run.</p>
<img src="{{ r.new_screenshot }}" alt="Current" style="max-height: 300px; object-fit: cover;">
{% endif %}
</div>
{% endfor %}
</body>
</html>
"""
def generate_report(results: list[dict], output_path: str = "./intelligence_report.html"):
template = Template(REPORT_TEMPLATE)
html = template.render(
date=datetime.now().strftime("%Y-%m-%d %H:%M"),
total=len(results),
results=results
)
Path(output_path).write_text(html, encoding="utf-8")
print(f"Report generated: {output_path}")Step 7: Schedule Daily Runs
import schedule
import time
def daily_monitoring_job():
"""The full daily monitoring pipeline."""
print(f"\n[{datetime.now().isoformat()}] Starting daily competitor monitoring...")
results = run_monitoring_cycle(COMPETITOR_PAGES)
generate_report(results)
slack_token = os.environ.get("SLACK_TOKEN")
if slack_token:
send_change_alert(results, slack_token)
changes = sum(1 for r in results if r.get("change") and r["change"]["change_level"] != "negligible")
print(f"Cycle complete: {changes}/{len(results)} pages with notable changes")
# Schedule daily at 8 AM
schedule.every().day.at("08:00").do(daily_monitoring_job)
if __name__ == "__main__":
# Run immediately on start
daily_monitoring_job()
# Then run on schedule
while True:
schedule.run_pending()
time.sleep(60)What Your Team Gets
After a week of running this system, your team has:
| Insight | How It Helps |
|---|---|
| Daily pricing page snapshots | Catch competitor price changes same day |
| Feature page diffs | Know about new features before customers tell you |
| Integration page tracking | See new partner integrations as they're announced |
| Historical archive | "When did they add that?" answered with visual proof |
| Automated alerts | PM team notified without manual checking |
Teams running competitive monitoring at this level report 3–4 weeks faster response time to competitor moves — the difference between being reactive and being proactive in product strategy.
Getting Started
WebShot API is available at apivult.com. The free tier supports up to 100 captures per month — enough to prototype your monitoring list before committing to a paid plan. Full-page screenshot, viewport control, and JavaScript execution are all included.
Start with your top 3 competitor pricing pages, run the baseline today, and by next week you'll have your first diff report. Most teams are surprised what they find in the first 30 days.
More Articles
5 Real-World Use Cases for Web Screenshot APIs in 2026
From competitor monitoring to compliance archiving — learn how developers are using web screenshot APIs to automate visual capture workflows at scale.
March 30, 2026
Build a Continuous Website Monitoring System with a Screenshot API
Learn how to automate visual website monitoring, detect page changes, and alert on regressions using the WebShot API and Python — no headless browser required.
March 31, 2026