Skip to content
PlaybookBy 13 min read

Website change monitoring with scheduled screenshots: one page, one month

A month of one page, as a calendar you can flip through: pick a day to see what changed and whether it was worth an alert. CurlShot takes the screenshots in this method. It does not schedule them, compare them or send alerts.

One pricing page, captured once a day

Illustrative month, not real data

MonTueWedThuFriSatSun

Changed, alert sentChanged, no alert

Day 5 compared with day 4

Noise

The promo strip rotated to another message

$9$29$79
Day 4, before
$9$29$79
Day 5, after
What happened
The line under the navigation swapped one offer text for another. The site does this on its own every few weeks.
Region, whole page
400 x 24 = 9,600 px0.94% of the screenshot
Pricing cards only
not seenOutside the element, so not seen.
Why it is noise
Nothing about the plans or their prices changed. An alert here trains you to ignore the next one.
At the current setting
False alarmAn alert goes out for something that does not matter.

Which days would have sent an alert?

One 1280 x 800 screenshot of the page.

SensitiveTolerant

5 alerts this month: 4 real changes, 1 false alarm. Nothing that mattered was missed.

What changed in the month, and what was worth an alert?

The illustrative month has 30 daily captures of one pricing page, each a 1280 x 800 screenshot of 1,024,000 pixels. On 25 days the capture is identical to the day before. On 5 days something changed, and each change stays on the page afterwards. It is a drawing made for this article, not data from a real site.

The five changes of the illustrative month: the day, what changed, the size of the changed region and the verdict
DayWhat changedChanged regionPricing cards onlyVerdict
Day 5The promo strip rotated to another messageNothing about the plans or their prices changed. An alert here trains you to ignore the next one.Changed region, whole page400 x 24 = 9,600 px0.94%Pricing cards onlynot seenVerdictNoise
Day 11The price of the middle plan went from $29 to $35This is the change the monitor exists for, and it is the smallest one of the month.Changed region, whole page96 x 40 = 3,840 px0.38%Pricing cards only1.15%VerdictWorth an alert
Day 16The money-back guarantee section was removedA promise to customers disappeared from the page. Someone should know, whether it was planned or not.Changed region, whole page1280 x 240 = 307,200 px30%Pricing cards onlynot seenVerdictWorth an alert
Day 22The layout broke: the third card dropped under the firstThe page still loads and an uptime check still passes. Only a look at the page shows that it is broken.Changed region, whole page320 x 320 + 1280 x 260 = 435,200 px42.5%Pricing cards onlysize changedVerdictWorth an alert
Day 27A sale banner was added above the navigationA sale changes what customers pay, even though no number in the pricing cards changed.Changed region, whole page1280 x 800 = 1,024,000 px100%Pricing cards onlynot seenVerdictWorth an alert

The size of a change here is the area of the region it touches, as a share of the capture. A real comparison counts the pixels that differ, which is a smaller number, because blank areas match. The element is the block of three pricing cards, #plans, 1040 x 320 pixels.

The smallest change is the one that matters most

The price change touches 3,840 pixels, 0.38% of the page. The rotated promo touches 9,600, 0.94%. Any threshold loose enough to ignore the promo also ignores the price, so no setting on the whole page gets both right. Watching the pricing cards alone does: the promo is outside the element, and the price is 1.15% of it.

Which changes send an alert at each sensitivity setting?

