Using a Screenshot API for Website Monitoring & Visual Regression Testing
Learn how to use a screenshot API for website monitoring and visual regression testing — scheduled captures, webhook delivery, image diffing, and automated visual alerts.
Unit tests and end-to-end tests catch logic bugs, but they cannot catch the category of bug that makes your customers say "something looks wrong." A misaligned nav bar, a broken hero image, a JavaScript error that silently removes half the page content — these are visual regressions, and a screenshot API is the most practical tool for catching them in production. This guide shows how to wire ScreenshotFreeAPI into a monitoring and visual regression workflow using webhooks, scheduled captures, and image diffing.
Pattern 1: Scheduled Screenshot via Webhook
The simplest monitoring pattern is a cron job that POSTs a screenshot request on a schedule, then stores the result. You compare each new screenshot to the previous one and alert if the visual difference exceeds a threshold. Here is a complete Node.js implementation using the native cron pattern:
import { CronJob } from 'cron';
import fs from 'node:fs/promises';
import path from 'node:path';
const API_KEY = process.env.SFA_API_KEY!;
const BASE_URL = 'https://api.screenshotfreeapi.com';
const MONITORED_URLS = [
'https://yoursite.com',
'https://yoursite.com/pricing',
'https://yoursite.com/blog',
];
async function captureAndStore(url: string): Promise<void> {
const res = await fetch(`${BASE_URL}/screenshots/web`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url,
fullPage: true,
format: 'png',
viewport: { width: 1440, height: 900 },
// Webhook receives the result; no polling needed
webhookUrl: 'https://yourapp.com/hooks/monitoring-complete',
// Tag the job so your webhook handler knows which URL it belongs to
metadata: { monitoredUrl: url, capturedAt: new Date().toISOString() },
}),
});
if (!res.ok) throw new Error(`Failed to queue capture for ${url}: ${res.status}`);
const { jobId } = await res.json() as { jobId: string };
console.log(`Queued monitoring capture for ${url}: ${jobId}`);
}
// Run every hour
const job = new CronJob('0 * * * *', async () => {
console.log('Running scheduled visual monitoring...');
await Promise.all(MONITORED_URLS.map(url => captureAndStore(url)));
});
job.start();Pattern 2: Webhook Handler and Image Diffing
When the job completes, ScreenshotFreeAPI delivers a POST to your webhookUrl. Your handler downloads the new screenshot, loads the previous screenshot from storage, runs a pixel-level diff, and alerts if the difference score exceeds your threshold. The example below uses pixelmatch, a lightweight diffing library with no native dependencies.
import express from 'express';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
import fs from 'node:fs/promises';
import path from 'node:path';
const app = express();
app.use(express.json());
const SCREENSHOT_DIR = '/var/data/monitoring-screenshots';
const DIFF_THRESHOLD = 0.05; // 5% pixel difference triggers an alert
interface WebhookPayload {
event: string;
jobId: string;
status: string;
screenshots: Array<{ url: string }>;
metadata?: { monitoredUrl?: string; capturedAt?: string };
}
app.post('/hooks/monitoring-complete', async (req, res) => {
const payload = req.body as WebhookPayload;
if (payload.event !== 'job.completed') return res.sendStatus(200);
const { monitoredUrl, capturedAt } = payload.metadata ?? {};
const screenshotUrl = payload.screenshots[0]?.url;
if (!screenshotUrl || !monitoredUrl) return res.sendStatus(400);
// Download the new screenshot
const imgRes = await fetch(screenshotUrl);
const newImageBuffer = Buffer.from(await imgRes.arrayBuffer());
// Build a slug from the URL for file storage
const slug = monitoredUrl.replace(/https?:///, '').replace(/[^a-z0-9]/gi, '_');
const newPath = path.join(SCREENSHOT_DIR, `${slug}_${Date.now()}.png`);
const previousPath = path.join(SCREENSHOT_DIR, `${slug}_latest.png`);
await fs.writeFile(newPath, newImageBuffer);
// Compare to the previous screenshot if it exists
try {
const previousBuffer = await fs.readFile(previousPath);
const img1 = PNG.sync.read(previousBuffer);
const img2 = PNG.sync.read(newImageBuffer);
if (img1.width === img2.width && img1.height === img2.height) {
const diff = new PNG({ width: img1.width, height: img1.height });
const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, img1.width, img1.height, { threshold: 0.1 });
const diffRatio = numDiffPixels / (img1.width * img1.height);
console.log(`Visual diff for ${monitoredUrl}: ${(diffRatio * 100).toFixed(2)}%`);
if (diffRatio > DIFF_THRESHOLD) {
console.error(`⚠ Visual regression detected on ${monitoredUrl}! Diff: ${(diffRatio * 100).toFixed(2)}%`);
// TODO: send Slack / PagerDuty / email alert here
}
}
} catch {
console.log(`No previous screenshot found for ${monitoredUrl} — storing baseline`);
}
// Update the "latest" reference
await fs.writeFile(previousPath, newImageBuffer);
res.sendStatus(200);
});
app.listen(3001);Pattern 3: App Store Visual Monitoring
The same monitoring pattern applies to app store screenshots. Use the /monitors/app endpoint with "notifyOnChange": true and ScreenshotFreeAPI handles the scheduling and diffing server-side, delivering a webhook only when the store page actually changes. This is more efficient than polling daily from your own cron job.
curl -X POST https://api.screenshotfreeapi.com/monitors/app \
-H "Authorization: Bearer sfa_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"appName": "YourApp",
"stores": ["ios", "android"],
"schedule": "daily",
"notifyOnChange": true,
"webhookUrl": "https://yourapp.com/hooks/app-monitor"
}'Note
The notifyOnChange mode performs server-side diffing before delivering the webhook. Your endpoint only fires when screenshots actually change — not on every scheduled capture. This dramatically reduces noise in high-frequency monitoring setups.
Tuning Your Diff Threshold
A 5% pixel difference threshold works well for most static marketing pages. Dynamic pages (news sites, dashboards with live data) will always have some visual churn from content updates — you may need to set a higher threshold (15–20%) or limit your capture to specific elements using the description AI targeting field rather than full-page screenshots. Targeting the navigation bar or hero section of a dynamic page isolates the structural elements you actually care about from the constantly changing content area.
Set up automated visual monitoring for your site in minutes — free tier includes 100 captures per month.
Start free — no credit card