ScreenshotNeo

BlogHow-to

How to Get Screenshot Alerts When a Website Banner Changes

Monitor a banner on a schedule, compare screenshots, and get notified when it changes. Includes a runnable Playwright monitor and a screenshot API option.

By the ScreenshotNeo team4 October 202611 min read

To get screenshot alerts when a website banner changes, run a browser check on a schedule, capture the banner, compare the new image with the previous one, and send a notification when the difference passes a threshold. A check must run after the site changes before an alert can be sent, so choose an interval that fits how quickly you need to know.

The example below uses Playwright, Node.js, and a scheduled job. It captures a selected banner element, saves each capture, compares images, and sends an email through SMTP when the difference is large enough. If you do not control a server, a hosted visual-change monitor can handle the schedule and notifications. ScreenshotNeo can capture the page on demand; it is a screenshot API, not a scheduled change-monitoring service.

1. Choose what to monitor and how often

Start with the exact page URL where the banner appears. Decide whether to monitor a stable banner element or the whole page. A narrow element capture reduces noise from unrelated page changes, but whole-page monitoring is useful if the banner moves or its location varies.

Choice Use it when Tradeoff
Banner element The banner has a stable CSS selector and you only care about it. Selector changes or responsive layouts can make the target disappear.
Whole page The banner moves, appears conditionally, or you want broader context. Unrelated page changes can trigger alerts.
Short check interval Changes need to be noticed quickly. More checks use more of a monitor’s plan allowance or your own compute.
Long check interval Daily changes are sufficient or checks are costly. Detection can be delayed until the next check.

Use a precise condition, such as a visual difference in the banner, where the monitoring tool supports it. Broad “any change” rules can produce irrelevant alerts from timestamps, rotating content, ad slots, or layout shifts. Check the first few alerts and refine the region or threshold.

2. Build a scheduled screenshot monitor with Playwright

This self-hosted example runs once per invocation. Schedule it with cron, a CI scheduler, or another job runner at the interval you want. It captures a CSS-selected element, compares it to the preceding capture with pixelmatch, and sends an email through an SMTP server if the changed-pixel ratio exceeds a threshold.

Install dependencies

mkdir banner-monitor
cd banner-monitor
npm init -y
npm install playwright sharp pixelmatch pngjs nodemailer
npx playwright install chromium

Set the following environment variables for the target and your SMTP account. Use an app password or a dedicated mail credential where your provider supports it.

export PAGE_URL='https://example.com'
export BANNER_SELECTOR='.promo-banner'
export STATE_DIR='./state'
export CHANGE_THRESHOLD='0.02'
export SMTP_HOST='smtp.example.com'
export SMTP_PORT='587'
export SMTP_USER='alerts@example.com'
export SMTP_PASS='replace-with-secret'
export ALERT_FROM='alerts@example.com'
export ALERT_TO='you@example.com'

Create the monitor script

Save this as monitor.mjs. On the first successful run it stores a baseline and does not alert. Later runs compare the banner against that baseline. A screenshot of a failed or empty capture is not saved as the new baseline.

import fs from 'node:fs/promises';
import path from 'node:path';
import { chromium } from 'playwright';
import sharp from 'sharp';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import nodemailer from 'nodemailer';

const pageUrl = process.env.PAGE_URL;
const selector = process.env.BANNER_SELECTOR;
const stateDir = process.env.STATE_DIR ?? './state';
const threshold = Number(process.env.CHANGE_THRESHOLD ?? '0.02');

if (!pageUrl || !selector) {
  throw new Error('Set PAGE_URL and BANNER_SELECTOR.');
}
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
  throw new Error('CHANGE_THRESHOLD must be a number from 0 to 1.');
}

await fs.mkdir(stateDir, { recursive: true });
const currentPath = path.join(stateDir, 'current.png');
const previousPath = path.join(stateDir, 'previous.png');
const browser = await chromium.launch({ headless: true });

