Website Change Monitor: Detect Page & Text Changes avatar

Website Change Monitor: Detect Page & Text Changes

Pricing

from $1.00 / 1,000 url checkeds

Go to Apify Store
Website Change Monitor: Detect Page & Text Changes

Website Change Monitor: Detect Page & Text Changes

Checks a list of URLs and returns only the pages whose content changed since the last run: unified diff, added/removed lines, SHA-256 hashes. Optional CSS selector, keyword triggers and ignore regexes. Plain HTTP, respects robots.txt. $0.001 per URL checked, $0.005 per change.

Pricing

from $1.00 / 1,000 url checkeds

Rating

0.0

(0)

Developer

Open Data Tools

Open Data Tools

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

an hour ago

Last modified

Share

Website Change Monitor

This tool checks a list of web pages and tells you only about the ones that changed since the last check. For each changed page you get a diff (a before-and-after list of the lines that were added or removed), so you can see what changed without reading the whole page. Set it to run on a schedule and you have a simple change alert for prices, stock, policies, job lists or notices.

Who it's for

  • Businesses keeping an eye on competitor pricing or product pages.
  • Shoppers and resellers waiting for "In stock" to appear or "Sold out" to disappear.
  • Compliance and legal teams watching terms, policy and regulation pages.
  • Anyone who follows careers pages, documentation, release pages or government notices.

Why use it

  • Only real changes: it compares the visible text after normalising it. Whitespace, scripts, styles and HTML comments are ignored, so a change to the page code alone does not trigger an alert.
  • Watch part of a page: add a CSS selector, such as #pricing, main or .job-list, for all pages or for each page separately.
  • Keyword triggers: only alert when, say, "In stock" appears or "Sold out" disappears.
  • Ignore patterns: strip dates, clocks, counters and other text that changes on every visit, using regular expressions.
  • Separate watch lists: each monitor name keeps its own saved snapshots in a store in your own Apify account.
  • Run summary: each run records how many pages were checked, changed, unchanged, failed or blocked.
  • Fits into your tools: works with Apify schedules, webhooks and integrations (Slack, email, Zapier, Make, Google Sheets) to send changes wherever you want them.

How to use it

  1. Open the Actor in Apify Console and add the pages you want to watch under Pages to watch.
  2. Optionally add a CSS selector (all pages), Keyword triggers or Ignore patterns.
  3. Give the watch list a Monitor name, for example competitor-pricing. Keep the same name for every run of this list.
  4. Click Start. The first run records a baseline (the starting version of each page) and reports no changes, unless you tick Output baseline on first run.
  5. Open Schedules in Apify Console and run it hourly or daily with the same input. Every later run outputs only the pages that changed. Open the Output tab to see them, and export them as JSON, CSV or Excel.
  6. Optionally add a webhook or integration (Slack, email, Google Sheets, Zapier or Make) that fires when the run finishes, so you get an alert.

Pricing

You pay $0.001 for each page checked and $0.005 for each change found. A page counts as checked once it has been fetched and compared. Pages that are blocked, fail or are not checked are free, and so are baseline rows. You can set a maximum total charge for a run, and the run stops cleanly when it is reached.

Example: checking 100 pages once a day costs $0.10 a day for the checks. If 5 of those pages change, add $0.025, so about $0.125 for the day, or about $3.75 over 30 days.

API and reference

This section is for developers and AI agents. It lists every input and output field, a sample record and code examples.

Summary

Fetches a list of URLs over plain HTTP, normalises the content, and compares it with the snapshot saved on the previous run. Only pages whose content changed are written to the dataset, each with a unified diff, added/removed lines and SHA-256 hashes of both versions. You can limit the comparison to a CSS selector, report only when keywords appear or disappear, and strip dynamic text with regular expressions. Snapshots persist between runs in a named key-value store in your account, so the Actor is meant to run on a schedule. robots.txt is respected. No browser, proxies or anti-bot workarounds are used.

Input

FieldTypeDefaultDescription
startUrlsarrayrequiredURLs to check. Items can be strings, {"url": "..."}, {"url": "...", "selector": "#main"} or {"url": "...", "userData": {"selector": "..."}}.
monitorNamestringdefaultWatch-list name. Snapshots go to the key-value store website-change-monitor-<name>. Keep it the same across scheduled runs.
selectorstringnoneCSS selector applied to every URL. A per-URL selector overrides it.
modetext, htmltexttext compares visible text (whitespace, scripts, styles and comments ignored). html compares markup.
keywordsstring[][]If set, a change is reported only when a keyword's count changes (case-insensitive).
keywordTriggerany, appears, disappearsanyWhich direction of keyword count change counts.
ignorePatternsstring[][]Case-insensitive regexes removed before comparing. Invalid regexes are matched as literal text.
outputBaselinebooleanfalseAlso write a changeType: "baseline" row on the first check of a page. Not charged as a change.
resetBaselinebooleanfalseIgnore saved snapshots and record a new baseline.
maxUrlsinteger1001 to 1000. Only the first N URLs are checked.
concurrencyinteger51 to 20. Requests to the same host are always sequential.
perHostDelaySecsnumber10.5 to 30. Gap between requests to one host. A longer robots.txt Crawl-delay is used, up to 10 s.
maxDiffCharsinteger4000500 to 50000. Max length of diff.

The first run for a given monitorName records baselines and returns no rows, unless outputBaseline is true.

Output

One dataset row per changed page (plus baseline rows if outputBaseline is on).

