Website Change Monitor avatar

Website Change Monitor

Pricing

from $0.60 / 1,000 page checkeds

Go to Apify Store
Website Change Monitor

Website Change Monitor

Watch any web page and get told exactly what changed since the last run: the added and removed lines, a change percentage, and a status of changed, unchanged or first_run. CSS selectors narrow the watched region and ignore clocks, carousels and ad slots.

Pricing

from $0.60 / 1,000 page checkeds

Rating

0.0

(0)

Developer

丂卩ㄖㄖҜㄚ

丂卩ㄖㄖҜㄚ

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 hours ago

Last modified

Share

Watch any web page and find out exactly what changed since the last time you looked. Every run returns the added lines, the removed lines, a change percentage and a plain status, so you get told what moved rather than just that something did.

🔍 What does Website Change Monitor do?

Give it a list of URLs. Each run fetches every page, pulls out the part you care about, and compares it against a fingerprint stored from the previous run. You get one row per page, with the actual differing text.

  • Watches as many pages as you like, up to 1,000 per run
  • Narrows the comparison to a CSS selector, so you can watch a price, a status line or a single table
  • Ignores the regions that change on their own: clocks, view counters, carousels, CSRF tokens and ad slots
  • Reports added and removed lines, not just a yes or no
  • Compares visible text by default, so a template rebuild or a CDN cache buster is not a change
  • Treats the first run as first_run, never as a change
  • Never fails a run because one page was down

The easiest way to try it is to press Try for free with the prefilled URL, run it twice, and watch the status move from first_run to unchanged.

📊 What data can I extract?

One row per watched page:

FieldDescription
urlThe page that was watched, as it was requested
statuschanged, unchanged, first_run or error
changedAtWhen the change was detected, ISO 8601. Null on any run that did not report a change
previousCheckedAtWhen the stored version this run was compared against was read, ISO 8601. Null on a first run
changePercentHow much of the watched content differs from the stored version, as a percentage of characters. Null on a first run, and on a page too large to keep in full
addedTextThe lines that appeared since the stored version. Null on a page too large to keep in full
removedTextThe lines that went away since the stored version. Null on a page too large to keep in full
contentLengthLength in characters of the watched content read on this run
previousContentLengthLength in characters of the stored version this run was compared against
contentHashSHA-256 of the content read on this run, which is what the next run compares against
previousContentHashSHA-256 of the stored version this run was compared against. Null on a first run
snapshotKeyKey in this run's key-value store holding the full watched content
httpStatusHTTP status code the page answered with on the final attempt
errorWhy the page could not be checked, in plain English

💡 Why monitor a website for changes?

Pricing pages. Watch a plan table and get the old figure and the new one in the same row. removedText holds "Starter plan costs 29 pounds a month", addedText holds "Starter plan costs 39 pounds a month", so you have the change and the evidence together.

Compliance and policy. Terms of service, privacy policies, licence conditions and regulator guidance all change quietly. Watch the page, keep the dataset, and you have a dated record of every revision.

Stock, availability and job listings. A product page going from out of stock to in stock is a text change on a known selector. So is a new role appearing on a careers page.

Documentation and API changes. Watch a changelog or an endpoint reference and get told which lines were added, rather than being asked to reread the page.

Tenders, grants and public notices. Public sector pages are usually server rendered, which is exactly what this Actor is fastest and most reliable on.

Feeding an agent. The output schema means an AI agent can chain this Actor and read the result. addedText is the part worth handing to a model, and it is already the diff rather than the whole page.

🚀 How do I use Website Change Monitor?

  1. Click Try for free.
  2. Add the pages you want to watch under urls. Paste them in, or point at a text file with one URL per line.
  3. Optional, set contentSelector to the part of the page that matters, for example main#content or .price-table.
  4. Optional, add ignoreSelectors for anything that changes on its own, such as .timestamp or #ad-slot-top.
  5. Click Start. The first run stores the baseline and reports first_run for every page.
  6. Put it on a schedule, hourly or daily. From the second run onwards you get real change reports.
  7. Optional, add a webhook or a Slack integration so a changed row reaches you without anyone opening the Console.

⬇️ Input

{
"urls": [
{ "url": "https://www.gov.uk/bank-holidays" },
{ "url": "https://example.com/pricing" }
],
"contentSelector": "main#content",
"ignoreSelectors": [".timestamp", "#cookie-banner"],
"changeThreshold": 0,
"mode": "text",
"storeSnapshot": true,
"maxRequestRetries": 3
}
InputTypeDefaultWhat it does
urlsarrayrequiredThe pages to watch. Individual URLs, or a text file with one URL per line
contentSelectorstringemptyCSS selector for the only region that counts. Empty watches the whole body
ignoreSelectorsarrayemptyCSS selectors stripped out before anything is compared
changeThresholdinteger0Percent of characters that must differ before it counts as a change
modestringtexttext compares what a reader sees, html compares the markup too
storeSnapshotbooleantrueKeep the full watched content in this run's key-value store
maxRequestRetriesinteger3Retries on a timeout or a server error before the page is reported as an error

