# Website Change Monitor – Page Diff, Price & Keyword Alerts (`gazidev/website-change-monitor`) Actor

Monitor web pages for changes on a schedule: line-by-line diff, change %, CSS-selector targeting, price-drop and keyword alerts, webhook to Slack/Discord/n8n. HTTP only, robots.txt respected. Unchanged checks cost $0.0005.

- **URL**: https://apify.com/gazidev/website-change-monitor.md
- **Developed by:** [Cemal Atakli](https://apify.com/gazidev) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 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, Price & Keyword Alerts

**Monitor any web page for changes and get only what changed.** Give the Actor a list of URLs (optionally a CSS selector per page), schedule it, and every run compares the page with the previous snapshot. You get a **line-by-line diff (added / removed lines), the change percentage, the old and new price, watched keywords that appeared or disappeared**, and an optional **webhook to Slack, Discord, n8n, Make or Zapier**. A cheap, API-first **Visualping / Distill alternative** for competitor pricing, terms & policy pages, tender and job boards, government registers and product stock pages.

- **Watch only the part that matters:** a CSS selector (`#pricing`, `main`, `.product-info`, `table.results`) plus "ignore these elements" (`.related-posts`, `#footer`).
- **Real diffs, not just "something changed":** `addedLines`, `removedLines`, a unified `diff` and `changeRatio`.
- **Price-drop alerts:** price read from product meta tags / JSON-LD, a price selector, or the first amount with a currency. Output: `price`, `previousPrice`, `priceChange`, `priceChangePercent`.
- **Keyword alerts:** `watchFor: ["sold out", "deadline", "Series A"]` → counts per keyword, plus `keywordsAppeared` / `keywordsDisappeared`.
- **No false alarms from clocks and nonces:** times of day, "5 minutes ago", session tokens, ads and cookie banners are masked; dates optionally too; your own regex ignore patterns.
- **Noise threshold:** `minChangePercent` and "only alert on keywords or price" mode.
- **State that survives runs:** snapshots live in your named key-value store `website-change-monitor-<stateKey>`; one Actor task = one monitor.
- **Polite and cheap:** HTTP only (no browser), robots.txt respected (incl. Crawl-delay), one request per site at a time, conditional requests (ETag / Last-Modified), honest User-Agent.

### What can I use it for?

- **Competitor price and plan monitoring:** pricing pages, product pages, feature lists.
- **Compliance and legal:** terms of service, privacy policies, cookie policies, supplier codes of conduct.
- **Procurement and grants:** tender notice pages, "open calls" pages, funding programme pages.
- **Government and regulators:** registers, guidance pages, "last updated" pages (the prefill watches the UK register of licensed sponsors, which updates every working day).
- **Stock and availability:** "sold out" → "in stock" alerts with `watchFor`.
- **Jobs, events, changelogs:** be told when a careers page, an event page or a changelog gets a new entry.
- **AI agents:** a "has this page changed and how?" tool (see below).

### How it works

1. First run: each page is fetched, its text is saved as the baseline → row with `status: "new"`.
2. Every next run (schedule it hourly / daily in Apify): the page is fetched again and compared.
3. Every checked page gets a row: `status: "changed"` with the diff, or `"unchanged"`. Changes are also sent to your webhook. Turn `includeUnchanged` off to get a pure change feed (only new, changed and error rows).

### Input

| Field | Description |
|---|---|
| `pages` | URLs or objects `{"url", "selector", "label", "ignoreSelectors", "watchFor", "priceSelector", "trackPrice"}`; per-page settings override the global ones |
| `urls` | Simple alternative: a plain list of URLs |
| `selector` | Global CSS selector to watch (empty = whole page) |
| `ignoreSelectors` | Elements removed before comparing |
| `watchFor` | Keywords / phrases (case-insensitive) |
| `trackPrice`, `priceSelector` | Price tracking |
| `notifyOn` | `anyChange` (default) or `keywordsOrPrice` |
| `minChangePercent` | Ignore edits touching fewer lines than this % |
| `ignoreDynamicContent`, `ignoreDates`, `ignorePatterns` | Noise filters |
| `webhookUrl`, `webhookOnEveryRun` | POST a JSON summary when something changed |
| `stateKey` | Monitor name; snapshots go to the named store `website-change-monitor-<stateKey>` |
| `includeUnchanged` | Default on; off = only new / changed / error rows |
| `includeSnapshotText` | Add the full normalised text to each row |
| `maxItems`, `maxConcurrency`, `requestTimeoutSecs`, `proxyConfiguration` | Advanced |

```json
{
  "pages": [
    {"url": "https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers", "selector": "main", "label": "UK sponsor register"},
    {"url": "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html", "selector": ".product_main", "trackPrice": true}
  ],
  "watchFor": ["sold out", "Worker"],
  "minChangePercent": 0,
  "webhookUrl": "https://hooks.slack.com/services/XXX/YYY/ZZZ",
  "stateKey": "my-monitor"
}
```

### Output

One row per checked page (and one free row per error). The **Changes** and **Price & keyword alerts** tables are in the Output tab. Real result (shortened):

```json
{
  "url": "https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers",
  "label": "UK sponsors",
  "selector": "main",
  "title": "Register of licensed sponsors: workers - GOV.UK",
  "status": "changed",
  "changed": true,
  "checkedAt": "2026-10-27T10:38:39Z",
  "previousCheckAt": "2026-10-27T10:38:00Z",
  "changeRatio": 0.0002,
  "addedCount": 2,
  "removedCount": 0,
  "addedLines": ["27 October 2026", "Show all updates"],
  "removedLines": [],
  "diff": "@@ -9,2 +9,4 @@\n Last updated:\n+27 October 2026\n+Show all updates\n 27 October 2026",
  "price": null,
  "previousPrice": null,
  "keywordsFound": {"Worker": 25},
  "keywordsAppeared": [],
  "keywordsDisappeared": [],
  "charged": true,
  "error": null
}
```

A price change looks like `"price": 51.77, "previousPrice": 55.0, "priceChange": -3.23, "priceChangePercent": -5.87, "currency": "GBP"`.

`status` values: `new` (first check, baseline saved), `changed`, `minor-change` (changed, but below your threshold), `unchanged`, `error`. With `includeUnchanged` off, only `new`, `changed` and `error` rows are written. The run summary (counts, changed URLs, webhook status) is in the `OUTPUT` record of the default key-value store.

#### Webhook payload

```json
{"event": "website-change-monitor.run-finished", "text": "Website Change Monitor: 1 of 4 pages changed\n- https://...: 0.02% changed, price 55.0 -> 51.77",
 "content": "...same text (Discord)...", "runId": "...", "datasetUrl": "https://api.apify.com/v2/datasets/.../items?clean=true",
 "pagesChecked": 4, "changedCount": 1, "changes": [{"url": "...", "changeRatio": 0.0002, "price": 51.77, "previousPrice": 55.0, "addedLines": ["..."], "removedLines": []}]}
```

`text` makes Slack incoming webhooks work as-is, `content` does the same for Discord.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Page checked (fetched and compared, changed or not) | **$0.0005** |
| Change detected (extra, only when a page changed) | **$0.002** |
| Errors, blocked pages, robots.txt-disallowed pages, invalid input | free |

Watching 20 pages once a day for a month = 600 checks = **$0.30**, plus $0.002 per real change.

| Actor (Oct 2026, public Store prices) | Price |
|---|---|
| **This Actor** | $0.0005 per check + $0.002 per change |
| ryanclinton/website-change-monitor | $0.10 per site (free tier) down to $0.018 |
| kondasviktor (page monitor) | $0.0025 per page |
| mrlarryjohnson (change monitor) | $0.002 per URL |
| muhammad-bilal/web-drift-detector | $0.0006 + $0.0002 per page |

### Use with AI agents / Apify MCP

Add the Actor as a tool through the [Apify MCP server](https://mcp.apify.com) (`gazidev/website-change-monitor`). An agent can call it with `{"pages": [{"url": "...", "selector": "..."}], "stateKey": "agent-watch"}` and read `status`, `addedLines`, `removedLines` and `priceChange` to answer "what changed on this page since last time?". The API works the same: `POST https://api.apify.com/v2/acts/gazidev~website-change-monitor/run-sync-get-dataset-items`.

### FAQ

**How do I run it every hour / day?** Create a task with your pages, then add a Schedule in Apify Console. Each run compares with the previous one through the `stateKey` store.

**How do I reset a monitor?** Use a new `stateKey`, or delete the named key-value store `website-change-monitor-<stateKey>` in Storage.

**Does it work on JavaScript-only pages?** No, it uses plain HTTP (that keeps it cheap). If the text you want is rendered in the browser, the row says the selector matched nothing or the page has no readable text, and you are not charged.

**Which sites are not supported?** Social networks and marketplaces whose terms forbid automated access (LinkedIn, Facebook, Instagram, TikTok, X, Amazon, eBay, Google...) are refused. Pages disallowed by robots.txt are skipped. You are responsible for having the right to monitor the pages you add.

**Why was a page reported as changed when nothing visible changed?** Some sites print counters or rotating content. Add the element to `ignoreSelectors`, add a regex to `ignorePatterns`, or set `minChangePercent`.

**Is the change percentage the share of text changed?** It is (added + removed lines) / (old + new lines).

# Actor input Schema

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

List of pages. Each item is a URL string or an object: `{"url": "https://site.com/pricing", "selector": "main", "label": "Competitor pricing", "ignoreSelectors": [".banner"], "watchFor": ["sold out"], "priceSelector": ".price", "trackPrice": true}`. Per-page settings override the global ones below.

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

Simple alternative to 'Pages to monitor': one URL per line, using the global settings below.

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

Only watch this part of the page, e.g. `main`, `#pricing`, `.product-info`, `table.results`. Empty = whole page body. Navigation, ads and cookie banners are always dropped.

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

CSS selectors removed before comparing, e.g. `.related-posts`, `#footer`, `.timestamp`.

## `watchFor` (type: `array`):

Case-insensitive words or phrases. The output lists how often each appears, and which keywords appeared or disappeared since the last run (e.g. `sold out`, `in stock`, `deadline`, `tender`).

## `trackPrice` (type: `boolean`):

Extract a price (from product meta tags / JSON-LD, otherwise the first amount with a currency in the watched text) and report old price, new price and % change.

## `priceSelector` (type: `string`):

Optional, more exact than auto-detection: the element that holds the price, e.g. `.price-current`. Turns price tracking on.

## `notifyOn` (type: `string`):

Controls which changes count as a 'changed' row, the change-detected event and the webhook.

## `minChangePercent` (type: `integer`):

Ignore tiny edits: a change touching fewer lines than this % of the page is a 'minor-change' (not reported, not charged as a change). Price and keyword changes are always reported.

## `ignoreDynamicContent` (type: `boolean`):

Masks times of day, relative times and long random tokens (nonces, session IDs) so they do not trigger false alerts.

## `ignoreDates` (type: `boolean`):

Mask calendar dates (2026-10-27, 27 October 2026, 10/27/2026). Leave off when a new date is the change you want to see.

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

Regular expressions removed from the text before comparing, e.g. `Visitors today: \d+`.

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

POSTed once per run when something changed, with a JSON list of changes. The payload has `text` and `content` fields, so Slack and Discord incoming webhooks show it directly; n8n, Make and Zapier get the full JSON.

## `webhookOnEveryRun` (type: `boolean`):

Useful as a heartbeat.

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

On (default): one row per checked page, so every run shows its result (filter on `changed` / `status`). Turn off for a pure change feed: only new baselines, changes and errors are written.

## `includeSnapshotText` (type: `boolean`):

Adds the normalised text of the watched part (up to 400k characters) to every row.

## `stateKey` (type: `string`):

Snapshots are kept in your named key-value store `website-change-monitor-<name>`, so every scheduled run compares with the previous one. Use a different name per task to keep monitors apart, or a new name to start fresh.

## `maxItems` (type: `integer`):

Maximum number of pages checked in one run. Extra pages are skipped.

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

Pages on the same website are always fetched one at a time with at least 1 s between requests (or the robots.txt crawl-delay).

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

Per request. Slow servers are retried up to 2 times.

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

Not needed for most sites. Datacenter proxy only helps when a site rate-limits by IP.

## Actor input object example

```json
{
  "pages": [
    {
      "url": "https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers",
      "selector": "main"
    }
  ],
  "watchFor": [
    "Worker"
  ],
  "trackPrice": false,
  "notifyOn": "anyChange",
  "minChangePercent": 0,
  "ignoreDynamicContent": true,
  "ignoreDates": false,
  "webhookOnEveryRun": false,
  "includeUnchanged": true,
  "includeSnapshotText": false,
  "stateKey": "default",
  "maxItems": 5,
  "maxConcurrency": 5,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `alerts` (type: `string`):

No description

## `results` (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 = {
    "pages": [
        {
            "url": "https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers",
            "selector": "main"
        }
    ],
    "watchFor": [
        "Worker"
    ],
    "maxItems": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("gazidev/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 = {
    "pages": [{
            "url": "https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers",
            "selector": "main",
        }],
    "watchFor": ["Worker"],
    "maxItems": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("gazidev/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 '{
  "pages": [
    {
      "url": "https://www.gov.uk/government/publications/register-of-licensed-sponsors-workers",
      "selector": "main"
    }
  ],
  "watchFor": [
    "Worker"
  ],
  "maxItems": 5
}' |
apify call gazidev/website-change-monitor --silent --output-dataset

```

## MCP server setup

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