# Website Change Monitor – Diff & Webhook Alerts (`oldjard/website-change-monitor`) Actor

Watch any web page, or one part of it, and get a before/after diff when it changes. Alerts by Slack, Discord or webhook. Ignores clocks and "2 hours ago", double-checks each change, renders JavaScript when needed. $1 per 1,000 checks + $5 per 1,000 changes.

- **URL**: https://apify.com/oldjard/website-change-monitor.md
- **Developed by:** [Joshua White](https://apify.com/oldjard) (community)
- **Categories:** Automation, Developer tools, E-commerce
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 – Diff & Webhook Alerts

**Website Change Monitor** watches any web page, or **just one part of it** (a price, a "Sold out" badge, a changelog),
and tells you exactly what changed: lines added and removed, a before/after view and optional screenshots. **Webhook
alerts** go to any URL you give it (Zapier, Make, n8n, your own server), and Slack and Discord incoming-webhook URLs
get a ready-formatted message. Clock times and "5 minutes ago" don't trigger false alerts, every change is
double-checked, and **pages that can't be checked are never charged**.

**Try it in one click:** press **Start**. The first run saves a **baseline** snapshot of the prefilled page; from the
second run on you get only what changed. Then schedule it (below).

### How to monitor a web page for changes in 3 steps

1. Paste the pages to watch into **URLs to monitor**. To watch only part of a page, add a CSS selector (right-click
   the part → **Inspect** → copy selector).
2. Add a **Slack or Discord webhook URL**, or any webhook, for alerts.
3. Run it once to save the baseline, then **schedule** it hourly, daily or weekly.

### How much does website change monitoring cost?

**$1 per 1,000 page checks** plus **$5 per 1,000 changes** found, and $2 per 1,000 pages that need a browser. Checks
that fail (blocked, timeouts, errors, selector not found) are free. Watching 10 pages daily costs about $0.30 a month
plus a few cents per change. You can set a maximum cost per run, and the actor stops cleanly when it is reached.

### What you can use a website change monitor for

- **Competitor pages:** pricing, plans, feature lists, job openings, product launches.
- **Prices and stock:** watch the price or the "Sold out" badge on a product page with one CSS selector.
- **Policies and docs:** terms of service, privacy policies, API docs, changelogs, release pages.
- **Government and public notices:** tenders, grant pages, permit lists, school or council announcements.
- **Your own sites:** catch an accidental content change, a broken deploy, or an SEO tag that disappeared.
- **APIs and feeds:** JSON endpoints and plain-text files (like `robots.txt`) are compared value by value.

### Run it on a schedule

Save your input as a **task** (the "Save as a new task" button), then in Apify Console open **Schedules → Create
new**, pick how often (for example daily at 9:00), and add the task. Every scheduled run compares with the
snapshots from the run before. Keep the same **Monitor name** for the same set of pages, and use a different name for
each separate monitor so their snapshots don't mix.

### Input example

```json
{
  "urls": ["https://example.com/pricing", "https://example.com/changelog"],
  "selector": "main",
  "webhookUrl": "https://hooks.slack.com/services/T000/B000/XXXX",
  "monitorName": "competitor-pages"
}
```

Different settings per page go in **Pages (advanced)**:

```json
{
  "pages": [
    { "url": "https://shop.example.com/widget", "selector": ".price", "label": "Widget price" },
    { "url": "https://shop.example.com/widget", "selector": ".stock", "label": "Widget stock" },
    { "url": "https://example.com/docs", "selector": "//article", "renderJavaScript": "always" },
    { "url": "https://example.com/blog", "contentMode": "attribute", "selector": "article a", "attribute": "href" }
  ],
  "ignorePatterns": ["Visitors: \\d+"],
  "ignoreSelectors": [".cookie-banner", ".ads"]
}
```

### What it compares

| Option | What it watches |
|---|---|
| **Visible text** (default) | The text a person sees, one line per paragraph, heading, list item or table row. Scripts, styles and hidden templates are ignored. |
| **HTML markup** | The HTML of the page or part, with comments, nonces, CSRF tokens and cache-buster query strings removed. Catches changed links and images too. |
| **An attribute** | One attribute of the selected elements, one value per line: `href` for a list of links, `src` for images, `data-price`, `content` of a meta tag. |

JSON responses are compared value by value (key order doesn't count as a change), and plain text line by line.

### Fewer false alerts

- **Noise filters.** Clock times and relative times ("2 hours ago", "yesterday") are ignored by default. You can also
  ignore dates or all numbers, add your own regular expressions, or remove parts of the page (cookie banners, ads,
  "related posts") with CSS selectors. Only the noisy part of a line is masked; the rest of it is still compared.
- **Double-check.** When a change is seen, the page is loaded again 3 seconds later. If it is back to the saved version
  (an A/B test, a rotating banner, a half-finished deploy), you don't get an alert.
- **Minimum change size.** Ignore changes smaller than a set number of characters. They still count toward the next
  alert.

### JavaScript pages

**Render JavaScript: auto** (the default) loads each page over plain HTTP first, which is fast and cheap. It opens the
page in a headless Chrome only when it needs to: when the page is blocked, when your selector isn't in the plain HTML,
or when the page is an empty JavaScript app shell. Once a page needs the browser, it keeps using it, so snapshots stay
comparable. Set **always** to render every page, or **off** to never use the browser.

**Screenshots** (optional) capture the watched part, or the first screen of the page. Each change links a **before**
and an **after** screenshot.

### Output

The dataset gets one row per **changed** page, plus rows for new baselines and for pages that couldn't be checked
(turn on **Save unchanged pages** to get a row for every page).

```json
{
  "url": "https://shop.example.com/widget",
  "label": "Widget price",
  "selector": ".price",
  "status": "changed",
  "changed": true,
  "summary": "1 line added, 1 line removed",
  "diff": "- $19.99\n+ $17.49",
  "added": ["$17.49"],
  "removed": ["$19.99"],
  "before": "$19.99",
  "after": "$17.49",
  "previousCheckAt": "2026-10-04T09:00:03.120Z",
  "checkedAt": "2026-10-05T09:00:02.871Z",
  "pageTitle": "Widget – Example Shop",
  "statusCode": 200,
  "method": "http",
  "screenshotBeforeUrl": null,
  "screenshotAfterUrl": null,
  "webhook": "sent",
  "warning": null,
  "errorCode": null,
  "error": null
}
```

**`status`** is one of:

| Status | Meaning |
|---|---|
| `changed` | The content differs from the last snapshot. The snapshot is updated and notifications are sent. |
| `baseline` | First check of this page (or of this selector): the snapshot was saved. |
| `unchanged` | No change (only saved to the dataset if you ask). |
| `selector-missing` | The page loaded, but nothing matched your selector. The snapshot is kept. If it used to match, you get one notification: either the part you watch was removed, or the page layout changed and the selector needs updating. |
| `blocked` | The site refused automated visitors (HTTP 401/403/429/503, a bot-check page) or its robots.txt disallows the page. |
| `error` | Timeout, DNS failure, HTTP error, or a response that isn't a web page, JSON or text. |

For `blocked` and `error` rows, `errorCode` and `error` say what happened in plain words. **The saved snapshot is never
overwritten by a failed check, and you aren't charged for it.** The run itself still succeeds, so changes on your other
pages are reported. If you'd rather have the run fail (to use Apify's failed-run alerts), turn on **Fail the run if any
page can't be checked**.

The run's `OUTPUT` record has a summary: how many pages changed, were unchanged, new, or couldn't be checked, and
the list of changed URLs.

### Notifications

- **Any webhook URL** gets a JSON `POST` per change with every field above, plus `event: "page.changed"` (or
  `"page.check_failed"`). Failed deliveries are retried, and each row records whether its webhook was `sent`. This is
  the tested path: generic webhooks have been delivered live.
- **Slack:** paste an incoming-webhook URL (`https://hooks.slack.com/...`) and each change is sent as a Slack-formatted
  message with the diff.
- **Discord:** paste a channel webhook URL (`https://discord.com/api/webhooks/...`) for a Discord-formatted message.
  The Slack and Discord message formats are unit-tested; delivery to a live Slack or Discord channel hasn't been tested
  by us yet.
- **Email (beta, not yet tested end to end):** one email per run listing every change, sent with Apify's own
  [Send Email](https://apify.com/apify/send-mail) actor from your account. Some developer-only Apify plans can't run
  other public actors; if so, the run summary says the email failed. A webhook to an email automation (Zapier, Make)
  is the reliable route for email today.

### How it behaves on the web

- One visit per page per run (plus a second look when a change is double-checked), and pages on the same site are
  loaded one at a time with a pause between them.
- It **respects robots.txt** for each page it checks. A page that robots.txt disallows is reported as `blocked`
  (`ROBOTS_DISALLOWED`) and isn't loaded.
- It identifies itself honestly with the user agent `WebsiteChangeMonitorBot`. It doesn't try to get around bot
  protection; a page behind a bot wall is reported as `blocked`.
- No logins and nothing behind a paywall: it reads what any visitor can see.

### More tools from oldjard

- [Tech Stack Detector](https://apify.com/oldjard/tech-stack-detector): what any list of websites is built with.
- [Sitemap URL Extractor](https://apify.com/oldjard/sitemap-url-extractor): every URL on a website, for RAG and SEO.
- [Shopify Products Scraper & Price Monitor](https://apify.com/oldjard/shopify-products-price-monitor): catalogs and price changes from any Shopify store.
- [Workday, Greenhouse, Lever & Ashby Jobs Scraper](https://apify.com/oldjard/ats-career-site-jobs): every open job from company career sites.
- [Bulk Website Screenshot & URL to PDF](https://apify.com/oldjard/screenshot-pdf): screenshots and PDFs of any list of pages.
- [AI Web Scraper (your own key)](https://apify.com/oldjard/ai-web-scraper): describe fields in English, get JSON.
- [Company Registry Lookup](https://apify.com/oldjard/company-registry-lookup): UK Companies House, Spain, France, Finland and Norway in one schema.
- [UK & EU Public Tenders](https://apify.com/oldjard/uk-eu-public-tenders): Find a Tender and TED notices in one table, with daily only-new alerts.

### Use it from an AI agent or the API

- **Minimal input:** `{"urls": ["https://www.python.org/downloads/"]}`. The first run saves a baseline; later runs with the
  same `monitorName` report what changed.
- **Cost:** $0.001 per page checked, plus $0.005 per change found and $0.002 per page opened in a browser. Failed
  checks are free. 10 pages checked once ≈ $0.01.
- **Run time (our runs):** 2–7 s for 1–3 pages fetched over plain HTTP; 20–23 s for 5 pages when one of them is
  opened in a browser.
- **Results:** the default dataset, one row per page check (`status`, `diff`, `before`/`after`); the run summary is
  the `OUTPUT` record in the key-value store.
- Works over the Apify MCP server (`search-actors`, then `call-actor`).

### FAQ

**Where are the snapshots kept?** In a key-value store in your Apify account named `change-monitor-<monitor name>`.
Only the latest snapshot per page is kept (and the latest two screenshots). Delete the store to start fresh.

**I changed the selector. What happens?** A new selector is a new check: the first run saves a new baseline for it.

**Can I watch hundreds of pages?** Yes. Pages run in parallel (5 at a time by default), and pages on the same site
take turns.

**Does it see content that loads after scrolling or clicking?** No. It sees the page as it first loads (after
JavaScript runs, in browser mode).

**Something it got wrong?** Open an issue on the Issues tab with the URL and selector, and what you expected.

# Actor input Schema

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

Pages to watch, one per line. Each run compares every page with what it saw last time. Bare domains like example.com/pricing work too.

## `selector` (type: `string`):

A CSS selector (like #price or .product-title) or an XPath (starting with /) for the part of the page to watch. Leave empty to watch all the text on the page. Applies to every URL above; use Pages (advanced) to give each page its own.

## `contentMode` (type: `string`):

Visible text catches what a person would notice. HTML also catches changes to links, images and markup. Attribute compares one attribute of the selected elements.

## `attribute` (type: `string`):

Only for What to compare: An attribute. For example href, src, content or data-price.

## `pages` (type: `array`):

Give each page its own settings. Each item: url (required), and optionally selector, contentMode, attribute, label (shown in alerts) and renderJavaScript. The same URL can appear more than once with different selectors.

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

Snapshots are saved under this name (in a key-value store called change-monitor-\<name>). Use a different name for each separate monitor or schedule, so they don't share snapshots.

## `renderJavaScript` (type: `string`):

Auto loads each page over plain HTTP first and opens it in headless Chrome only when the page is blocked, the selector isn't found, or the page is nearly empty without JavaScript. Once a page needs the browser it keeps using it, so snapshots stay comparable. Browser checks cost a little extra.

## `screenshots` (type: `boolean`):

Take a screenshot of the watched part (or the first screen of the page) and link before/after screenshots in each change. Uses the browser for every page.

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

How long to wait for a page.

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

How many pages to check in parallel. Pages on the same site are always checked one at a time.

## `ignoreNoise` (type: `array`):

These are masked before comparing, so a page whose only change is its clock or "posted 5 minutes ago" doesn't trigger an alert. The rest of each line is still compared.

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

Regular expressions for text to ignore, matched case-insensitively. Example: Visitors: \d+ or Ad #\d+.

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

CSS selectors for parts of the page to remove before comparing, like cookie banners, ads or a "related posts" box. Example: .cookie-banner, #ads, footer.

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

Ignore changes smaller than this many characters added plus removed. Small changes still count toward the next alert, because the baseline isn't updated until a change is reported.

## `confirmChanges` (type: `boolean`):

When a change is seen, look again 3 seconds later. If the page is back to the saved version (A/B tests, rotating banners), no alert is sent.

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

Where to send each change. Slack and Discord incoming-webhook URLs get a readable message; any other URL (Zapier, Make, n8n, your server) gets a JSON POST with the full diff.

## `notifyEmail` (type: `string`):

Beta, not yet tested end to end: send one email per run listing every change (comma-separate several addresses). Sent with Apify's own send-mail actor, which some developer-only Apify plans can't run. For reliable email alerts, use a webhook to Zapier or Make.

## `notifyOnErrors` (type: `boolean`):

Send a notification when a page is blocked, times out or returns an error. A watched part of a page that disappears is always reported, once.

## `saveUnchanged` (type: `boolean`):

By default the dataset only gets changes, new baselines and pages that couldn't be checked. Turn this on to get a row for every page.

## `failRunOnErrors` (type: `boolean`):

Mark the run as failed when a page is blocked or errors, so Apify's own failed-run alerts reach you. Off by default: changes on the other pages are still detected and reported either way.

## Actor input object example

```json
{
  "urls": [
    "https://www.iana.org/help/example-domains"
  ],
  "contentMode": "text",
  "pages": [],
  "monitorName": "default",
  "renderJavaScript": "auto",
  "screenshots": false,
  "requestTimeoutSecs": 30,
  "maxConcurrency": 5,
  "ignoreNoise": [
    "times",
    "relative-times"
  ],
  "ignorePatterns": [],
  "ignoreSelectors": [],
  "minChangeChars": 0,
  "confirmChanges": true,
  "notifyOnErrors": false,
  "saveUnchanged": false,
  "failRunOnErrors": false
}
```

# Actor output Schema

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

Dataset, one row per page check: url, label, selector, status (changed/unchanged/baseline/selector-missing/blocked/error), changed, summary, diff, added, removed, before, after, previousCheckAt, checkedAt, pageTitle, finalUrl, statusCode, method, screenshot links, webhook result, warning, errorCode, error.

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

Run summary (JSON): pages checked, changed, unchanged, baselines saved, blocked and failed, the changed URLs, the monitor name and its snapshot store.

# 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"
    ],
    "pages": [],
    "ignorePatterns": [],
    "ignoreSelectors": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("oldjard/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"],
    "pages": [],
    "ignorePatterns": [],
    "ignoreSelectors": [],
}

# Run the Actor and wait for it to finish
run = client.actor("oldjard/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"
  ],
  "pages": [],
  "ignorePatterns": [],
  "ignoreSelectors": []
}' |
apify call oldjard/website-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,oldjard/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/bBMuE8IRO5LS8OaD5/builds/VvFvucyDrYOZZVzYe/openapi.json