Watching the whole page, 1280 x 800
Alert when the changed region isAlerts sentFalse alarmsMissed changes
Any differenceAlerts sentday 5, day 11, day 16, day 22, day 27False alarmsday 5Missed changesnone
More than 0.5%Alerts sentday 5, day 16, day 22, day 27False alarmsday 5Missed changesday 11
More than 1%Alerts sentday 16, day 22, day 27False alarmsnoneMissed changesday 11
More than 35%Alerts sentday 22, day 27False alarmsnoneMissed changesday 11, day 16
More than 50%Alerts sentday 27False alarmsnoneMissed changesday 11, day 16, day 22
Watching the pricing cards only, 1040 x 320
Alert when the changed region isAlerts sentFalse alarmsMissed changes
Any differenceAlerts sentday 11, day 22False alarmsnoneMissed changesday 16, day 27
More than 0.5%Alerts sentday 11, day 22False alarmsnoneMissed changesday 16, day 27
More than 1%Alerts sentday 11, day 22False alarmsnoneMissed changesday 16, day 27
More than 35%Alerts sentday 22False alarmsnoneMissed changesday 11, day 16, day 27
More than 50%Alerts sentday 22False alarmsnoneMissed changesday 11, day 16, day 27

The element capture never sends a false alarm in this month, and it catches the price at every sensible setting. It cannot see the removed section or the sale banner. Watch a second element, or add a slower whole-page check, for what happens around the first one.

How does website change monitoring work? The loop

Website change monitoring is one loop with four stations: schedule, capture, compare, decide and notify. Every monitoring tool runs this loop, and each station needs something stored between runs.

  1. 1

    Schedule

    A cron job or a scheduled workflow starts a run at a fixed time.

    You store: The list of pages, each with its selector and capture options.

  2. 2

    Capture

    A browser opens the page with the same options as every other run and takes a fresh screenshot.

    You store: The image file, named with the time it was taken.

  3. 3

    Compare

    The new capture is compared with the baseline: the last capture you accepted as good.

    You store: The baseline, and how large the difference was.

  4. 4

    Decide and notify

    A difference over the threshold that is still there on the next run sends a message with both images. The new capture becomes the baseline.

    You store: A change seen once and waiting to be confirmed, and a log of alerts sent.

Then back to step 1, at the next scheduled time.

The first run has nothing to compare with, so its capture becomes the baseline. After an alert, the new capture becomes the baseline: otherwise the same change would send a message on every run.

Three ways to detect a change on a web page

A change on a web page can be detected by comparing screenshots, by comparing text, or by comparing one element. They fail in different ways, so the right one depends on what you are watching for.

  1. Compare the screenshots, pixel by pixel

    Two images of the whole page.

    Right when: Your own pages, where the question is "does it still look right?"

  2. - $29 per month+ $35 per month

    Compare the text or the HTML

    The words or the markup of the page, as a line-by-line diff.

    Right when: Terms, policies, notices and price lists, where the words are the content.

  3. Compare one element only

    A screenshot or the text of one element, picked with a CSS selector.

    Right when: One price, one stock badge, one table: the best signal for the least noise.

Three ways to detect a change on a web page: what each one catches and what it misses
MethodCatchesMisses or gets wrong
Compare the screenshots, pixel by pixelCatchesAnything a visitor can see: a broken layout, a missing image, a new banner, a changed colour.Misses or gets wrongNothing visible, which is the problem: ads, animations, dates and font loading all count as changes.
Compare the text or the HTMLCatchesPrices, wording, stock notices, dates of effect. The alert can quote the exact words that changed.Misses or gets wrongLayout, images, text inside images, and anything a script draws after loading unless a real browser renders the page first.
Compare one element onlyCatchesEvery change to the thing you care about, with almost nothing else in the way.Misses or gets wrongEverything outside the element. A renamed class makes the selector match nothing, so treat "element not found" as an alert.

How pixel comparison works, and what a colour threshold and a pixel budget do, is in the visual regression testing guide. How to pick a selector that survives a redesign is in CSS selectors for element screenshots.

How do you stop false alarms?