FieldTypeDescription
urlstringWatched URL
finalUrlstringURL after redirects
selectorstring or nullCSS selector compared, null for the whole page
monitorNamestringWatch-list name
pageTitlestring or nullHTML <title>
httpStatusintegerHTTP status
changeTypestringchanged or baseline
changedAtstring or nullISO 8601 UTC time the change was detected
checkedAtstringISO 8601 UTC time of this check
previousCheckedAtstring or nullTime of the snapshot compared against
previousHash / currentHashstringSHA-256 of the normalised content
diffstringUnified diff, capped at maxDiffChars
diffTruncatedbooleantrue if diff was cut
addedLines / removedLinesstring[]Changed lines, up to 200 each
addedCount / removedCountintegerNumber of lines added / removed
keywordsMatchedstring[]Keywords that triggered (for baseline rows: keywords present)
keywordEventsobject[]{keyword, event, before, after}, where event is appeared or disappeared
lineCountintegerLines in the normalised content

Sample record (from a test run; URL replaced):

{
"url": "https://example.com/product",
"finalUrl": "https://example.com/product",
"selector": "main",
"monitorName": "cloud-test",
"pageTitle": null,
"httpStatus": 200,
"currentHash": "617a923d3a91fde26988f373269559be8ae081907d8740ada892310ccbff0aee",
"changeType": "changed",
"changedAt": "2026-10-20T12:20:29+00:00",
"checkedAt": "2026-10-20T12:20:29+00:00",
"previousCheckedAt": "2026-10-20T12:20:17+00:00",
"previousHash": "38bc39eaa56895cd748e771d74971e9062017ca7915fb47232e0f24a9006ed37",
"diff": "--- previous\n+++ current\n@@ -4 +4,2 @@\n New line added\n+SOLD OUT",
"diffTruncated": false,
"addedLines": ["SOLD OUT"],
"removedLines": [],
"addedCount": 1,
"removedCount": 0,
"keywordsMatched": ["sold out"],
"keywordEvents": [{"keyword": "sold out", "event": "appeared", "before": 0, "after": 1}],
"lineCount": 5
}

The run's default key-value store gets a SUMMARY record with counts (checked, changed, unchanged, baseline, changedNoKeywordTrigger, failed, blocked, notChecked), stoppedEarly, and a per-URL status list.

Per-URL statuses that produce no dataset row:

  • blocked: disallowed by robots.txt, HTTP 401/403/407/429/451, or an anti-bot challenge page.
  • failed: network error, invalid URL, parse error, the selector matched nothing, or the page suddenly came back with no text (e.g. a maintenance page). The previous snapshot is kept.
  • notChecked: skipped because the run was close to its timeout or the max total charge was reached. The next run with the same input starts from these URLs.

Pricing details

Pay per event: $0.001 per URL checked (url-checked) and $0.005 per change detected (change-detected). A URL is charged once it has been fetched and compared, whether the result is baseline, unchanged or changed. Blocked, failed and not-checked URLs are not charged, and neither are baseline rows. The run stops cleanly when the max total charge is reached.

Usage

Apify API (synchronous run, returns dataset items):

curl -X POST "https://api.apify.com/v2/acts/opendata-tools~website-change-monitor/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"startUrls": [
{"url": "https://www.python.org/downloads/", "selector": ".download-list-widget"},
{"url": "https://www.iana.org/domains/reserved"}
],
"monitorName": "release-pages",
"ignorePatterns": ["\\d{1,2}:\\d{2}(:\\d{2})?"]
}'

Python (pip install apify-client):

import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("opendata-tools/website-change-monitor").call(run_input={
"startUrls": [
{"url": "https://www.python.org/downloads/", "selector": ".download-list-widget"},
{"url": "https://www.iana.org/domains/reserved"},
],
"monitorName": "release-pages",
"keywords": ["Python 3.15"],
"keywordTrigger": "appears",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["url"], item["addedCount"], item["removedCount"])

JavaScript (npm install apify-client):

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('opendata-tools/website-change-monitor').call({
startUrls: [
{ url: 'https://www.python.org/downloads/', selector: '.download-list-widget' },
{ url: 'https://www.iana.org/domains/reserved' },
],
monitorName: 'release-pages',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) console.log(item.url, item.diff);

For recurring checks, create an Apify schedule with a fixed input and attach a webhook on ACTOR.RUN.SUCCEEDED to receive results.

Limits

  • Plain HTTP only. Pages that render content with JavaScript may compare as empty or unchanged. Use a selector on server-rendered content, or watch the site's RSS/JSON endpoint.
  • No logins, proxies or captcha solving. Blocked pages are reported, not retried.
  • User-Agent token: WebsiteChangeMonitor. robots.txt is checked for every host, including after redirects.
  • Snapshots contain the normalised text of watched pages and are stored only in your account. The Actor does not extract contact details or other personal data.

FAQ

Why did my first run return nothing?

The first run for a monitor name saves a baseline of each page and has nothing to compare against yet, so it returns no rows. Tick Output baseline on first run (outputBaseline) if you want a row for each page on that first run.

Does it work on pages built with JavaScript?

Not always. It fetches pages over plain HTTP without a browser, so pages that render their content with JavaScript may compare as empty or unchanged. Use a selector on content the server sends, or watch the site's RSS or JSON feed instead.

What happens if a page is blocked or fails?

It is listed in the run summary as blocked or failed, no row is written and you are not charged. For a failed check the previous snapshot is kept, so a temporary error does not wipe your baseline.

Where are my snapshots stored?

In a named key-value store, website-change-monitor-<name>, in your own Apify account. Snapshots hold the normalised text of watched pages, and the Actor does not extract contact details or other personal data.

Issues and feature requests: use the Issues tab.