Uptime monitoring tells you if a site is responding. But it doesn't tell you if the page looks right. A site can return HTTP 200 while showing a blank page, broken layout, missing images, or stale content. Visual monitoring with screenshots catches what HTTP status codes miss.

This guide shows you how to build an automated visual monitoring system that captures screenshots on a schedule, compares them with pixel-level diffing, and alerts you when something changes.

System Architecture

A visual monitoring system has four components:

  • Scheduler: Triggers captures at regular intervals (cron, BullMQ, cloud scheduler)
  • Capture engine: Takes screenshots of target URLs (browser or API)
  • Diff engine: Compares current screenshot with the baseline using pixel matching
  • Alert system: Notifies when visual diff exceeds threshold (Slack, email, webhook)

Step 1 — Screenshot Capture

Using SnapAPI (Recommended)

import fs from 'fs';

async function captureScreenshot(url, device = null) {
  const body = {
    url,
    format: 'png',
    full_page: true,
    block_ads: true,
  };
  if (device) body.device = device;

  const response = await fetch('https://api.snapapi.pics/v1/screenshot', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.SNAPAPI_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  if (!response.ok) throw new Error(`Capture failed: ${response.status}`);
  return Buffer.from(await response.arrayBuffer());
}

Step 2 — Pixel Diff Comparison

import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';

function compareScreenshots(baseline, current) {
  const img1 = PNG.sync.read(baseline);
  const img2 = PNG.sync.read(current);

  // Handle size differences
  const width = Math.max(img1.width, img2.width);
  const height = Math.max(img1.height, img2.height);

  // Pad images to same size if needed
  const padded1 = padImage(img1, width, height);
  const padded2 = padImage(img2, width, height);

  const diff = new PNG({ width, height });

  const mismatchedPixels = pixelmatch(
    padded1.data, padded2.data, diff.data,
    width, height,
    { threshold: 0.1 }
  );

  const totalPixels = width * height;
  const diffPercent = (mismatchedPixels / totalPixels) * 100;

  return {
    mismatchedPixels,
    totalPixels,
    diffPercent: diffPercent.toFixed(2),
    diffImage: PNG.sync.write(diff),
  };
}

function padImage(img, targetWidth, targetHeight) {
  if (img.width === targetWidth && img.height === targetHeight) return img;
  const padded = new PNG({ width: targetWidth, height: targetHeight, fill: true });
  PNG.bitblt(img, padded, 0, 0, img.width, img.height, 0, 0);
  return padded;
}

Step 3 — Scheduled Monitoring

import cron from 'node-cron';
import path from 'path';

const TARGETS = [
  { url: 'https://myapp.com', name: 'homepage', threshold: 1.0 },
  { url: 'https://myapp.com/pricing', name: 'pricing', threshold: 0.5 },
  { url: 'https://myapp.com/dashboard', name: 'dashboard', threshold: 2.0 },
];

const BASELINE_DIR = './baselines';
const DIFF_DIR = './diffs';

// Run every 30 minutes
cron.schedule('*/30 * * * *', async () => {
  console.log(`[${new Date().toISOString()}] Starting visual check...`);

  for (const target of TARGETS) {
    try {
      const current = await captureScreenshot(target.url);
      const baselinePath = path.join(BASELINE_DIR, `${target.name}.png`);

      if (!fs.existsSync(baselinePath)) {
        // First run — save as baseline
        fs.writeFileSync(baselinePath, current);
        console.log(`  [${target.name}] Baseline created`);
        continue;
      }

      const baseline = fs.readFileSync(baselinePath);
      const result = compareScreenshots(baseline, current);

      if (parseFloat(result.diffPercent) > target.threshold) {
        // Visual change detected!
        const diffPath = path.join(DIFF_DIR,
          `${target.name}-${Date.now()}.png`);
        fs.writeFileSync(diffPath, result.diffImage);

        await sendAlert({
          target: target.name,
          url: target.url,
          diffPercent: result.diffPercent,
          diffImage: diffPath,
        });

        console.log(`  [${target.name}] CHANGE: ${result.diffPercent}%`);
      } else {
        console.log(`  [${target.name}] OK (${result.diffPercent}% diff)`);
      }
    } catch (err) {
      console.error(`  [${target.name}] Error: ${err.message}`);
    }
  }
});

Step 4 — Alert System

async function sendAlert({ target, url, diffPercent, diffImage }) {
  // Slack webhook
  await fetch(process.env.SLACK_WEBHOOK, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `Visual change detected on *${target}*`,
      blocks: [{
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `*Visual Change Alert*\n` +
            `Page: ${url}\n` +
            `Change: ${diffPercent}%\n` +
            `Time: ${new Date().toISOString()}`,
        },
      }],
    }),
  });
}

Monitoring Use Cases

Competitor Price Monitoring

Screenshot competitor pricing pages daily and alert when they change. Combine with SnapAPI's /v1/extract endpoint to also pull structured pricing data for analysis.

Deployment Verification

After every deployment, capture screenshots across key pages and device sizes. Compare against pre-deployment baselines to catch visual regressions before users see them.

Content Change Detection

Monitor news sites, job boards, or government pages for content updates. Use /v1/scrape for text-level diffing alongside visual comparison.

Brand Compliance

Monitor franchise or partner websites to ensure they follow brand guidelines. Screenshot multiple URLs daily and flag visual deviations.

SLA Documentation

Capture screenshots of third-party services your app depends on. Build a visual archive that documents uptime and visual state over time.

Multi-Device Monitoring

const DEVICES = ['iphone-15-pro', 'ipad-pro-12.9', 'macbook-pro-16'];

async function monitorAcrossDevices(url, name) {
  const results = [];

  for (const device of DEVICES) {
    const screenshot = await captureScreenshot(url, device);
    const baselinePath = `./baselines/${name}-${device}.png`;

    if (fs.existsSync(baselinePath)) {
      const baseline = fs.readFileSync(baselinePath);
      const diff = compareScreenshots(baseline, screenshot);
      results.push({ device, ...diff });
    }

    fs.writeFileSync(baselinePath, screenshot);
  }

  return results;
}

Monitor Any Website Visually

Screenshot any URL across 30+ devices. No browser infrastructure needed. 200 free requests/month.

Get Your Free API Key