# Website Change Monitor: Diffs, Prices & Alerts (`offerastudio/website-change-monitor`) Actor

Monitor up to 500 web pages and get a row only when one changes: added and removed lines, change size, the full diff, and a rule-based summary such as "price text changed from €49 to €59". Main-content detection or a CSS selector, ignore patterns, schedules and alerts.

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

## Pricing

from $1.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

### What does Website Change Monitor do?

**Website Change Monitor** watches a list of web pages and gives you **a row only when a page changes**, with a **clean diff** and a **short plain-English summary** of what changed. Add up to 500 URLs, schedule it daily or hourly, and connect alerts by email, Slack or webhook.

For every change you get:

- 🔗 the **URL**, when it was **checked** and when it was **checked before**
- 📏 the **change size** in characters
- ➕➖ the **added and removed lines** (first 50 of each) and a **link to the full diff**
- 📝 a **rule-based summary** (no AI, no guessing), for example *"Price text changed from €49 to €59 under 'Pro'"*, *"3 lines added under 'Pricing'"*, *"Availability text changed from 'in stock' to 'sold out'"*
- 💶 **price, number and date changes** as structured data (`priceChanges`, `numberChanges`)

The first run saves a **baseline** of every page (one "baseline saved" row each). From then on, only changes produce rows.

### What can you monitor?

- **Competitor pricing pages**: get "Price text changed from $19 to $24" the morning it happens.
- **Terms of service and privacy policies**: see which clauses were added or removed, under which heading.
- **Job and careers pages**: new openings show up as "2 lines added under 'Open positions'".
- **Tender and procurement notices**: new tenders, changed deadlines and amended documents.
- **Product availability**: "in stock" → "sold out", "notify me" → "add to cart", price drops.
- Also: regulatory pages, documentation, status pages, school and council notices, real estate listings.

### How to use it

1. Paste the pages into **URLs to monitor** (or upload a text file with one URL per line).
2. Optional: a **CSS selector** to compare only part of each page, e.g. `.pricing-table` or `#terms`. Without one, the Actor finds the **main content** itself and leaves out navigation, header, footer, sidebars, cookie banners, scripts and hidden elements.
3. Optional: **ignore patterns** for text that changes on every visit (see below), **Ignore dates and times**, and a **minimum change size**.
4. Turn **Include unchanged pages** off (it is on in the example input only so a first try shows a row for every page).
5. Click **Start**: the first run saves the baselines. Then add a **schedule** (Console → Schedules), e.g. every day at 7:00 or every hour.

#### Ignore patterns: dates, counters and other noise

Patterns are regular expressions, matched case-insensitively against each line. Matching text is ignored when comparing (it still appears in the diff).

| To ignore | Pattern |
| --- | --- |
| View or visitor counters | `\d[\d,]* (views\|visitors\|comments)` |
| "Last updated" lines | `last updated:? .*` |
| Session or tracking IDs | `session id: \w+` |
| Prices you don't care about, in a list | `\$\d+(\.\d\d)? shipping` |

**Ignore dates and times** covers dates (2026-10-01, 10/01/2026, 1 October 2026), clock times and "5 minutes ago".

**Minimum change size** skips changes smaller than that many characters. Smaller changes are not lost: they are compared against the saved version until, together, they reach the minimum.

### Alerts: email, Slack or webhook on every change

Save your input as a **task**, add a schedule, then pick one of these in the task's **Integrations** tab or in your automation tool:

- **Slack (built in):** add the Slack integration for the event **Run succeeded**. Each run posts one message; the default text can be replaced with a template, for example `{{resource.statusMessage}} https://console.apify.com/storage/datasets/{{resource.defaultDatasetId}}`. The status message reads like "Checked 50 page(s), 2 changed", so you see at a glance whether anything moved.
- **Email or Slack only when something changed (Zapier or Make):** trigger **Finished Task Run** (Zapier) or **Watch Task Runs** (Make), then **Get Dataset Items** for the run's dataset, then continue only if there are items with `status` = `changed`, and send `url`, `summary` and `diffUrl` by email or to Slack.
- **Webhook:** add a webhook for **Run succeeded**. Your endpoint receives the run in `resource`, reads `https://api.apify.com/v2/datasets/{defaultDatasetId}/items?view=overview` and alerts when it has rows.