False alarms in page monitoring come from things that change on their own: banners, ads, animations, dates. Remove them before the capture, and make every capture as alike as you can. Work down this list; each line removes one source of noise.

  1. Capture one element, not the page

    Everything outside the element stops mattering: the promo strip, the footer date, a banner that pushes the page down.

    In CurlShot: selector

  2. Block cookie banners, ads and chat widgets

    A consent dialog, an ad slot or a chat bubble is different on every visit and covers what you are watching.

    In CurlShot: block_cookie_banners, block_ads, block_chats

  3. Fix the viewport and the device scale

    A page lays out differently at every width, and a different pixel density changes every pixel. Send the same numbers on every run.

    In CurlShot: viewport_width, viewport_height, device_scale_factor

  4. Freeze animations

    A carousel or a fade caught at a different instant looks like a change.

    In CurlShot: reduced_motion

  5. Hide dates, counters and rotating promos

    A "last updated" stamp, a visitor counter or a countdown changes on its own. Hide it before the capture.

    In CurlShot: hide_selectors, or styles to hide it and keep its space

  6. Wait until the element is there

    A capture taken before a script has filled in the price shows a placeholder, and the next run shows the price.

    In CurlShot: wait_for_selector

  7. Capture at the same time of day, in one time zone

    Pages that show "today", opening hours or a daily deal differ between morning and night.

    In CurlShot: time_zone, plus a fixed hour in the schedule

  8. Require a change to persist for two runs

    A half-loaded page, a failed image or a test variant shown to one visit in ten is gone on the next run. A real change is still there.

How often should you check a page for changes?

Check a page as rarely as still answers your question. The interval is the longest delay you accept between a change and your alert, and every check is a page load on someone else's server and one screenshot on your side.

Sensible check intervals for five kinds of page, with the number of checks per month for one page
SituationSensible intervalChecks a month, one pageWhy
Legal or terms pageSensible intervalOnce a weekChecks a month, one page4WhyThese pages change a few times a year, and a week of delay rarely matters.
Competitor pricing pageSensible intervalOnce a dayChecks a month, one page30WhyA price change matters within days, not minutes. A daily capture at a fixed hour also makes a tidy calendar.
News or regulator pageSensible intervalEvery 6 hoursChecks a month, one page120WhyNotices appear during working hours and you want them the same day.
Your own home page, as an "is it broken?" checkSensible intervalEvery hourChecks a month, one page720WhyAn hour is how long you accept a broken page going unnoticed. A plain uptime check is cheaper for "is it down?"; the screenshot answers "is it up but wrong?"
Stock or availability noticeSensible intervalEvery 30 minutes, while it mattersChecks a month, one page1,440WhyThe item can sell out again quickly. Stop the monitor once you have what you wanted.

The arithmetic

checks per month = pages x checks per day x 30

Once a week is 1/7 of a check per day. The intervals in the table are starting points reasoned from how fast each kind of page moves, not measurements.

  • 10 pages, once a day10 x 1 x 30 = 300
  • 50 pages, once a week50 x 1/7 x 30 = 214
  • 3 pages, every hour3 x 24 x 30 = 2,160
  • 1 page, every 30 minutes1 x 48 x 30 = 1,440

For scale: the free plan of CurlShot includes 100 screenshots a month, which is a daily check of 3 pages (100 / 30, rounded down).

A working setup: cron and a short Node.js script

A minimal page monitor is one scheduled command and one script of about 70 lines. The script below fetches a screenshot of one element, compares a hash of its bytes with the last good one, keeps both files, and prints or posts a message when they differ on two runs in a row.

1. The schedule

A crontab line is five time fields and then the command.[1]

The five time fields of a crontab line, in order
FieldAllowedHere
Minute0 to 590
Hour0 to 237
Day of the month1 to 31*
Month1 to 12*
Day of the week0 to 6, 0 is Sunday*
crontab -e
0 7 * * * cd $HOME/page-watch && . ./.env && node watch.mjs >> watch.log 2>&1

This line runs the script every day at 07:00, in the time zone of the machine. The file .env holds one line, export SCREENSHOT_ACCESS_KEY=..., so the key stays out of the crontab.

  • 0 7 * * *Every day at 07:00.
  • 0 7 * * 1Every Monday at 07:00.
  • 0 * * * *Every hour, on the hour.
  • 0,30 * * * *Every 30 minutes, written as a list so it works in any cron.
  • 0 0,6,12,18 * * *Every 6 hours.