try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
  await page.goto(pageUrl, { waitUntil: 'domcontentloaded', timeout: 45000 });
  await page.locator(selector).waitFor({ state: 'visible', timeout: 15000 });
  const banner = page.locator(selector).first();
  await banner.screenshot({ path: currentPath, animations: 'disabled' });

  let previousExists = true;
  try { await fs.access(previousPath); } catch { previousExists = false; }

  if (!previousExists) {
    await fs.copyFile(currentPath, previousPath);
    console.log('Baseline saved; no alert on the first run.');
  } else {
    const [a, b] = await Promise.all([
      sharp(previousPath).png().toBuffer(),
      sharp(currentPath).png().toBuffer()
    ]);
    const [metaA, metaB] = await Promise.all([sharp(a).metadata(), sharp(b).metadata()]);
    const width = Math.max(metaA.width, metaB.width);
    const height = Math.max(metaA.height, metaB.height);
    const left = await sharp(a).resize(width, height, { fit: 'contain', background: '#ffffff' }).png().toBuffer();
    const right = await sharp(b).resize(width, height, { fit: 'contain', background: '#ffffff' }).png().toBuffer();
    const imgA = PNG.sync.read(left);
    const imgB = PNG.sync.read(right);
    const diff = new PNG({ width, height });
    const changedPixels = pixelmatch(imgA.data, imgB.data, diff.data, width, height, { threshold: 0.12 });
    const ratio = changedPixels / (width * height);
    console.log(`Changed pixel ratio: ${(ratio * 100).toFixed(2)}%`);

    if (ratio >= threshold) {
      const transport = nodemailer.createTransport({
        host: process.env.SMTP_HOST,
        port: Number(process.env.SMTP_PORT ?? '587'),
        secure: Number(process.env.SMTP_PORT ?? '587') === 465,
        auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS }
      });
      await transport.sendMail({
        from: process.env.ALERT_FROM,
        to: process.env.ALERT_TO,
        subject: `Website banner changed: ${new URL(pageUrl).hostname}`,
        text: `The monitored banner changed on ${pageUrl}. Changed pixel ratio: ${(ratio * 100).toFixed(2)}%. The current screenshot is attached.`,
        attachments: [{ filename: 'banner-current.png', path: currentPath }]
      });
      console.log('Change alert sent.');
      // Advance the baseline after a successful alert so subsequent checks
      // compare against the latest notified state instead of alerting repeatedly.
      await fs.copyFile(currentPath, previousPath);
    } else {
      console.log('Below threshold; baseline unchanged.');
    }
  }
} finally {
  await browser.close();
}

Run it once to create the baseline, then run it on a schedule:

node monitor.mjs

For example, a five-minute cron entry (adjust paths for your server) is:

*/5 * * * * cd /path/to/banner-monitor && /usr/bin/node monitor.mjs >> monitor.log 2>&1

Cron does not load your interactive shell configuration by default. Put environment variables in a protected service environment or a permissions-restricted environment file, and set the working directory explicitly.

Adjust the comparison

  • Threshold: CHANGE_THRESHOLD=0.02 means alert when at least 2% of normalized pixels differ. Lower values catch smaller changes but can be noisy. This is an example setting, not a universal recommendation.
  • Selector: replace .promo-banner with a stable selector from the page. If the site changes markup, update it.
  • Viewport: set a viewport representative of the visitors whose banner you care about. A responsive banner may differ on mobile and desktop; monitor each viewport separately if both matter.
  • Stability: if the banner animates or loads late, wait for a stable state before capture. Playwright can wait for a selector, a specific text, or a short delay; avoid arbitrary long waits unless the page needs them.
  • Noise: disable animations and avoid capturing regions containing clocks, rotating promotions, or personalized content where possible.

This script sends a new alert only when the current capture crosses the threshold against the last alerted baseline. To track every intermediate state, store each capture with a timestamp instead of overwriting the baseline. To avoid repeated alerts for a persistent change, retain the current behavior or add a separate “alerted state” record. If email delivery fails, the script exits with an error and the baseline is not advanced in the alert path.

3. Configure a hosted website change monitor

If you do not want to run a browser and scheduler, a hosted monitor can perform checks and notify you. Visualping documents adding a webpage, selecting the whole page or a specific area, describing relevant changes, choosing a check frequency, and reviewing changes through notifications or its dashboard. It supports visual, text, and code change detection according to its help center. Email is enabled by default; other notification options depend on plan. Its cloud monitors keep checking while your computer is off; local monitoring requires Chrome to remain open. See the Visualping setup guide and alert documentation.

Distill documents scheduled local or cloud monitors. Its monitors can compare content, apply optional conditions before sending notifications, and show visual, text, or source views in change history. Documented channels include email, SMS, mobile push, Discord, Slack, Microsoft Teams, and webhook-integrated apps; check current availability and plan terms directly. A local monitor needs the device and app or browser running, while a cloud monitor runs on Distill’s servers. See Distill’s monitor documentation and visual change history.