⬆️ Output

Every row below is copied out of two real runs of this Actor on the Apify platform on 15 August 2026, runs eFh2x9ks27CVeg7LP and HHCoWy4HAGjsDH6zB, including the awkward parts. Three pages were watched twice, about thirty seconds apart, so the second run has something to compare against.

Table view

urlstatuschangePercentaddedTextremovedText
https://www.bbc.co.uk/newschanged2.0445 minutes ago45min, 50 minutes ago50min, 32 minutes ago32min, and 5 more lines44 minutes ago44min, 49 minutes ago49min, 31 minutes ago31min, and 5 more lines
https://www.gov.uk/bank-holidaysunchanged0
https://www.bbc.co.uk/newsfirst_run
https://httpbin.org/status/503error

JSON

A changed row. This is the BBC News front page watched twice about thirty seconds apart, with no ignore selectors set, so what moved is the relative timestamp on each story rather than the stories themselves. That is what watching a news homepage raw actually looks like, and it is the case ignoreSelectors exists for: add the selector on the timestamp element and the same watch comes back unchanged until a headline really moves.

{
"url": "https://www.bbc.co.uk/news",
"status": "changed",
"changedAt": "2026-08-15T16:54:21.827Z",
"previousCheckedAt": "2026-08-15T16:53:49.613Z",
"changePercent": 2.04,
"addedText": "45 minutes ago45min\n45 minutes ago45min\n45 minutes ago45min\n45 minutes ago45min\n45 minutes ago45min\n50 minutes ago50min\n32 minutes ago32min\n52 minutes ago52min",
"removedText": "44 minutes ago44min\n44 minutes ago44min\n44 minutes ago44min\n44 minutes ago44min\n44 minutes ago44min\n49 minutes ago49min\n31 minutes ago31min\n51 minutes ago51min",
"contentLength": 15566,
"previousContentLength": 15566,
"contentHash": "e2d4264293328a22d5f7373639c69abba98e1a72ee1c3e974c2ab33aa163736e",
"previousContentHash": "fdc59b152fc9cdf5164b5f9c7b49fc309458cc084b42cc1702ea251b0e1ef395",
"snapshotKey": "snapshot-defc1a587061144b0595eb74328f76d4",
"httpStatus": 200,
"error": null
}

An unchanged row, from the same run. The two hashes match, so nothing further was done with the page.

{
"url": "https://www.gov.uk/bank-holidays",
"status": "unchanged",
"changedAt": null,
"previousCheckedAt": "2026-08-15T16:53:48.733Z",
"changePercent": 0,
"addedText": null,
"removedText": null,
"contentLength": 17602,
"previousContentLength": 17602,
"contentHash": "6d4d4fa05c663b5b826b5293f1aa34f45ed371403ed916f526bf8887f494969a",
"previousContentHash": "6d4d4fa05c663b5b826b5293f1aa34f45ed371403ed916f526bf8887f494969a",
"snapshotKey": "snapshot-952500e7d20aa6a2936738ec326cda91",
"httpStatus": 200,
"error": null
}

A first_run row, from the run before it. Everything that describes a comparison is null, because there was nothing to compare against yet.

{
"url": "https://www.bbc.co.uk/news",
"status": "first_run",
"changedAt": null,
"previousCheckedAt": null,
"changePercent": null,
"addedText": null,
"removedText": null,
"contentLength": 15566,
"previousContentLength": null,
"contentHash": "fdc59b152fc9cdf5164b5f9c7b49fc309458cc084b42cc1702ea251b0e1ef395",
"previousContentHash": null,
"snapshotKey": "snapshot-defc1a587061144b0595eb74328f76d4",
"httpStatus": 200,
"error": null
}

An error row. The page was fetched four times, once plus the three retries, and answered 503 every time. The stored fingerprint for this page is left exactly as it was, so the outage cannot manufacture a change on the next run.

{
"url": "https://httpbin.org/status/503",
"status": "error",
"changedAt": null,
"previousCheckedAt": null,
"changePercent": null,
"addedText": null,
"removedText": null,
"contentLength": null,
"previousContentLength": null,
"contentHash": null,
"previousContentHash": null,
"snapshotKey": null,
"httpStatus": 503,
"error": "HTTP 503 SERVICE UNAVAILABLE after 4 attempt(s)"
}

The first run is not a change

This is the single most important behaviour in the Actor.