POSIX defines * for all values, lists with commas and ranges with a hyphen. It does not define step values such as */15.[1] Many cron programs accept them, and GitHub Actions documents them.[2]

2. The script

The script uses only the Node.js standard library. fetch is built in from Node.js 18 and marked stable in Node.js 21.[3] The decision is one small function, decide, with no network and no disk in it.

watch.mjs
// watch.mjs: tell me when one element of one page changes.
// Node.js 18 or newer. No packages to install.
import { createHash } from 'node:crypto'
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
import { pathToFileURL } from 'node:url'

const API = 'https://curlshot.com/api/v1'
const DIR = 'captures'
const OPTIONS = {
  url: 'https://example.com/pricing',
  selector: '#plans',
  wait_for_selector: '#plans',
  format: 'png',
  viewport_width: '1280',
  viewport_height: '1024',
  device_scale_factor: '1',
  block_cookie_banners: 'true',
  block_ads: 'true',
  block_chats: 'true',
  reduced_motion: 'true',
  time_zone: 'UTC',
}

export const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')

// The whole decision, with no network and no disk in it.
// state.baseline is the hash of the last good capture,
// state.pending the hash of a change that was seen once.
export function decide(state, hash) {
  if (!state.baseline) return { event: 'first', state: { baseline: hash, pending: null } }
  if (hash === state.baseline) return { event: 'same', state: { baseline: state.baseline, pending: null } }
  if (hash !== state.pending) return { event: 'seen-once', state: { baseline: state.baseline, pending: hash } }
  return { event: 'changed', state: { baseline: hash, pending: null } }
}

async function capture() {
  const response = await fetch(API + '/screenshot?' + new URLSearchParams(OPTIONS), {
    headers: { 'X-Access-Key': process.env.SCREENSHOT_ACCESS_KEY ?? '' },
    signal: AbortSignal.timeout(90_000),
  })
  if (!response.ok) {
    throw new Error('Capture failed: HTTP ' + response.status + ' ' + (await response.text()).slice(0, 300))
  }
  return Buffer.from(await response.arrayBuffer())
}

async function notify(text) {
  console.log(text)
  const hook = process.env.ALERT_WEBHOOK_URL
  if (!hook) return
  await fetch(hook, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text }),
  })
}

async function main() {
  await mkdir(DIR, { recursive: true })
  const stateFile = DIR + '/state.json'
  const state = JSON.parse(await readFile(stateFile, 'utf8').catch(() => '{}'))

  const png = await capture()
  const result = decide(state, sha256(png))

  await writeFile(DIR + '/latest.png', png)
  if (result.event === 'changed') await rename(DIR + '/baseline.png', DIR + '/previous.png')
  if (result.event === 'first' || result.event === 'changed') await writeFile(DIR + '/baseline.png', png)
  await writeFile(stateFile, JSON.stringify(result.state, null, 2) + '\n')

  if (result.event === 'changed') {
    await notify(
      'Changed: ' + OPTIONS.url + ' ' + OPTIONS.selector +
        '. Compare ' + DIR + '/previous.png with ' + DIR + '/baseline.png.',
    )
  } else {
    const lines = {
      first: 'First capture saved as the baseline.',
      same: 'No change.',
      'seen-once': 'Different from the baseline. Waiting for the next run to confirm.',
    }
    console.log(lines[result.event])
  }
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
  main().catch((error) => {
    console.error(error.message)
    process.exitCode = 1
  })
}
The four outcomes of one run of the script
OutcomeWhenWhat the script does
firstWhenThere is no baseline yet.What the script doesSaves baseline.png and its hash. No alert.
sameWhenThe hash equals the baseline hash.What the script doesClears any pending change. No alert.
seen-onceWhenThe hash is new and was not seen on the run before.What the script doesRemembers it as pending. No alert yet.
changedWhenThe same new hash arrives on two runs in a row.What the script doesMoves the old baseline to previous.png, saves the new baseline.png, prints the alert and posts it to ALERT_WEBHOOK_URL if that is set.