Because the dataset only gets rows when something changed, "the run's dataset is not empty" is the alert condition. Keep **Include unchanged pages** off, and filter on `status` = `changed` if you don't want the one-time baseline rows or the free error rows.

### Input example

```json
{
    "startUrls": [
        { "url": "https://competitor.example/pricing" },
        { "url": "https://supplier.example/terms" }
    ],
    "cssSelector": "",
    "ignorePatterns": ["\\d[\\d,]* (views|visitors)"],
    "ignoreDatesAndTimes": true,
    "minChangeChars": 0,
    "includeUnchanged": false,
    "monitorName": "competitors"
}
```

### Output example

A real change row from a local run against the Hacker News front page, checked twice 40 seconds apart (shortened; locally `diffUrl` is a `file://` link, on Apify it points to the record in your key-value store). A busy page like this changes all the time; an ignore pattern such as `\d+ (points|comments)` or a CSS selector would keep only the changes you care about:

```json
{
    "url": "https://news.ycombinator.com/",
    "status": "changed",
    "summary": "Number changed from 35 to 38 in \"185 points by Snowly 2 hours ago | hide | 38 comments\"; Number changed from 10 to 12 in \"28 points by trickypr 1 hour ago | hide | 12 comments\"; Number changed from 36 to 37 in \"83 points by giuliomagnifico 3 hours ago | hide | 37 commen…\"; Number changed from 154 to 156 in \"156 points by sagacity 6 hours ago | hide | 88 comments\"; Numbers changed in \"78 points by speckx 51 minutes ago | hide | 30 comments\": 73 → 78, 49 → 51; and 12 more changes.",
    "monitorName": "readme demo",
    "checkedAt": "2026-10-01T13:47:48.417Z",
    "previousCheckedAt": "2026-10-01T13:47:08.815Z",
    "previousVersionAt": "2026-10-01T13:47:08.815Z",
    "changeSize": 763,
    "addedLinesCount": 20,
    "removedLinesCount": 20,
    "addedLines": [
        "185 points by Snowly 2 hours ago | hide | 38 comments",
        "28 points by trickypr 1 hour ago | hide | 12 comments",
        "…"
    ],
    "removedLines": [
        "185 points by Snowly 2 hours ago | hide | 35 comments",
        "28 points by trickypr 1 hour ago | hide | 10 comments",
        "…"
    ],
    "changeTypes": ["number", "text-changed", "text-added", "text-removed"],
    "priceChanges": [],
    "numberChanges": [
        { "from": "35", "to": "38", "context": "185 points by Snowly 2 hours ago | hide | 38 comments", "section": null },
        "…"
    ],
    "diffUrl": "https://api.apify.com/v2/key-value-stores/<store id>/records/diff-0f63a2a5a5620b745938e6a2-20261001134748",
    "title": "Hacker News",
    "httpStatus": 200,
    "contentMode": "main-content",
    "contentRoot": "body",
    "contentLines": 93,
    "error": null
}
```

On a pricing page the summary reads like *"Price text changed from €49 to €59 under 'Pro' ("€59 per month"); 3 lines added under 'Pro'"* and `priceChanges` holds `[{ "from": "€49", "to": "€59", "context": "€59 per month", "section": "Pro" }]` (from the tests in `test/`).

The first check of a page (real row, example.com):

```json
{
    "url": "https://example.com/",
    "status": "baseline",
    "summary": "Baseline saved (first check of this page). Changes are reported from the next run.",
    "checkedAt": "2026-10-01T13:40:36.887Z",
    "previousCheckedAt": null,
    "title": "Example Domain",
    "httpStatus": 200,
    "contentMode": "main-content",
    "contentRoot": "body",
    "contentLines": 2
}
```