For either tool, select the banner when its location is stable, set a frequency appropriate to the urgency and plan limits, and start with a focused change condition. Review the first notifications and narrow the selection if unrelated page updates cause noise. A check has to take place after the change; alerts are not inherently instantaneous.

4. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It can capture a page on demand, but this one-call example does not schedule monitoring or send change alerts by itself. To build alerts with it, call the API from your own scheduled job, save the previous image, compare captures, and connect the job to your notification channel. The ScreenshotNeo docs cover API options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.

5. Troubleshooting alerts and screenshot differences

Symptom Likely cause Fix
No alert after a visible update The monitor has not run since the update, the alert channel is disabled or muted, or its configured condition was not met. Check the latest run time, notification settings, and change criteria. Visualping documents these as causes to check when an alert is missing.
The script says the selector was not found The selector is wrong, the banner is in an iframe, appears only after interaction, or has not loaded before the wait expires. Inspect the live DOM, use a stable selector, handle the relevant frame or interaction, and verify the page state manually.
Alerts arrive for unrelated changes The capture includes dynamic content, the monitored area is too broad, or the pixel threshold is too low. Select only the banner, disable animation, use a representative viewport, and raise the threshold gradually.
A real banner change is missed The threshold is too high, the banner was absent during the check, or the scheduled interval has not reached a post-change run. Lower the threshold cautiously, check selector visibility, and shorten the interval if the delay matters.
Images differ on every run Rotating creatives, personalized content, fonts, timestamps, animation, or asynchronous layout makes captures unstable. Wait for the intended state, disable animation, narrow the region, or monitor a textual condition if the tool supports it.
Screenshot is blank or incomplete The page is blocked, requires authentication or interaction, timed out, or content has not rendered. Confirm the page is accessible in the monitor’s browser context. Add necessary authentication or state only through secure configuration, and wait for the actual banner rather than relying only on navigation completion.
Email send fails SMTP host, port, credentials, sender permissions, or network access is invalid. Check the SMTP provider’s current settings and logs. Keep credentials out of source control and verify delivery before relying on alerts.
Scheduled runs overlap A page load or network request lasts longer than the schedule interval. Prevent concurrent executions with a lock, increase the interval, or set a job timeout longer than the expected capture duration.

6. Performance, reliability, and cost

  • Check delay: the maximum detection delay is roughly the chosen interval plus the time to load, capture, compare, and deliver the notification. A monitor cannot report a change it has not checked yet.
  • Reliability: keep the runner available, log each run and failure, and monitor the monitor’s own job status. A scheduled job that silently stops cannot alert. For hosted tools, understand whether checks run locally or in the cloud and verify the required device availability.
  • Baseline behavior: decide whether to compare against the last observed image or last alerted image. Advancing on every observation follows gradual changes but may hide small cumulative shifts; advancing only after alerting preserves the prior alert state but can repeatedly compare against an old image. The example advances after a successful alert.
  • Storage: keeping just a baseline and current capture uses little space. Keeping every image and diff helps audit changes but requires a retention policy and disk limits.
  • Cost: self-hosting uses your compute, storage, email provider, and any CI or scheduler allowance. Hosted monitor prices, check quotas, and notification availability can change; verify current plan details with the provider. ScreenshotNeo charges only for clean shots; its response includes X-Page-Verdict and X-Billed headers, and cache hits cost nothing.
  • Request rate: avoid checks more frequent than the site and your use case warrant. Each run loads the target page and may generate requests. Follow the site’s access rules and your monitoring provider’s limits.

FAQ

Can I get an alert the moment a banner changes?

Usually the monitor must perform a check after the change. Shorter intervals reduce the wait but do not make scheduled polling instantaneous.

Should I monitor the banner image or its text?

Use visual comparison when layout, color, or artwork matters. Use text conditions when the wording is the only meaningful change and the tool supports them. Some workflows can combine both.

Can I monitor a page that requires login?

Only if the monitoring browser can access the required authenticated state. Confirm support for login sessions and interactive flows with the chosen tool; access is not universal.

Why did the first run not send an alert?

The example uses the first successful screenshot as its baseline. There is no earlier image to compare until a later run.

Does ScreenshotNeo itself send banner-change alerts?

The API captures screenshots on request. Scheduling checks, comparing captures, and sending notifications require a separate job or monitoring workflow.