A hash has no tolerance

One different pixel gives a different hash, so this comparison only works on a stable element saved as lossless PNG. It cannot tell a small change from a large one, and it can fire with no visible change if the browser or the image encoder behind the capture is updated. Run the script twice in a row before you rely on it: the second run must print "No change." For a tolerance, compare the pixels of previous.png and baseline.png with an image-diff library and a threshold, as visual regression tools do.

A failed capture exits with an error and leaves the saved state alone. The error selector_not_found means the element is gone or renamed, which deserves a look in its own right. A page that shows a different variant on every visit never repeats a hash, so it never confirms: hide the varying part or watch a steadier element.

3. The same check as a GitHub Actions workflow

.github/workflows/watch.yml
name: Watch a page
on:
  schedule:
    - cron: '17 7 * * *'
  workflow_dispatch:

permissions:
  contents: write

jobs:
  watch:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
      - run: node watch.mjs
        env:
          SCREENSHOT_ACCESS_KEY: ${{ secrets.SCREENSHOT_ACCESS_KEY }}
          ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }}
      - name: Keep the captures for the next run
        run: |
          git config user.name "page-watch"
          git config user.email "[email protected]"
          git add captures
          git diff --cached --quiet || git commit -m "Capture of $(date -u +%F)"
          git push

Put watch.mjs in a private repository and add the access key as a repository secret. The last step commits the captures folder, so the next run finds the baseline and the repository becomes a dated history of the element.

  • GitHub Actions reads the same five fields and adds / for steps, so */15 is valid there.[2]
  • The shortest interval GitHub runs a scheduled workflow at is once every 5 minutes.[2]
  • Times are in UTC unless you add a timezone key with an IANA name next to the cron line.[2]
  • A scheduled run can be delayed when GitHub is busy, and the start of every hour is a busy time. That is why the sample runs at minute 17.[2]
  • In a public repository, scheduled workflows are disabled automatically after 60 days without repository activity.[2]

Website change monitoring tools, and when to use one

Dedicated change-monitoring tools run the whole loop for you: the schedule, the comparison, the history and the alert. These five are in alphabetical order and described from their own sites. Two are open source.

Five website change monitoring tools: what kind of tool each is, where it runs, what it watches and how it alerts
ToolWhere it runsWhat it watchesAlerts
changedetection.io[4][5]Open source (Apache-2.0), with a paid hosted versionWhere it runsOn your own machine with Docker or pip, or in the vendor's cloud.What it watchesText changes with filters and CSS or XPath rules, chosen elements, restock and price changes on product pages. It can attach a screenshot to an alert.AlertsEmail, Slack, Discord, Telegram and many more, through the Apprise library.
Distill[7]Browser extension and hosted serviceWhere it runsLocal monitors in your browser (Chrome, Firefox, Edge, Opera) or cloud monitors that run around the clock.What it watchesThe parts of a page you select, plus PDFs and password-protected pages.AlertsBrowser notification, email, SMS and mobile apps.
urlwatch[9][10]Open source (BSD 3-Clause), command lineWhere it runsA Python program you add to your own crontab. Its documentation suggests checking no more often than every 30 minutes.What it watchesA URL, a page loaded in a headless browser, or the output of a shell command. The alert contains a unified diff of the text.AlertsEmail, the terminal and third-party services.
Visualping[6]Hosted serviceWhere it runsIn the vendor's cloud. Its site says getting started is free.What it watchesVisual changes (it compares the current and the prior screenshot), text changes and code changes.AlertsEmail, Slack, Microsoft Teams, webhooks and an API.
Wachete[8]Hosted serviceWhere it runsIn the vendor's cloud, around the clock.What it watchesA whole page or the part you care about, including pages that need a login.AlertsEmail, phone notification, Slack, Teams, Discord and Telegram.

Prices and plan limits change often and are left out on purpose. Each tool's own site has them.