The first time a URL is seen under a given watch configuration there is no stored history, so there is nothing to compare against. That is reported as first_run, with changePercent, addedText, removedText, previousContentHash and previousCheckedAt all null. It is not a change, it never appears as one, and a webhook filtered on status == "changed" will not fire for it.

The same applies when you edit the watch configuration. The stored fingerprint is keyed on the URL together with the content selector, the ignore rules and the mode, because changing any of those changes what is being watched. Editing a selector gives you a fresh first_run rather than a change nobody made to the page.

How a page is compared

Before anything is hashed, the content is normalised, because otherwise every trivial reflow reads as an edit:

  • <script>, <style>, <noscript> and inline SVG are removed. Build hashes and inline state churn on every deploy and are not content.
  • HTML comments are removed, which is where server render timestamps usually sit.
  • Everything matching ignoreSelectors is removed.
  • Runs of whitespace collapse to a single space, non breaking and zero width spaces go, and blank lines are dropped.
  • In text mode only the visible text survives, broken into lines at every paragraph, heading, list item, table cell and other block level boundary. The line breaks come from the structure of the page rather than from whitespace in the source, so a minified site and a pretty printed one produce the same lines and the same diff. In html mode the markup is kept, one tag per line so the diff is readable.

What is left is hashed with SHA-256. If the hash matches the stored one the page is unchanged and no further work is done. If it differs, the lines are aligned to work out what was added and what was removed, and changePercent is the changed characters as a share of the larger of the two versions.

changeThreshold is applied last. At the default of 0 every difference is reported. Set it to 5 and a page whose content moved by less than five percent comes back unchanged, with the measured percentage still in changePercent so you can see what was suppressed.

A difference held back by the threshold is not forgotten. The stored copy is only replaced when a change is actually reported, so the next run still compares against the last version you were told about rather than against the version you were not. A page that moves two percent a run under a five percent threshold therefore keeps adding up: the second and third runs come back unchanged at two and four percent, and the fourth reports one six percent change carrying all of it. Without that, every run would quietly become the new baseline and a page could be rewritten from end to end, two percent at a time, without ever crossing the threshold.

Very large pages are watched by hash alone

The stored copy of a page is capped at two million characters, about two megabytes of plain text and far beyond any normal page. Past that the fingerprint is kept and the copy is not, because a state record that large is slower and more fragile than the thing it is protecting.

The consequences are worth stating plainly, because they are the one place the output is thinner than the field table suggests:

  • The page is still watched, and a change is still detected. The hash catches any edit at all.
  • changePercent, addedText and removedText come back null, because there is no previous copy to diff against.
  • changeThreshold cannot be applied, so any difference at all is reported as changed.

Setting contentSelector to the region you actually care about brings a page of that size back under the cap and gives you the line level diff again, which is usually the better answer anyway.

What happens when a page will not load

A page that times out, refuses the connection or answers with a server error is retried with a backoff, up to maxRequestRetries times. If it still will not answer, the page comes back as an error row with the reason in error and the code in httpStatus.

Two things follow from that, and both are deliberate:

  • The stored fingerprint is left alone. A site that was down for ten minutes does not read as a change when it comes back.
  • The run still succeeds. One unreachable host does not cost you the other forty nine pages, and it does not throw away the rows that were collected before it.

A page that ships an empty shell and builds itself with JavaScript in the browser is also reported as an error, with an explanation, rather than being silently recorded as an empty page that then "changes" the moment anything appears. This Actor is a fetch and a diff by design, which is what makes it cheap and fast. If you hit one of those pages, point contentSelector at a server rendered part of the site, or watch the JSON feed the page itself calls.

💰 How much does it cost?

This Actor is priced per page checked, on Apify's pay per event model, through a single event called page-checked. There is no monthly rental and no subscription, so a run you do not make costs nothing.

Your Apify planPrice per page checked
Free$0.006
Bronze$0.0051
Silver$0.0042
Gold$0.0036
Platinum$0.003
Diamond$0.0024

Apify also charges its standard Actor start event of $0.00005 once per run, so the run itself is effectively free and the pages are the bill.

Worked example. Ten pages on an hourly schedule is 240 checks a day. On the Bronze plan that is 240 x $0.0051 = $1.224, plus 24 start events at $0.00005 = $0.0012, so $1.23 a day or about $37 a month. On Diamond the same watch list is 240 x $0.0024 = $0.58 a day.

A page is charged once per run whatever the outcome. That includes unchanged, because the fetch and the comparison cost the same either way, and it includes an error row, because a page that answers with a server error is still fetched and retried. Fifty URLs on an hourly schedule is fifty charges an hour, whether nothing moved or everything did.

Set a charge limit on the run or the schedule to cap it, and use changeThreshold and ignoreSelectors to control the noise rather than the bill.

🔌 Integrations