`status` is one of `baseline`, `changed`, `unchanged` (only with **Include unchanged pages**) and `error`. The **Price changes** tab lists every price that changed, appeared or disappeared.

#### Errors are free

A page that can't be checked gets a free row with `status: "error"`, an `error` code and a plain-English `errorMessage`, and its saved version is **not** touched, so the next successful check still compares against it:

| `error` | Meaning |
| --- | --- |
| `dns-not-found`, `connection-failed`, `timeout`, `tls-error` | The site could not be reached. |
| `http-error` | The page answered 404, 500 … (429 and 5xx are retried first). |
| `blocked` | The site needs a login or refuses automated visits (401, 403, bot challenge). |
| `not-text`, `too-large` | Not a web page (e.g. a PDF), or larger than 5 MB. |
| `selector-not-found` | Your CSS selector matched nothing on this page. |
| `empty-content` | No visible text: the page probably builds its content with JavaScript. |
| `invalid-url`, `invalid-input` | A URL or ignore pattern in the input can't be used. |

### How much does it cost?

This Actor uses **pay per event**:

| Event | Price |
| --- | --- |
| Page checked (fetched and compared, changed or not) | **$0.001** per page |
| Pages that fail to load, error rows | **free** |

- 100 pages checked daily cost **$0.10 a day** (about $3 a month); hourly checks of 10 pages cost **$0.24 a day**.
- The first (baseline) check of a page costs the same $0.001.
- Apify also charges a tiny standard start fee per run, and the snapshots use a little key-value storage in your account.
- Set **Maximum cost per run** in the run options and the Actor stops when it is reached.

### Where the history is kept

Snapshots live in a **named key-value store** in your Apify account, called `change-monitor-<monitor name>` (or `change-monitor-<hash of your URLs>` without a name). Named stores are kept until you delete them, so every scheduled run remembers the previous one. Each page keeps its current version and the latest 20 full diffs.

- Same **Monitor name** = same history, even if you add or remove URLs (new URLs just get a baseline).
- Without a name, changing the URL list or selector starts a fresh monitor. Set a name for monitors you edit over time.
- Changing the **CSS selector** saves a new baseline for each page (rows say so) instead of reporting the whole page as changed.
- To start over, delete the store in **Storage → Key-value stores**.

### Limitations

- **No JavaScript rendering.** Pages are fetched over plain HTTP, like a search engine sees them. Content that only appears after scripts run (many single-page apps) gives an `empty-content` error or misses those parts; a CSS selector on server-rendered parts often helps.
- **Text only.** Images, styles and layout changes are not compared; image alt text is not either.
- Pages behind a login, a paywall or bot protection can't be checked.
- One request per page per run, one at a time per website, with retries; up to 500 URLs per run.
- The summary is rule-based: it recognises prices in common formats (€49, $1,299.00, 49 EUR, 129,90 zł), numbers, dates and availability words in English. Other changes are described as lines added, removed or changed under the nearest heading.

### FAQ

#### Why did my first run return a row for every page?

The first check of each page saves its baseline and says so. Changes are reported from the next run on.

#### Why do I get no rows?

Nothing changed since the last check. That is the point: rows only appear when a page changes (or turn on **Include unchanged pages** to see every check).

#### How do I watch only the price on a product page?

Use a CSS selector such as `.price, .availability`. Only those elements are compared.

#### A clock or counter on the page reports a change every time.

Turn on **Ignore dates and times**, add an **ignore pattern** for the counter, or set a **minimum change size**.

#### Is my data shared?

No. Snapshots and diffs stay in your own Apify storage.

### More tools from the same developer

All pay-per-result, no proxy or login needed, built and maintained by the same developer:

**Website audits**