A dedicated tool is the better choice when

  • You watch more than a handful of pages and want a list of them with a history, without keeping a server.
  • Someone who does not write code has to add pages and read the alerts.
  • You need a text diff in the alert: the exact words that changed, not two images.
  • You need alerts in several channels with rules, quiet hours or a digest.

Building the loop yourself fits when

  • You watch a few pages and already have a place to run a scheduled job.
  • The check is part of something larger: your deploy pipeline, a report, an archive of dated captures.
  • You want the images themselves, kept in your own storage, more than the alert.

What a scheduled screenshot cannot see, and good manners

A scheduled screenshot shows the page as a signed-out visitor from a data centre sees it. That is not always the page you see, and the site you watch pays for every visit.

A capture is a signed-out visitor from a data centre
The page is loaded without your cookies, from a server, often in another country. Prices shown per region, content behind a login and anything personalised differ from what you see at your desk.
A time zone is not a location
Setting the time zone changes the clock the page reads. It does not change where the request comes from, so a site that picks content by the visitor's address still sees the server.
Some pages refuse automated visits
A site can answer a server with a challenge page or an error. The monitor then sees the challenge, not the page: look at the first capture before you trust the alerts.
Check no more often than you need
Every check is a full page load on someone else's server. Read the site's terms of use, and pick the slowest interval that still answers your question.

The documentation of urlwatch, one of the tools above, suggests running it no more often than every 30 minutes.[9] Whether you may monitor someone else's site, and what you may do with the captures, depends on the site's terms and on the law where you are. This article is not legal advice.

What CurlShot does in a monitoring setup

CurlShot is the capture station of the loop and nothing more. It renders the page when your scheduler asks, and it has options that make two captures of an unchanged page come out alike.

It does

  • Renders a URL to a PNG, JPEG or WebP screenshot each time you ask.
  • Captures one element with selector, cropped to its edges.
  • Blocks cookie banners, ads and chat widgets, hides elements you name, and stops animations.
  • Renders the page fresh on every request unless you send cache=true.

It does not

  • Run on a schedule. Your cron job or workflow sends each request.
  • Keep a baseline or compare two screenshots. Your script does.
  • Send an alert when a page changes. webhook_url reports that an asynchronous render finished, which is a different thing.
  • Change where the request comes from. time_zone sets the clock, not the location.

The options that keep two captures alike

Options of the screenshot API that reduce noise in change monitoring
OptionValueWhat it keeps steady
selectorValuea CSS selectorWhat it keeps steadyCaptures the first element that matches, and nothing around it. No match returns the error selector_not_found with status 422, which is not counted.
wait_for_selectorValuea CSS selectorWhat it keeps steadyWaits until the element is visible, for prices that a script fills in late.
viewport_width, viewport_heightValue1280 and 1024 by defaultWhat it keeps steadyThe window the page is laid out in. Send them on every run so the layout never depends on a default.
device_scale_factorValue1 by defaultWhat it keeps steadyThe pixel density. At 2, the same element is twice as wide and every pixel differs.
block_cookie_banners, block_ads, block_chatsValuefalse by defaultWhat it keeps steadyRemoves the three things most likely to differ between two visits.
reduced_motionValuefalse by defaultWhat it keeps steadyAsks the site for no animation and stops CSS animations and transitions at the moment of capture.
hide_selectorsValueup to 50 selectorsWhat it keeps steadyHides a date stamp, a counter or a promo strip. A hidden element takes up no space.
time_zoneValuean IANA name such as UTCWhat it keeps steadyFixes the clock the page reads. There is no locale option: send an Accept-Language header with headers for the language.
formatValuepng by defaultWhat it keeps steadyPNG is lossless, so equal pixels give equal pixel data. JPEG and WebP are for viewing, not for comparing.

