# Website Change Monitor: Page Diff & Alerts (`charlesmarkette/website-change-monitor-page-diff-alerts`) Actor

Watch web pages for changes and get only what changed: added and removed lines, links or HTML, for the whole page or one CSS selector, with ignore rules for dates and counters. Run it on a schedule and send alerts by email, Slack or Teams.

- **URL**: https://apify.com/charlesmarkette/website-change-monitor-page-diff-alerts.md
- **Developed by:** [Charles Markette](https://apify.com/charlesmarkette) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.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** watches a list of web pages and tells you what changed since the last run. Each run fetches every page, takes the part you care about (the **visible text**, the **HTML markup**, or the **set of links**, optionally only inside a **CSS selector** such as `.price` or `#stock`), compares it with the snapshot saved by the previous run, and outputs the **added and removed lines**. Ignore the parts that change on every visit (dates, view counters, "updated 3 minutes ago") with regular expressions. Put it on a schedule and connect an alert, and you get an email or a Slack, Teams, Discord or Telegram message only when something changed.

### Why this Actor

- **Shows what changed, not just that something did.** Every changed page comes with `addedLines`, `removedLines` and a one-sentence `changeSummary` such as "3 lines added, 1 removed."
- **Watch exactly the part you need.** A CSS selector per page (`.price`, `#availability`, `main article`), the same page watched twice with different selectors, labels you choose for each.
- **Quiet by design.** Ignore patterns, *Ignore numbers*, *Ignore upper/lower case* and a *Minimum change size* keep counters and timestamps from raising alarms. In the default *Changes only* output, a run where nothing changed leaves the dataset empty, so alert integrations stay silent.
- **You notice when a watch breaks.** Pages that fail to load and selectors that no longer match are always reported (`status: "error"` or `"selectorNotFound"`), and a failed check never overwrites the saved snapshot.
- **Light and polite.** Plain HTTP requests (no browser), at most 2 requests at a time to any one host, retries with backoff on HTTP 429 and 5xx (honouring `Retry-After`), and a clear User-Agent (`WebsiteChangeMonitor/1.0; +https://apify.com/charlesmarkette`). Only public addresses are fetched; private, loopback and cloud-metadata addresses are refused, also after a redirect.

### Use cases

- **Competitor pricing:** watch `.price` on competitors' product and pricing pages.
- **Back-in-stock alerts:** watch the availability label of a product.
- **Terms, policies and documentation:** see the exact sentences that were added or removed from a terms-of-service, privacy policy or changelog page.
- **New articles, jobs or files:** watch the *links* of a blog index, careers page or downloads page; added links are the new items.
- **Regulatory and public notices:** watch announcement pages of agencies, schools or councils.
- **Your own site:** get told when a deployment changes a page you didn't expect to change.

### How to use it

1. Add the **Pages to watch**. Plain URLs work, or objects that pick a part of the page and name it:
   `{"url": "https://shop.example.com/mug", "cssSelector": ".price", "label": "Mug price"}`.
2. Give it a **Monitor name** (for example `competitor-prices`). Runs with the same name share one memory of snapshots, so you can add or remove pages later without losing history.
3. Choose **What to watch** (visible text, HTML or links) and add **Ignore patterns** for anything that changes on every visit.
4. Run it once: every page gets a `baseline` row (the first snapshot). From the second run on, you get a row for each page that changed.
5. Schedule it and connect an alert (below).

**Input example**

```json
{
  "monitorName": "competitor-prices",
  "startUrls": [
    {"url": "https://shop.example.com/products/blue-mug", "cssSelector": ".price", "label": "Blue mug price"},
    {"url": "https://shop.example.com/products/blue-mug", "cssSelector": "#availability", "label": "Blue mug stock"},
    "https://www.example.org/terms"
  ],
  "watch": "text",
  "ignorePatterns": ["Last updated:? .*", "\\d+ people are viewing this"],
  "outputMode": "changesOnly"
}
```

**Output example** (one changed page)

```json
{
  "url": "https://www.example.org/terms",
  "label": null,
  "finalUrl": "https://www.example.org/terms",
  "statusCode": 200,
  "checkedAt": "2026-10-22T08:00:05Z",
  "status": "changed",
  "changed": true,
  "previousCheckedAt": "2026-10-21T08:00:04Z",
  "lastChangedAt": "2026-10-22T08:00:05Z",
  "addedLines": ["Refunds are available within 14 days of purchase.", "Disputes are handled in Delaware."],
  "removedLines": ["Refunds are available within 30 days of purchase."],
  "addedCount": 2,
  "removedCount": 1,
  "changeSummary": "2 lines added, 1 removed.",
  "contentLength": 18452,
  "contentHash": "4f3c9a…",
  "title": "Terms of Service",
  "watch": "text",
  "cssSelector": null,
  "monitorName": "competitor-prices",
  "error": null,
  "note": null
}
```

`status` is one of:

| Status | Meaning | In *Changes only* output |
|---|---|---|
| `baseline` | First time this page (with this selector and watch mode) is checked; its snapshot is saved. | Yes |
| `changed` | The watched content differs from the last snapshot. | Yes |
| `unchanged` | Same as last time (after the ignore rules). | No |
| `error` | The page couldn't be checked: network error, HTTP 4xx/5xx, not a text page, or an invalid URL. The saved snapshot is kept. | Yes |
| `selectorNotFound` | The page loaded but nothing matches the CSS selector. The saved snapshot is kept. | Yes |

Errors and missing selectors are always reported, so you find out when a watch stopped working instead of hearing nothing. In *Links* mode, `addedLines` and `removedLines` hold absolute URLs. The lists hold at most *Max diff lines per page* entries; `addedCount` and `removedCount` are the full numbers. `note` explains special cases (a change below the minimum size, a page over 5 MB, content too big to list the lines). A run summary (counts, the changed URLs, the snapshot store's name) is saved as `OUTPUT` in the run's key-value store.

### Run it on a schedule

1. Save your input as a **task** (Actor page → *Save as a new task*), so every scheduled run uses the same pages and monitor name.
2. In Apify Console open **Schedules → Create new**, set how often (for example every day at 08:00, or every hour), and add the task.

Each scheduled run compares with the previous one. Checking more often than a page actually changes only adds cost: daily is plenty for terms and policies, hourly suits prices and stock.

### Get alerts by email, Slack, Teams, Discord or Telegram

Connect our [Send Email Report & Slack, Discord, Teams, Telegram Alerts](https://apify.com/charlesmarkette/send-email-report-slack-discord-teams-telegram-alerts) Actor as an integration:

1. Open your monitoring task → **Integrations** → **Add integration**, pick that Actor and choose to run it when the run **succeeded**.
2. In its input set the dataset to `{{resource.defaultDatasetId}}` (Apify fills in this run's output), add your email or webhook, and pick the fields to show, for example `label`, `url`, `changeSummary`, `addedLines`, `removedLines`, `error`.

By default that Actor sends nothing when the dataset is empty, and with *Changes only* the dataset is empty when nothing changed, so you hear from it only on changes, first checks and broken watches.

### Ignoring noise

- **Ignore patterns** are regular expressions (Python syntax) removed from each line before comparing: `Updated \d+ (minutes|hours) ago`, `©\s*\d{4}`, `\(\d+ views\)`. An invalid expression fails the run with a message saying which one.
- **Ignore numbers** treats all numbers as equal (leave it off when you watch prices).
- **Ignore upper/lower case** treats "In stock" and "IN STOCK" alike.
- **Minimum change size** skips changes smaller than N characters (the size of the changed text, after the ignore rules). Small changes add up: until a change is reported, comparisons stay against the last reported version.

The lines in the output are shown as they appear on the page; the ignore rules only decide whether something counts as a change. If you edit the ignore settings later, the saved lines are compared again under the new rules, so editing them doesn't cause false alarms.

### How much does it cost?

Pay per event: **$0.02 per page checked**. Apify platform usage is included, plus Apify's run start fee of **$0.005** per run (one start event per GB of run memory; the default 256 MB run is charged one). **Failed fetches are free**: pages that couldn't be loaded (network errors, HTTP 4xx/5xx, non-text files) and invalid URLs cost nothing.

A page counts as checked when it loaded and was compared, whatever the outcome: `baseline`, `changed`, `unchanged` or `selectorNotFound`. That includes unchanged pages that *Changes only* leaves out of the dataset. A page listed twice with two selectors is two checks.

**Worked examples:**

- 10 competitor pages checked once a day: 10 × $0.02 = $0.20 per run, about **$6 per month**.
- 3 product pages checked every hour: 3 × 24 × $0.02 = $1.44 per day, about **$43 per month**.
- 50 policy pages checked once a week: $1.00 per run, about **$4.30 per month**.

Each run in these examples also has the $0.005 run start fee (about $0.15 a month for a daily schedule).

If you set a maximum cost per run, the Actor stops cleanly at that limit; pages it didn't get to keep their snapshots and are compared on the next run.

### FAQ and limits

#### Does it see content that JavaScript adds to the page?

No. It downloads the page over plain HTTP, like `curl`, and doesn't run JavaScript. Content that a script loads after the page opens (many single-page apps, some prices and stock widgets) isn't seen. If a selector shows `selectorNotFound` although you can see the element in your browser, that's the likely reason. Check by viewing the page source (Ctrl+U): if the text is there, this Actor sees it.

#### A site returns errors (403, 429, 503).

Some sites block automated requests or rate-limit them. The Actor retries a few times, then reports an `error` row (free) and keeps the old snapshot. It doesn't use proxies, log in, or solve CAPTCHAs, so sites that block bots can't be watched with it.

#### Where are the snapshots stored?

In a named key-value store in your Apify account, called `change-monitor-<monitor name>-<short code>` (the exact name is in the run summary). Named stores are kept until you delete them. Delete the store to start the monitor from scratch. Without a monitor name, the store is derived from the exact URL list, so changing the list starts new baselines; set a name to avoid that.

#### What resets a page to a new baseline?

Changing its CSS selector or the *What to watch* mode (a different part of the page can't be compared with the old one). Changing ignore settings doesn't, except for pages whose content was too big to keep the lines (below).

#### Are there size limits?

Pages are read up to 5 MB (the rest is ignored, and the row's `note` says so). The watched content of each page is kept as lines up to about 200 KB; beyond that only its hash is kept, so changes are still detected but the changed lines can't be listed. Use a CSS selector to watch a smaller part. Very large pages take more memory to parse; if a run runs out of memory, raise it to 512 MB. Up to 10,000 pages per run.

#### What counts as visible text?

What a reader sees: one line per paragraph, heading, list item or table row, whitespace collapsed. Scripts, styles, `<noscript>`, `<template>`, SVG and elements marked `hidden` are left out. *HTML* mode compares the markup of the selection (the page body by default) with attributes sorted and whitespace normalized, so a changed class or link target counts too.

#### Can I watch URLs from another Actor's output?

Yes: pick a **Dataset of URLs** and set the **URL field**. Set a monitor name as well, so the memory doesn't depend on the list.

### Other tools by the same author

| Actor | Use it to |
|---|---|
| [Send Email Report & Slack, Discord, Teams, Telegram Alerts](https://apify.com/charlesmarkette/send-email-report-slack-discord-teams-telegram-alerts) | Get a message after each run that found changes. |
| [Google Sheets Sync](https://apify.com/charlesmarkette/google-sheets-sync) | Keep a log of every change in a Google Sheet. |
| [Broken Link Checker](https://apify.com/charlesmarkette/broken-link-checker-crawl-website-404) | Crawl a whole site and find broken links and 404 pages. |
| [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) | Combine the change rows of several monitors, or keep only new items between runs. |

# Actor input Schema

## `startUrls` (type: `array`):

The pages to check on every run. Each entry is a URL ("https://example.com/pricing"), or an object that watches one part of the page and names it: {"url": "https://shop.example.com/item", "cssSelector": ".price", "label": "Item price"}. The same URL can be listed twice with different selectors.

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

Runs with the same name share one memory of page snapshots, so you can add or remove pages without losing history. Leave empty to tie the memory to the exact URL list (changing the list then starts fresh baselines). Snapshots are kept in a named key-value store called change-monitor-<name>-<short code>.

## `watch` (type: `string`):

Visible text: what a reader sees, one line per paragraph, heading or list item (scripts, styles and hidden elements are left out). HTML: the normalized markup, so attribute and structure changes count too. Links: the set of link URLs, for new or removed articles, files or products.

## `cssSelector` (type: `string`):

Optional. Watch only the elements that match, for example .price, #stock-status or main article. Applies to every page unless an entry in 'Pages to watch' has its own cssSelector. If nothing matches, the row says selectorNotFound and the saved snapshot is kept.

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

Regular expressions (Python syntax) removed from each line before comparing, for parts that change on every visit: dates, view counters, "x minutes ago". Example: \d+ (views|comments) or Updated .\* ago. An invalid expression fails the run with a clear message.

## `ignoreCase` (type: `boolean`):

Treat "In stock" and "IN STOCK" as the same.

## `ignoreNumbers` (type: `boolean`):

Treat every number as equal, so only text changes count. Leave off to watch prices or stock counts.

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

Ignore changes smaller than this many characters (after the ignore rules). Small changes still add up: the comparison stays against the last reported version until the total is big enough. 0 reports every change.

## `outputMode` (type: `string`):

Changes only: rows for changed pages, first checks (baseline) and pages that couldn't be checked or whose selector matched nothing, so you notice when a watch breaks. An unchanged run leaves the dataset empty, which keeps alert integrations quiet. Every page: a row for each page, unchanged ones included.

## `maxDiffLines` (type: `integer`):

At most this many added and this many removed lines are listed per page; addedCount and removedCount always give the full numbers.

## `maxResults` (type: `integer`):

Optional cap on output rows. The run stops when it is reached; pages not checked yet keep their snapshots for the next run.

## `datasetId` (type: `string`):

Optional. Watch the URLs in a dataset, for example a sitemap or scraper output. Use {{resource.defaultDatasetId}} in an integration. Set a Monitor name when you do, so the memory doesn't depend on the list.

## `urlField` (type: `string`):

The item field that holds the page URL. Dotted paths work (page.url).

## Actor input object example

```json
{
  "startUrls": [
    "https://www.python.org/downloads/",
    {
      "url": "https://www.iana.org/domains/reserved",
      "cssSelector": "main",
      "label": "IANA reserved domains"
    }
  ],
  "watch": "text",
  "ignoreCase": false,
  "ignoreNumbers": false,
  "minChangeChars": 0,
  "outputMode": "changesOnly",
  "maxDiffLines": 50,
  "urlField": "url"
}
```

# Actor output Schema

## `items` (type: `string`):

No description

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

No description

# 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 = {
    "startUrls": [
        "https://www.python.org/downloads/",
        {
            "url": "https://www.iana.org/domains/reserved",
            "cssSelector": "main",
            "label": "IANA reserved domains"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("charlesmarkette/website-change-monitor-page-diff-alerts").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 = { "startUrls": [
        "https://www.python.org/downloads/",
        {
            "url": "https://www.iana.org/domains/reserved",
            "cssSelector": "main",
            "label": "IANA reserved domains",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("charlesmarkette/website-change-monitor-page-diff-alerts").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 '{
  "startUrls": [
    "https://www.python.org/downloads/",
    {
      "url": "https://www.iana.org/domains/reserved",
      "cssSelector": "main",
      "label": "IANA reserved domains"
    }
  ]
}' |
apify call charlesmarkette/website-change-monitor-page-diff-alerts --silent --output-dataset

```

## MCP server setup

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

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/5aMh3yiv8KufSsfHN/builds/OlML7fhjnTgsRu6x0/openapi.json