- [Website Accessibility Checker: WCAG 2.2 & EAA](https://apify.com/offerastudio/website-accessibility-audit): accessibility issues with fixes, SEO basics and security headers.
- [Cookie & Tracker Audit: GDPR Consent Checker](https://apify.com/offerastudio/cookie-tracker-audit): cookies and tracking tags that load before consent.
- [AI Crawler Access Checker: robots.txt & llms.txt](https://apify.com/offerastudio/ai-crawler-access-audit): which AI crawlers a site allows, plus llms.txt.

**Company data and compliance**

- [Company Contact Finder: Emails, Phones & Socials](https://apify.com/offerastudio/company-contact-finder): contact details published on company websites.
- [UK New Companies Feed: Companies House Daily](https://apify.com/offerastudio/uk-new-companies-feed): newly incorporated UK companies with sector filters.
- [EU VAT Number Validator: Bulk VIES Checker](https://apify.com/offerastudio/eu-vat-number-validator): bulk VAT checks with name, address and consultation number.
- [LEI Corporate Tree: GLEIF Parents & Subsidiaries](https://apify.com/offerastudio/gleif-lei-corporate-tree): LEI lookup with parents, subsidiaries and a KYC summary.

**Market signals**

- [US WARN Layoff Notices: 12 States Daily Feed](https://apify.com/offerastudio/us-warn-layoff-notices): layoff and plant closure notices from official state sources.
- [US Product Recalls Monitor: FDA & CPSC Feed](https://apify.com/offerastudio/us-product-recalls-monitor): FDA and CPSC recalls in one feed, with severity.

### Feedback

Missing a rule in the summary, or a page type that doesn't work well? Open an issue on the **Issues** tab.

# Changelog

This Actor's version history is a separate document: https://apify.com/offerastudio/website-change-monitor/changelog.md

# Actor input Schema

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

Pages to watch, up to 500 per run. Each page is fetched once per run and compared with the version saved by the previous run.

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

Compare only the elements matching this selector on every page, e.g. .pricing-table, #terms or main article. Leave empty to detect the main content automatically: navigation, header, footer, sidebars, cookie banners, scripts and hidden elements are left out.

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

Text matching these regular expressions (case-insensitive) is ignored when comparing, e.g. \d+ (views|comments) for counters, Last updated:.\* for timestamps, or a session ID pattern. Up to 30.

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

Ignore dates (2026-10-01, 10/01/2026, 1 October 2026), clock times and "5 minutes ago" texts, so pages that only show today's date don't count as changed.

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

Report a change only when at least this many characters changed (0 = every change). Smaller changes are not lost: they add up against the saved version until they reach the minimum.

## `includeUnchanged` (type: `boolean`):

Also add a row for pages that did not change. Off by default, so the dataset (and your alerts) only get rows when something changed. It is switched on in the example input only so a first try shows a row for every page; turn it off for monitoring.

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

Name of this monitor's snapshot store (letters, digits, spaces, - and \_). Runs with the same name share saved versions. Leave empty to derive it from the URL list and selector; then adding or removing a URL starts a new monitor with new baselines. Set a name to keep history when you edit the list.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://example.com"
    },
    {
      "url": "https://www.iana.org/help/example-domains"
    }
  ],
  "ignoreDatesAndTimes": false,
  "minChangeChars": 0,
  "includeUnchanged": true
}
```

# Actor output Schema

## `changes` (type: `string`):

No description

## `prices` (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": [
        {
            "url": "https://example.com"
        },
        {
            "url": "https://www.iana.org/help/example-domains"
        }
    ],
    "includeUnchanged": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("offerastudio/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 = {
    "startUrls": [
        { "url": "https://example.com" },
        { "url": "https://www.iana.org/help/example-domains" },
    ],
    "includeUnchanged": True,
}

# Run the Actor and wait for it to finish
run = client.actor("offerastudio/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 '{
  "startUrls": [
    {
      "url": "https://example.com"
    },
    {
      "url": "https://www.iana.org/help/example-domains"
    }
  ],
  "includeUnchanged": true
}' |
apify call offerastudio/website-change-monitor --silent --output-dataset

```

## MCP server setup

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