A cached response is not a fresh look

  • The response cache is off unless you send cache=true. Without it, every request opens the page again, which is what a monitor needs.
  • With cache=true, a repeat request inside cache_ttl (14,400 seconds, 4 hours, by default) returns the stored copy with the header X-Cache: HIT. That is the old look of the page, not a new one.
  • To force a fresh render while keeping the cache on, change cache_key, for example to the date of the run. Changing cache_ttl does not refresh anything.
  • One successful render uses one screenshot of the monthly allowance. A cache hit and a failed request use none.

The request the script sends

One element, with everything that moves switched off
curl "https://curlshot.com/api/v1/screenshot?url=https://example.com/pricing\
&selector=%23plans\
&wait_for_selector=%23plans\
&format=png\
&viewport_width=1280\
&viewport_height=1024\
&device_scale_factor=1\
&block_cookie_banners=true\
&block_ads=true\
&block_chats=true\
&reduced_motion=true\
&time_zone=UTC" \
  -H "X-Access-Key: YOUR_ACCESS_KEY" \
  --output plans.png

The result is a PNG of the element #plans alone. More: screenshot one element of a page, capturing an element, caching.

Change monitoring glossary

Baseline
The last capture you accepted as correct. Every new capture is compared with it.
Diff
The difference between two captures: an image with the changed pixels marked, or a list of added and removed lines of text.
False positive
An alert for a change that does not matter, such as a rotated promo or a new date stamp. Also called a false alarm.
Missed change
A change that mattered and sent no alert, because it was under the threshold or outside the watched element. Also called a false negative.
Threshold
How large a difference must be before it counts as a change, as a number of pixels or a share of the image.
Cron
The scheduler on Unix systems. A cron expression is five fields (minute, hour, day of the month, month, day of the week) that say when a command runs.
Element capture
A screenshot of one element of a page, picked with a CSS selector and cropped to its edges.
Debounce
Waiting for a change to still be there on the next run before acting on it, so that a one-off glitch sends no alert.

Questions people ask

How do I get notified when a website changes?

Capture the page on a schedule, compare each capture with the previous good one, and send yourself a message when they differ. You can build that with a cron job, a screenshot request and a short script, or use a dedicated service such as changedetection.io, Distill, urlwatch, Visualping or Wachete, which do the scheduling, the comparison and the alert for you.

Does CurlShot monitor websites for changes?

No. CurlShot takes a screenshot each time you ask for one. It has no scheduler, keeps no baseline, does not compare two screenshots and sends no change alerts. In a monitoring setup it is the capture step: your cron job sends the request and your script does the comparison.

How often should I check a web page for changes?

As rarely as still answers your question. Once a week suits a terms page, once a day a pricing page, every 6 hours a news or regulator page, every hour your own home page, and every 30 minutes a stock notice while it matters. Multiply pages by checks per day by 30 to get the checks per month.

Why does my page monitor send false alarms?

Because something on the page changes on its own: a cookie banner, an ad, a chat bubble, an animation, a date stamp or a rotating promo. Capture one element instead of the page, block banners, ads and chats, fix the viewport size, stop animations, hide dates and counters, and only alert when a change is still there on the next run.

Is it better to compare screenshots or the text of a page?

Compare text when the words are the content: prices, terms, notices. A text diff quotes exactly what changed and ignores layout. Compare screenshots when the look is the content: a broken layout or a missing image never shows up in a text diff. Comparing one element, as an image or as text, gives the least noise of all.

Can I compare two screenshots by hashing the files?

Only as a strict equal-or-not test. A hash changes when a single pixel changes, so it suits a stable element saved as lossless PNG and nothing else. It cannot tell a small difference from a large one. For a tolerance, decode both images and count the pixels that differ, as visual regression tools do.

Is it legal to monitor a competitor's website?

It depends on the site's terms of use, on what you capture and on the law where you and the site are. Loading a public page at a modest rate is ordinary web traffic, but terms can forbid automated access. This is not legal advice: ask a lawyer when the answer matters to your business.

Screenshot any page in one step

Paste an address into CurlShot and download the screenshot. A free account gives you 100 screenshots every month. No card needed.