Website Change Monitor: Detect Page & Text Changes
Pricing
from $1.00 / 1,000 url checkeds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
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,mainor.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
- Open the Actor in Apify Console and add the pages you want to watch under Pages to watch.
- Optionally add a CSS selector (all pages), Keyword triggers or Ignore patterns.
- Give the watch list a Monitor name, for example
competitor-pricing. Keep the same name for every run of this list. - 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.
- 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.
- 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
| Field | Type | Default | Description |
|---|---|---|---|
startUrls | array | required | URLs to check. Items can be strings, {"url": "..."}, {"url": "...", "selector": "#main"} or {"url": "...", "userData": {"selector": "..."}}. |
monitorName | string | default | Watch-list name. Snapshots go to the key-value store website-change-monitor-<name>. Keep it the same across scheduled runs. |
selector | string | none | CSS selector applied to every URL. A per-URL selector overrides it. |
mode | text, html | text | text compares visible text (whitespace, scripts, styles and comments ignored). html compares markup. |
keywords | string[] | [] | If set, a change is reported only when a keyword's count changes (case-insensitive). |
keywordTrigger | any, appears, disappears | any | Which direction of keyword count change counts. |
ignorePatterns | string[] | [] | Case-insensitive regexes removed before comparing. Invalid regexes are matched as literal text. |
outputBaseline | boolean | false | Also write a changeType: "baseline" row on the first check of a page. Not charged as a change. |
resetBaseline | boolean | false | Ignore saved snapshots and record a new baseline. |
maxUrls | integer | 100 | 1 to 1000. Only the first N URLs are checked. |
concurrency | integer | 5 | 1 to 20. Requests to the same host are always sequential. |
perHostDelaySecs | number | 1 | 0.5 to 30. Gap between requests to one host. A longer robots.txt Crawl-delay is used, up to 10 s. |
maxDiffChars | integer | 4000 | 500 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).
| Field | Type | Description |
|---|---|---|
url | string | Watched URL |
finalUrl | string | URL after redirects |
selector | string or null | CSS selector compared, null for the whole page |
monitorName | string | Watch-list name |
pageTitle | string or null | HTML <title> |
httpStatus | integer | HTTP status |
changeType | string | changed or baseline |
changedAt | string or null | ISO 8601 UTC time the change was detected |
checkedAt | string | ISO 8601 UTC time of this check |
previousCheckedAt | string or null | Time of the snapshot compared against |
previousHash / currentHash | string | SHA-256 of the normalised content |
diff | string | Unified diff, capped at maxDiffChars |
diffTruncated | boolean | true if diff was cut |
addedLines / removedLines | string[] | Changed lines, up to 200 each |
addedCount / removedCount | integer | Number of lines added / removed |
keywordsMatched | string[] | Keywords that triggered (for baseline rows: keywords present) |
keywordEvents | object[] | {keyword, event, before, after}, where event is appeared or disappeared |
lineCount | integer | Lines 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 osfrom apify_client import ApifyClientclient = 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.