Website Change Detection: Page Diff Alerts
Under maintenancePricing
from $2.00 / 1,000 page checkeds
Website Change Detection: Page Diff Alerts
Under maintenanceWatch many web pages for changes and get the exact lines or links that were added or removed. Scope it to one part of a page, ignore timestamps, render JavaScript when needed, and get alerts by email, Slack, Discord or webhook. Failed checks are free.
Pricing
from $2.00 / 1,000 page checkeds
Rating
0.0
(0)
Developer
Plainsource
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
13 hours ago
Last modified
Categories
Share
Website Change Monitor is a website change detection Actor. Give it a list of URLs and run it on a schedule: it monitors website changes by comparing each page with the copy it saved on the previous run, and returns the exact lines or links that were added or removed. You can also get an alert by email, Slack, Discord or webhook, but only when something actually changed.
It works as a Visualping alternative for people who want the diff as data (a dataset, an API response or a webhook) and prefer to pay per page checked. It compares text and links, not pixels, so it is built for pricing pages, changelogs, terms of service, listings and feeds.
What data can Website Change Monitor extract?
Every watched page produces one row per run.
| Field | What it contains |
|---|---|
status | baseline, changed, unchanged, selector_missing, failed, site_blocked, not_allowed or skipped |
changeSummary | One line such as "2 lines added, 1 line removed" or "5 new links, 6 links gone" |
addedText, removedText | The lines that appeared or disappeared (text mode). Up to 50 lines per row, each cut at 500 characters |
addedLinks, removedLinks | New and removed links as { url, text } (links mode). Up to 100 per row |
charactersAdded, charactersRemoved | Size of the change |
url, finalUrl, label | The page you gave, where it ended up after redirects, and your own label |
title, httpStatus | Page title and HTTP status |
selector, compareMode, renderJavaScript | The settings used for this page |
screenshotUrl | Link to a JPEG of the changed area, when screenshots are on |
textPreview | On a baseline row, the first 1,000 characters of what is being watched |
checkedAt, previousCheckAt, lastChangedAt, watchedSince | Timestamps for this check, the one before, the last change and the first check |
error | The reason a page could not be checked |
The run also writes a summary to the OUTPUT record of its key-value store, with counts per status and the list of changed pages.
How to use Website Change Monitor
- Open the Actor on Apify and click Try for free.
- Paste the pages you want to watch into Pages to watch.
- Optional: set Part of the page to watch to a CSS selector such as
mainor#pricing, and add noisy elements or text patterns (dates, counters) to the ignore lists. - Click Start. The first run saves a baseline for every page and reports each one with the status
baseline. - Create a schedule in Apify Console (hourly, daily or weekly) with the same input. Every later run compares with the previous one and returns
changedorunchanged. - Read the results in the dataset, or add email addresses or a Slack, Discord or webhook URL to be told when a page changes.
Saved versions live in a key-value store named website-change-monitor-state in your account, grouped by Watch list name. Use a different name for each task so separate lists keep separate history.
How much does it cost to monitor website changes?
You pay per event, not for compute time.
| Event | Price |
|---|---|
| Actor start | $0.002 per GB of run memory (the default is 1 GB, so $0.002 per run) |
| Page checked, plain HTTP | $0.002 |
| Page checked with JavaScript rendering | $0.004 |
| Change detected | $0.005 per changed page, on top of the page check |
| Screenshot of a change | $0.003 |
Email alerts and webhook messages have no event price. Pages that fail, are blocked, are disallowed by robots.txt or whose selector matches nothing are not charged.
Worked examples, from the prices above:
- 1,000 pages checked once without JavaScript, no changes: $0.002 + 1,000 x $0.002 = $2.002.
- 20 pages checked daily for 30 days, with about one change a week: 30 x ($0.002 + 20 x $0.002) + 4 x $0.005 = $1.28 a month.
- The same 1,000 pages with JavaScript rendering: $0.002 + 1,000 x $0.004 = $4.002.
If a run reaches the maximum cost you set, the remaining pages are marked skipped, their saved versions are kept, and they are checked again on the next run.
Input
The only required field is Pages to watch. This example watches two pages with their own selectors, emails the changes and posts them to Slack:
{"pages": [{ "url": "https://www.python.org/downloads/", "selector": ".download-list-widget", "label": "Python releases" },{ "url": "https://nodejs.org/en/blog", "selector": "main", "compareMode": "links", "label": "Node.js blog" }],"ignorePatterns": ["Updated: .*"],"notificationEmails": ["you@example.com"],"emailSubjectPrefix": "[Releases]","webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ","webhookFormat": "slack","stateKey": "release-pages"}
All settings:
| Field | Default | What it does |
|---|---|---|
Pages to watch (urls) | none | List of page URLs. |
Pages with their own settings (pages) | none | JSON list for pages that need their own selector, compareMode, label, ignoreSelectors, ignorePatterns, renderJavaScript, waitForSelector, waitSecs or screenshotSelector. |
Part of the page to watch (selector) | whole page | A CSS selector such as main, #pricing or .release-list. A smaller area means fewer false alerts from menus, footers and banners. |
What to compare (compareMode) | text | text reports added and removed lines. links reports new and removed links. |
Ignore elements (ignoreSelectors) | none | CSS selectors removed before comparing, such as .ad or time. |
Ignore text matching (ignorePatterns) | none | Regular expressions deleted before comparing, such as \d+ (minutes|hours) ago. Matching ignores case. |
Minimum change size (minChangedCharacters) | 1 | Smaller changes are not reported. The saved version is kept, so small edits add up until they count. |
Render JavaScript (renderJavaScript) | false | Loads pages in Chrome. Needed for pages that build their content in the browser. |
Wait for element (waitForSelector) | empty | With JavaScript rendering, waits up to 20 s for this selector before reading the page. |
Extra wait (waitSecs) | 0 | With JavaScript rendering, seconds to wait after the page loads (0 to 30). |
Screenshot changed pages (screenshotOnChange) | false | Saves a JPEG of the changed area and adds its link to the row. |
Screenshot area (screenshotSelector) | the watched part | CSS selector of the element to capture. |
Email alerts to (notificationEmails) | none | Up to 5 addresses. One email per run, sent only when at least one page changed. |
Email subject prefix (emailSubjectPrefix) | empty | Text put in front of the subject, up to 60 characters. |
Webhook URL (webhookUrl) | empty | Sends a POST to this https URL when at least one page changed. |
Webhook format (webhookFormat) | json | json (full rows), slack or discord (a readable message). |
Also notify about pages that failed (notifyOnErrors) | false | Also sends the email and webhook message when pages could not be checked, and lists those pages. |
Only output changed pages (onlyChanges) | false | Off: one row per page every run. On: rows only for changed pages. |
Watch list name (stateKey) | default | Saved versions are kept per watch list. |
Respect robots.txt (respectRobotsTxt) | true | Skips pages the site's robots.txt disallows. Turn off only for sites you own or may monitor. |
Pages checked at once (maxConcurrency) | 5 | 1 to 20. Requests to the same site stay at least 1 s apart, and at most 2 pages render in Chrome at a time. |
Page timeout (timeoutSecs) | 30 | Seconds to wait for each page (5 to 120) before reporting it as failed. |
Output
This is a real row from a test run on 2026-10-28. It watched the links in the Hacker News "newest" list (selector .athing .titleline, compareMode set to links), and two runs about 40 seconds apart found changes. The addedLinks and removedLinks lists are cut to two entries here; the real row had more.
{"url": "https://news.ycombinator.com/newest","label": null,"status": "changed","changeSummary": "5 new links, 6 links gone","addedText": [],"removedText": [],"addedLinks": [{"url": "https://chrispattle.com/avatars","text": "Show HN: I made my own version of Google Doodles"},{"url": "https://news.ycombinator.com/from?site=chrispattle.com","text": "chrispattle.com"}],"removedLinks": [{"url": "https://ao.news/live/n/US","text": "Show HN: Breaking abroad, not yet in [your country]"},{"url": "https://news.ycombinator.com/from?site=ao.news","text": "ao.news"}],"charactersAdded": 252,"charactersRemoved": 276,"title": "New Links | Hacker News","finalUrl": "https://news.ycombinator.com/newest","httpStatus": 200,"selector": ".athing .titleline","compareMode": "links","renderJavaScript": false,"screenshotUrl": null,"textPreview": null,"checkedAt": "2026-10-28T22:05:56.178Z","previousCheckAt": "2026-10-28T22:05:19.977Z","lastChangedAt": "2026-10-28T22:05:56.178Z","watchedSince": "2026-10-28T22:05:19.977Z","error": null}
Statuses:
baseline: first check, the version is saved.changed: see the added and removed fields.unchanged: nothing changed, or the change was below the minimum size.selector_missing: the selector matched nothing, so the layout may have changed. Not charged.failed,site_blocked,not_allowed,skipped: the reason is inerror.
What people use website change detection for
- Competitor pricing and plan pages.
- Changelogs, release pages and status pages.
- Terms of service, privacy policies and other legal pages.
- Product, stock or job listing pages. Use Links only to see which items are new.
- RSS and Atom feeds, JSON endpoints and plain-text files.
- Documentation pages that your own product depends on.
FAQ
How does it tell what changed?
It saves the text (or the links) of the watched part of each page, then compares the next fetch with that copy line by line. Lines that only moved position are not reported as a change. A saved page text is cut at 400,000 characters.
Can it detect visual changes like a new image or colour?
No. The comparison is on text and links. It can save a screenshot when a change is found, but a change that only affects pixels will not be detected.
Why do I get selector_missing or "no text"?
The page probably builds its content with JavaScript. Turn on Render JavaScript, and if needed set Wait for element to the selector you watch.
I get alerts for changes I do not care about. What can I do?
Narrow Part of the page to watch, add the noisy parts to Ignore elements, or add an Ignore text matching pattern for counters and dates. You can also raise Minimum change size.
How do email alerts work?
The run starts Apify's apify/send-mail Actor in your account with one message listing every changed page (up to 30), each change summary, the first 5 added and removed lines, the screenshot link and a link to the run. If nothing changed, no email is sent. On the Apify free plan, apify/send-mail can only send to your account's own email address, so check the run log and the emailError field of the OUTPUT record if nothing arrives, and check your spam folder.
How do I schedule it?
Create a task with your pages, then add it to a schedule in Apify Console (Schedules, then Create new). Each run compares with the previous one.
How do I send changes to Slack, Discord, Make, Zapier or n8n?
For Slack, create an incoming webhook, paste its URL into Webhook URL and set the format to Slack. For Discord, create a channel webhook and set the format to Discord. For Make, Zapier or n8n, use the json format to post full rows to their webhook trigger, or use the task's Integrations tab in Apify Console. Turn on Only output changed pages so integrations that act on new dataset rows only fire for real changes.
Can I call it from code or an AI agent?
Yes. Use the Apify API or the Apify MCP server (https://mcp.apify.com) with the same input. With the JavaScript client: client.actor("fernandosord/website-change-monitor").call({ urls: ["https://example.com/pricing"] }).
How do I start over?
Change the Watch list name. The next run saves a new baseline.
Is it polite to the sites it visits?
Robots.txt is respected by default, and requests to one site are at least 1 second apart (more if the site sets a Crawl-delay). The Actor identifies itself as PlainsourceChangeMonitor.
What does it not do?
- No proxies, logins or bot-check workarounds. Pages that block automated visitors are reported as
site_blockedand are not charged. - LinkedIn, Facebook, Instagram, X and TikTok are not supported, because their terms forbid automated access.
- Private and local addresses (such as
localhostand10.x.x.x) are refused, including through redirects. - Up to 2,000 pages per run, and pages up to 10 MB.
- Screenshots are stored in the run's default key-value store, which follows your account's data retention period.
Found a bug or need an option? Open an issue on this Actor's Issues tab.
Other Actors by this developer
- Website Screenshot & URL to PDF: bulk page captures, a good partner when you want a picture of a page you are watching.
- Website Tech Stack Detector: a BuiltWith alternative that lists the technologies a site uses.
- WHOIS & DNS Lookup: domain age, SSL and email security for a list of domains.
- Career Site Jobs API: open jobs from Greenhouse, Lever and Ashby career sites, with a daily new-jobs feed.
- PDF to Text & Markdown: turns PDFs, Office files and scans into text.