Send results straight to Google Sheets, Slack, Airtable, Zapier, Make or your own endpoint with Apify integrations. A webhook on run success is the usual setup here: filter the dataset on status == "changed" and you have a change alert with the diff in it.

Scheduling is what turns this from a one off comparison into monitoring. Hourly, daily or weekly, whatever matches how often the page you care about actually moves.

🔗 Using Website Change Monitor with the Apify API

curl -X POST "https://api.apify.com/v2/acts/spookyweb~website-change-monitor/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"urls": [{"url": "https://example.com/pricing"}], "contentSelector": "main#content", "ignoreSelectors": [".timestamp"]}'

Or with the Apify client:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('spookyweb/website-change-monitor').call({
urls: [{ url: 'https://example.com/pricing' }],
contentSelector: 'main#content',
ignoreSelectors: ['.timestamp'],
changeThreshold: 0,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const changed = items.filter((row) => row.status === 'changed');

Full detail is in the Apify API reference, and every run is also callable from the Python and JavaScript clients.

❓ FAQ

Why does my first run say first_run instead of changed?

Because it is a first run. There was no stored copy of that page to compare against, so nothing changed. Run it a second time and you will get unchanged, then real change reports from there. This is on purpose: a monitor that calls the first sighting a change floods you with alerts on day one and teaches you to ignore it.

Where is the previous version kept, and for how long?

In a named key-value store on your own Apify account, called website-change-monitor-state, one record per watched page. It persists between runs and belongs to you. Each record holds the normalised content, its hash, its length and the time it was checked. Delete the store and every page starts again from first_run.

Add its CSS selector to ignoreSelectors. That region is removed from the page before anything is hashed, so it can never register as a change. Timestamps, view counters, session tokens, cookie banners and ad slots are the usual suspects. If you cannot name the element, raise changeThreshold a few percent instead.

Can I watch just one part of a page?

Yes, that is what contentSelector is for. Any CSS selector works, for example #price, main article or table.results tbody. If the selector matches nothing, the page is reported as an error rather than falling back to the whole body, because a silent fallback would look like the entire page had changed.

What is the difference between text mode and html mode?

text compares what a reader sees, so a template change, a class rename or a new inline script is not a change. html compares the markup as well, so it catches an attribute, a link target or a hidden field moving. Start with text. Switch to html only when you know you need it, because it is noisier on any site with a build step.

Does it work on pages that need JavaScript?

No, and it says so rather than pretending. This Actor is a plain HTTP fetch and a diff, which is what makes it fast and cheap. A page that ships an empty shell comes back as an error with an explanation. Most of the time the fix is to watch the JSON feed the page calls, or a server rendered part of the same site.

Does a small change I set a threshold to ignore build up over time?

Yes. The stored copy is only replaced when a change is reported, so a page drifting under the threshold is still measured against the last version you were told about. The drift accumulates and is reported as one change the run it crosses the threshold, rather than being reset to zero every run.

Will a site being down show up as a change?

No. A failed fetch is an error row, and the stored fingerprint is left exactly as it was. When the site comes back, it is compared against the last version that was actually read, so an outage cannot manufacture a change.

Can I run it on a schedule?

Yes, and that is the intended use. Set an Apify schedule at whatever interval matches the page, then add a webhook filtered on status == "changed" so you only hear from it when something actually moves.

How many pages can one run watch?

Up to 1,000. Past that the extras are skipped and the log says how many, rather than dropping them silently. Split a longer list across several runs or several schedules.

Reading a public web page is the same act a browser performs, and comparing two versions of something you were shown is not a separate right. This Actor only fetches pages you name, it never logs in, it never submits a form, and it stores only the content of the pages you asked it to watch.

What you should still check is the target site's terms of use and its robots.txt, whether the content is copyrighted before you republish any of it, and whether the page carries personal data, which brings the UK GDPR and the EU GDPR into play regardless of how public the page is. Apify's ethical scraping guide covers the wider picture, and none of this is legal advice.

👍 Your feedback

Found a page this handles badly, or want a field that is not here yet? Open an issue on the Actor's Issues tab. Requests that make the output more useful get built, and problems get fixed quickly.

🔎 You might also like

ActorWhat it does
Website Contact ScraperEmails, phone numbers, social profiles and named people from any company website
Wayback Machine ScraperHistorical snapshots of a page from the Internet Archive, for the changes that happened before you started watching
UK Case Law Search and MonitorCourt judgments and tribunal decisions from Find Case Law, with change monitoring built in
Company Email FinderContact addresses for a domain, and the address pattern behind them
Company Domain FinderCompany names in, official website addresses out, so you have URLs to watch in the first place
Company Data EnrichmentA full profile from one domain: name, address, phones, emails, socials and the technology the site runs on
Company Jobs ScraperEvery open role a company is advertising, read from its own hiring board rather than from its careers page