# Website Change Monitor — Page Diff, CSS Selector & Webhook (`actor-forge/website-change-monitor`) Actor

Monitor web pages for changes on a schedule: compares each page with the previous run (readable text, HTML or CSS-selector elements), ignores timestamps and noise, returns a unified diff and posts changes to Slack, Discord, Zapier, Make or n8n. A Visualping alternative at $2 per 1,000 checks.

- **URL**: https://apify.com/actor-forge/website-change-monitor.md
- **Developed by:** [ActorForge](https://apify.com/actor-forge) (community)
- **Categories:** Automation, Developer tools, SEO tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 page checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Website Change Monitor — Page Diff, CSS Selector & Webhook

Monitor web pages for changes on a schedule: compares each page with the previous run (readable text, HTML or CSS-selector elements), ignores timestamps and noise, returns a unified diff and posts changes to Slack, Discord, Zapier, Make or n8n. A Visualping alternative at $2 per 1,000 checks.

**Get told when a web page changes, without checking it yourself.** This **website change monitor** fetches each of your URLs, compares it with what it saw on the previous run, and returns one row per page: **baseline** (first check), **unchanged**, **changed** (with a **unified diff**, added/removed line counts and percent changed) or **error**. Use it for **page change detection** on competitor pricing pages, product and stock pages, terms of service and privacy policies, job boards, government notices, documentation and changelogs. It is a **Visualping alternative** (and ChangeTower / Distill alternative) that you own: results land in an Apify dataset and API, and changes can be posted to **Slack, Discord, Zapier, Make, n8n or your own webhook**.

**Three ways to compare.** `text` (default) compares the readable page as Markdown, without menus, header, footer, cookie banners or scripts, so redesigns and ad rotations don't trigger alerts. `html` compares the markup itself (scripts and styles excluded) to catch changed links, images and attributes. `selector` watches only the elements you name with **CSS selectors**, e.g. `.price` or `#pricing-table`: the cheapest way to **monitor competitor prices** or one section of a busy page.

**No false alarms from noise.** Dates, clocks and "5 minutes ago" are ignored by default. Add your own **ignore patterns** (regular expressions such as `Visitors: \d+`) and **ignore selectors** (`.ad`, `#related`), and set a **minimum change size** so a one-character edit doesn't page anyone (small edits add up until they cross the threshold). Whitespace and re-indentation are ignored.

**Built for schedules.** Snapshots live in a named key-value store, `change-monitor-<monitorName>`, that persists between runs, so every scheduled run compares against the last one. Use one monitor name per watch list. Fetch failures and robots.txt blocks are reported as free error rows and never overwrite the saved snapshot, so one bad response can't hide a change.

### How to schedule it (daily, hourly or weekly checks)

1. Run the actor once with your URLs and a `monitorName` (e.g. `competitor-prices`). That run saves the baselines.
2. In Apify Console, open the actor and click **Save as a new task** so the input is stored, or keep using the actor's input.
3. Go to **Schedules** → **Create new schedule**, choose how often (e.g. every day at 08:00 with the cron `0 8 * * *`, or hourly `0 * * * *`), and under **Add** pick the task (or this actor with the same input).
4. Add a `webhookUrl` (a Slack or Discord incoming webhook, or a Zapier / Make / n8n catch hook) to get a message only when something changed. You can also attach an Apify integration or webhook to the task's **Run succeeded** event and read the run's dataset.

Each scheduled run returns rows for every URL (or only the changes, with `outputOnlyChanges`) plus an `OUTPUT` summary record listing the changed URLs. Via the API, start the task and read `.../runs/last/dataset/items?status=SUCCEEDED`.

### Webhook payload

Slack and Discord get a readable message with the changed lines. Any other URL gets JSON: `{"event": "changes-detected", "monitorName", "runId", "datasetUrl", "checkedAt", "counts": {"checked", "baseline", "unchanged", "changed", "errors"}, "changes": [{"url", "title", "addedLines", "removedLines", "percentChanged", "diff"}], "errors": [{"url", "error"}]}`. One POST per run; by default only when at least one page changed.

### What data does it return?

Every run outputs clean, schema-validated rows like these:

| url | status | statusCode | changeSummary | diff | previousCheckedAt | checkedAt |
| --- | --- | --- | --- | --- | --- | --- |
| https://competitor.example/pricing | changed | 200 | {"addedLines":1,"removedLines":1,"changedChars":36,"percentChanged":1.12} | @@ -12,5 +12,5 @@ ... -Pro: $49 per month +Pro: $59 per month | 2026-10-18T08:00:04Z | 2026-10-19T08:00:03Z |
| https://www.iana.org/help/example-domains | unchanged | 200 | {"addedLines":0,"removedLines":0,"changedChars":0,"percentChanged":0} |  | 2026-10-18T08:00:04Z | 2026-10-19T08:00:03Z |
| https://shop.example/new-product | baseline | 200 |  |  |  | 2026-10-19T08:00:05Z |
| https://down.example/terms | error | 503 |  |  | 2026-10-18T08:00:06Z | 2026-10-19T08:00:09Z |

### How much does it cost?

Transparent pay-per-event pricing — you only pay for what you get, and the
actor **always respects your run budget**: set a maximum total charge and it
stops cleanly at the cap instead of overrunning.

| Event | Price | Charged | Worked example |
| --- | --- | --- | --- |
| `page-checked` | $0.002 | per URL fetched and compared (baseline, unchanged or changed) | 50 pages checked daily for a month = 1,500 checks = $3.00 |
| `change-detected` | $0.005 | extra, per URL whose content changed since the previous run | 20 changes in that month = $0.10 |
| `apify-actor-start` | $0.00005 | per run per GB of memory (platform start fee) | One run at up to 1 GB = $0.00005 (charged as one event) |

### Input options

| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `urls` | array | yes | — | Pages to watch, one per line (up to 5,000; duplicates removed). Each run compares every page with what it saw on the previous run of the same monitor. Bare domains like example.com/pricing work too. |
| `monitorName` | string | no | `"default"` | Snapshots are saved in a named key-value store called change-monitor-\<name>, which persists between runs. Use one name per schedule (e.g. competitor-prices, legal-pages) so separate watch lists keep separate baselines. Letters, numbers and dashes. |
| `mode` | string | no | `"text"` | Text catches what a reader would notice and ignores layout changes. HTML also catches changes to links, images and markup (scripts and styles are excluded). Selector watches only the parts you name in CSS selectors, e.g. a price or a table. |
| `cssSelector` | array | no | — | For mode "selector": CSS selectors of the parts to watch, e.g. .price, #pricing-table, main h2. The text of every matching element is compared. A page where no selector matches is reported as an error (free) and its snapshot is kept. |
| `ignoreSelectors` | array | no | — | CSS selectors for parts of the page to remove before comparing, like ads, a related-posts box, a live chat widget or a visitor counter. Example: .ad, #related, .view-count. |
| `ignorePatterns` | array | no | — | Regular expressions for text to strip before comparing (case-insensitive; ^ and $ match line starts/ends). Example: Visitors: \d+ or ^Ad #\d+.\*$. Use /pattern/flags to choose flags yourself. |
| `ignoreDatesAndTimes` | boolean | no | `true` | Strip clocks, dates and relative times (2026-10-19, 10:42 am, Oct 19 2026, 19/10/2026, 5 minutes ago) before comparing, so a page whose only change is its timestamp is not reported as changed. Prices and counts are not touched. |
| `ignoreWhitespace` | boolean | no | `true` | Collapse spaces and tabs and drop blank lines before comparing, so re-indented or re-flowed pages are not reported as changed. |
| `minChangeChars` | integer | no | `0` | Report a page as changed only when at least this many characters were added plus removed. Smaller changes are reported as unchanged and the old baseline is kept, so small edits add up until they cross the threshold. 0 = any change counts. |
| `includeDiff` | boolean | no | `true` | Add a unified diff (lines starting with - were removed, + were added, with 2 lines of context) to each changed row, capped at about 20 KB. Added/removed line counts are always included. |
| `outputOnlyChanges` | boolean | no | `false` | Leave unchanged pages out of the dataset (they are still checked and their snapshot is refreshed). Baselines, changes and errors are always output. |
| `webhookUrl` | string | no | — | Optional. At the end of the run, POST a summary of the changes here. Slack and Discord incoming-webhook URLs get a readable message with the changed lines; any other URL (Zapier, Make, n8n, your server) gets JSON with counts, each change (URL, title, line counts, diff) and errors. Private and internal addresses are blocked. |
| `webhookOnlyOnChanges` | boolean | no | `true` | On by default: no message on runs where nothing changed. Turn off to get a summary after every run (useful as a heartbeat). |
| `respectRobotsTxt` | boolean | no | `true` | Skip pages that the site's robots.txt disallows for crawlers (reported as a free error row). Turn off only for sites you own or are allowed to monitor. |
| `maxUrls` | integer | no | `500` | Safety cap on how many URLs one run checks; extra URLs are skipped. |
| `requestTimeoutSecs` | integer | no | `30` | How long to wait for each page before reporting it as an error (free). Transient failures (timeouts, 429, 5xx) are retried twice. |
| `maxConcurrency` | integer | no | `10` | How many pages to check at once. At most 2 pages per site are fetched at the same time. |
| `proxyConfiguration` | object | no | `{"useApifyProxy":false}` | Off by default (direct requests from Apify's datacenter). Use Apify Proxy (datacenter groups) for sites that rate-limit or block a single IP, or your own proxy URLs. Residential and SERP proxy groups are not supported: pages are fetched with plain HTTP, and bot-protected sites usually need a browser anyway. |

### Limits & honest notes

- You pay only for pages that were fetched and compared. Errors are free: timeouts, HTTP 4xx/5xx, robots.txt blocks, unsupported file types (images, PDFs, binaries), a selector that matches nothing, blocked internal addresses. Error rows never overwrite the saved snapshot.
- The first run of a monitor (or the first time a URL is seen) is a baseline: only page-checked is charged and there is nothing to diff yet. Changing the mode, selectors or ignore rules also starts a new baseline instead of reporting a false change.
- Pages are fetched with plain HTTP (no browser), which keeps checks cheap and fast. Content that only appears after JavaScript runs is not seen; such pages are reported as an error (no text content) or compare only their static HTML. Sites with CAPTCHAs or strong bot protection are out of scope.
- Up to 5,000 URLs per run (maxUrls, default 500), 10 in parallel by default (up to 50), at most 2 at a time per site. Pages larger than 10 MB are skipped; compared content is capped at 1 million characters and the diff at about 20 KB per page (counts stay exact).
- Snapshots are kept per monitor name in your own Apify account (named key-value store change-monitor-<name>), so separate schedules need separate monitor names. Deleting that store resets the monitor to baselines.
- robots.txt is respected by default. If a site's robots.txt can't be fetched (HTTP 5xx or a network error, after one retry), that site's URLs get a free error row for the run (RFC 9309) and their saved snapshots are kept. Requests to localhost, private networks and cloud-metadata addresses are blocked for pages and webhooks, including after redirects; webhooks do not follow redirects.
- Proxy: off by default; Apify datacenter proxy and your own proxy URLs are supported, residential and SERP groups are not.
- Respects your run budget: set a maximum total charge and the run stops cleanly at the cap. Pages not checked keep their snapshot, so the next run still reports their changes.

### Related ActorForge actors

- [Wayback Machine Scraper](https://apify.com/actor-forge/wayback-snapshots) — what a page looked like in the past
- [Website Screenshot API](https://apify.com/actor-forge/website-screenshot)
- [Website to Markdown](https://apify.com/actor-forge/website-to-markdown)
- [RSS Feed Reader](https://apify.com/actor-forge/rss-feed-reader)

### Changelog

- **2026-10-19** — Initial release: text, HTML and CSS-selector comparison, unified diffs, ignore patterns/selectors, date and time masking, minimum change size, named per-monitor snapshots for schedules, Slack/Discord/JSON webhook, robots.txt and SSRF protection, optional proxy.

***

*Built and maintained by **ActorForge**. Reliability first: every actor in the
portfolio is monitored by an automated nightly self-test, and breakages are
typically fixed the same day.*

# Actor input Schema

## `urls` (type: `array`):

Pages to watch, one per line (up to 5,000; duplicates removed). Each run compares every page with what it saw on the previous run of the same monitor. Bare domains like example.com/pricing work too.

## `monitorName` (type: `string`):

Snapshots are saved in a named key-value store called change-monitor-<name>, which persists between runs. Use one name per schedule (e.g. competitor-prices, legal-pages) so separate watch lists keep separate baselines. Letters, numbers and dashes.

## `mode` (type: `string`):

Text catches what a reader would notice and ignores layout changes. HTML also catches changes to links, images and markup (scripts and styles are excluded). Selector watches only the parts you name in CSS selectors, e.g. a price or a table.

## `cssSelector` (type: `array`):

For mode "selector": CSS selectors of the parts to watch, e.g. .price, #pricing-table, main h2. The text of every matching element is compared. A page where no selector matches is reported as an error (free) and its snapshot is kept.

## `ignoreSelectors` (type: `array`):

CSS selectors for parts of the page to remove before comparing, like ads, a related-posts box, a live chat widget or a visitor counter. Example: .ad, #related, .view-count.

## `ignorePatterns` (type: `array`):

Regular expressions for text to strip before comparing (case-insensitive; ^ and $ match line starts/ends). Example: Visitors: \d+ or ^Ad #\d+.\*$. Use /pattern/flags to choose flags yourself.

## `ignoreDatesAndTimes` (type: `boolean`):

Strip clocks, dates and relative times (2026-10-19, 10:42 am, Oct 19 2026, 19/10/2026, 5 minutes ago) before comparing, so a page whose only change is its timestamp is not reported as changed. Prices and counts are not touched.

## `ignoreWhitespace` (type: `boolean`):

Collapse spaces and tabs and drop blank lines before comparing, so re-indented or re-flowed pages are not reported as changed.

## `minChangeChars` (type: `integer`):

Report a page as changed only when at least this many characters were added plus removed. Smaller changes are reported as unchanged and the old baseline is kept, so small edits add up until they cross the threshold. 0 = any change counts.

## `includeDiff` (type: `boolean`):

Add a unified diff (lines starting with - were removed, + were added, with 2 lines of context) to each changed row, capped at about 20 KB. Added/removed line counts are always included.

## `outputOnlyChanges` (type: `boolean`):

Leave unchanged pages out of the dataset (they are still checked and their snapshot is refreshed). Baselines, changes and errors are always output.

## `webhookUrl` (type: `string`):

Optional. At the end of the run, POST a summary of the changes here. Slack and Discord incoming-webhook URLs get a readable message with the changed lines; any other URL (Zapier, Make, n8n, your server) gets JSON with counts, each change (URL, title, line counts, diff) and errors. Private and internal addresses are blocked.

## `webhookOnlyOnChanges` (type: `boolean`):

On by default: no message on runs where nothing changed. Turn off to get a summary after every run (useful as a heartbeat).

## `respectRobotsTxt` (type: `boolean`):

Skip pages that the site's robots.txt disallows for crawlers (reported as a free error row). Turn off only for sites you own or are allowed to monitor.

## `maxUrls` (type: `integer`):

Safety cap on how many URLs one run checks; extra URLs are skipped.

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each page before reporting it as an error (free). Transient failures (timeouts, 429, 5xx) are retried twice.

## `maxConcurrency` (type: `integer`):

How many pages to check at once. At most 2 pages per site are fetched at the same time.

## `proxyConfiguration` (type: `object`):

Off by default (direct requests from Apify's datacenter). Use Apify Proxy (datacenter groups) for sites that rate-limit or block a single IP, or your own proxy URLs. Residential and SERP proxy groups are not supported: pages are fetched with plain HTTP, and bot-protected sites usually need a browser anyway.

## Actor input object example

```json
{
  "urls": [
    "https://www.iana.org/help/example-domains",
    "https://news.ycombinator.com/"
  ],
  "monitorName": "default",
  "mode": "text",
  "cssSelector": [],
  "ignoreSelectors": [],
  "ignorePatterns": [],
  "ignoreDatesAndTimes": true,
  "ignoreWhitespace": true,
  "minChangeChars": 0,
  "includeDiff": true,
  "outputOnlyChanges": false,
  "webhookOnlyOnChanges": true,
  "respectRobotsTxt": true,
  "maxUrls": 500,
  "requestTimeoutSecs": 30,
  "maxConcurrency": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

One row per URL: baseline, unchanged, changed (with a unified diff) or error.

## `summary` (type: `string`):

Counts of checked, changed, unchanged, baseline and error pages, the changed URLs, and the webhook delivery result.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://www.iana.org/help/example-domains",
        "https://news.ycombinator.com/"
    ],
    "cssSelector": [],
    "ignoreSelectors": [],
    "ignorePatterns": [],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("actor-forge/website-change-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": [
        "https://www.iana.org/help/example-domains",
        "https://news.ycombinator.com/",
    ],
    "cssSelector": [],
    "ignoreSelectors": [],
    "ignorePatterns": [],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("actor-forge/website-change-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://www.iana.org/help/example-domains",
    "https://news.ycombinator.com/"
  ],
  "cssSelector": [],
  "ignoreSelectors": [],
  "ignorePatterns": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call actor-forge/website-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,actor-forge/website-change-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/aIbGICSlIhccpKRKs/builds/cxSDhD7Gbogo1hv1M/openapi.json
