# Website Change Monitor — Learns What to Ignore (`drkiwi/website-change-detection`) Actor

Website change monitor that learns which parts of a page are noise — clocks, counters, A/B tests — and never alerts or bills you for them.

- **URL**: https://apify.com/drkiwi/website-change-detection.md
- **Developed by:** [Kevin Baldassari](https://apify.com/drkiwi) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $120.00 / 1,000 real change detecteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Website Change Monitor — Learns What to Ignore

Every website change monitor can tell you a page changed. The hard part is telling you when it **matters**. Pages carry clocks, view counters, "posted 3 hours ago", rotating testimonials and A/B variants — and a monitor that diffs raw content fires on all of them. You get an alert channel nobody reads, and under pay-per-event pricing, a bill for every false alarm. The usual fix makes it your problem: declare the noisy parts up front with ignore patterns or CSS selectors, which requires knowing on day one exactly which parts will turn out to be noisy.

**This Actor learns it instead.** It watches how each region of your page behaves across runs, works out which ones change on their own, and stops reporting them — automatically, per page, with no configuration. You are never billed for a change it suppressed, and the learning period itself is free.

### What it does

- **Monitor any website for content changes** — give it a URL, put it on a schedule, get told when something real happens.
- **Automatically ignores timestamps, counters and rotating content** — no ignore rules to write, no CSS selectors to hunt down.
- **Detects word-level changes**, so a price moving from $29 to $39 is reported as those two words, not as an entire paragraph replaced.
- **Tracks changes to pricing pages, terms of service, competitor sites and documentation** with a signed webhook per confirmed change.
- **Filters A/B tests and rotating content** by optionally requiring a change to survive several consecutive checks.
- **Never charges for noise, baselines, unchanged pages or failed fetches.**

### Who it's for

- **Competitive intelligence and product teams** — know the hour a rival changes their pricing page, not the week.
- **Legal, compliance and procurement** — get told when a supplier quietly edits their terms of service or SLA.
- **SEO and content teams** — catch silent edits to title tags, canonical URLs and on-page copy across a site.
- **Developers and integrators** — watch an API changelog or docs page and fire a webhook into your own automation.

### How it works

1. **Add your URLs.** A plain list is enough. Everything else has a working default.
2. **Put it on a schedule.** Save your input as a Task, then **Task → Schedules → Create schedule**. Daily suits most pages; hourly for fast-moving ones. The Actor does not schedule itself.
3. **Get told only when something real changes.** Confirmed changes land in the dataset and fire your webhook. Noise does not.

#### The first few runs

The first run on a URL records a **baseline** — there is nothing to compare against yet, so nothing is reported and nothing is charged.

The next few comparisons are the **learning window**. During it the Actor reports everything it sees, including noise, because it cannot yet tell the two apart. **Those runs are free.** By default it has made up its mind after three comparisons, and from then on the noisy regions go quiet.

You can watch this happen in the output: `volatileSlots` counts the noisy regions it has identified, and `learningComplete` flips to `true` when its map is trusted.

### Input

```json
{
  "urls": [
    { "url": "https://example.com/pricing", "label": "Competitor pricing" },
    { "url": "https://example.com/terms", "label": "Terms of service" }
  ],
  "mode": "text",
  "webhookUrl": "https://hooks.example.com/website-changed"
}
```

| Field | Type | Default | What it does |
|---|---|---|---|
| `urls` | array | — | Pages to watch. Plain strings, or objects with `url`, `label` and an optional per-page `selector`. |
| `mode` | string | `text` | `text` compares visible text. `selector` compares one CSS selector. `html` compares raw markup. |
| `selector` | string | — | Required when `mode` is `selector`, e.g. `.pricing-table`. |
| `ignoreSelectors` | array | `[]` | Elements removed before comparing. Optional — skips the learning period for regions you already know are noisy. |
| `ignorePatterns` | array | `[]` | Regexes blanked before comparing, e.g. `Last updated: .*`. |
| `learningRuns` | integer | `3` | Comparisons observed before a region can be judged as noise. These are free. |
| `volatilityThreshold` | number | `0.6` | How often a region must change to count as noise. |
| `minChangedWords` | integer | `1` | Changes smaller than this are treated as noise. |
| `confirmRuns` | integer | `1` | Raise to require a change to survive N consecutive checks. Filters A/B tests. |
| `webhookUrl` | string | — | POST fired per confirmed change. |
| `webhookSecret` | string | — | Signs each POST so your endpoint can verify it. |
| `stateStoreName` | string | `website-change-state` | Where snapshots live. Schedules sharing memory must share this name. |
| `includeUnchanged` | boolean | `false` | Also write a row for quiet checks, for a full audit trail. |

### Output

One dataset row per check. A confirmed change looks like this:

```json
{
  "url": "https://example.com/pricing",
  "label": "Competitor pricing",
  "changeType": "changed",
  "changed": true,
  "charged": true,
  "similarity": 0.9231,
  "changedPercent": 7.69,
  "addedText": ["The Pro plan costs $39 per month."],
  "removedText": ["The Pro plan costs $29 per month."],
  "wordsAdded": 1,
  "wordsRemoved": 1,
  "noiseLinesMasked": 2,
  "suppressedSegments": 1,
  "volatileSlots": 2,
  "learningComplete": true,
  "httpStatus": 200,
  "responseTimeMs": 412,
  "timesChanged": 3,
  "checkedAt": "2026-08-07T09:00:04.881Z"
}
```

`changeType` is one of `baseline`, `unchanged`, `changed`, `noise-only`, `pending` or `error`. `charged` tells you plainly whether that row cost you anything. `noiseLinesMasked` and `suppressedSegments` are the regions that were ignored — the alerts you did not get and did not pay for.

### Use cases

#### Monitor a competitor's pricing page for changes

Point it at every rival's pricing page on a daily schedule. Those pages are full of noise — live chat widgets, "X people are viewing this", rotating customer logos — which is exactly what gets suppressed. When a plan price genuinely moves, you get the words that changed.

#### Track terms of service and SLA updates

Suppliers change terms quietly, and the "last updated" date is often the only visible sign — and is itself noise on many pages. Watch the terms URL with `confirmRuns: 2` so a staging deploy does not alert you, and route the webhook where legal or procurement will actually see it.

#### Watch API documentation and changelogs for breaking changes

Docs pages are usually server-rendered and stable, which makes them ideal targets. Use `mode: "selector"` on the main content to skip navigation and version pickers, and get a word-level diff of the endpoint you depend on.

#### Detect silent SEO and on-page content edits

Run in `html` mode against key landing pages to catch changes to meta tags, canonical URLs and structured data that visible-text monitoring would miss.

### Pricing

Pay-per-event. Checking is nearly free; you pay when the Actor hands you something worth knowing.

| Event | Price | Fires |
|---|---|---|
| `actor-start` | $0.005 | Once per run |
| `page-checked` | $0.0005 | Per page successfully checked |
| `change-detected` | $0.25 | Per confirmed real change, after learning |

**Worked example — 10 pages, checked daily for a month:**
30 runs × $0.005 = $0.15, plus 300 checks × $0.0005 = $0.15, plus, say, 12 genuine changes × $0.25 = $3.00. **About $3.30 for the month.**

**50 pages, daily:** roughly $11 a month. **100 pages, hourly:** roughly $120 a month.

Volume discounts apply automatically on higher Apify plans.

**You are not charged for:** the first-run baseline · any change reported during the learning window · anything suppressed as noise · unchanged pages · failed fetches, timeouts and 404s · changes withheld because you hit your own spending limit.

That last one matters: if your cap is reached mid-run, the undelivered changes are held back from the stored baseline too, so the next run reports them again instead of losing them.

### Limitations

Stated plainly, because finding out later is worse:

- **No JavaScript rendering.** Raw HTML only. If a page builds itself in the browser the Actor says so in `jsWarning` rather than silently monitoring an empty shell — monitor a server-rendered page or an API endpoint instead.
- **No authentication.** Public pages only.
- **No visual monitoring.** It compares content, not screenshots or layout.
- **Learning needs runs, not time.** A page checked weekly takes three weeks to settle.
- **A wholesale redesign re-baselines the page** and learning restarts.

### FAQ

#### Why not just diff the page myself?

The diffing is the easy part. What costs weeks is everything around it: recognising that "23 people viewing" is noise but the price line is not, keeping snapshots alive between scheduled runs, not re-reporting a whole page when a site briefly returns a partial response, and not alerting on an A/B test that flips back tomorrow.

#### How is this different from other change monitors on the store?

Others detect changes and ask you to declare the noise up front. This one learns it per page and never bills you for it. It also diffs at word level rather than line level.

#### Will it wake me up for a timestamp?

Only during the free learning window, and only until it has seen the timestamp move a few times. After that it goes quiet on it permanently — unless the region genuinely settles down, in which case it starts listening again.

#### What if a site is temporarily down?

The check is reported as an error and the stored baseline is left completely untouched. When the site comes back, the next run compares against the last good content — not against nothing — so you do not get a false "the whole page changed" alert.

#### How do I verify the webhook is really from this Actor?

Set `webhookSecret`. Each POST carries `x-wcm-timestamp` and `x-wcm-signature`, where the signature is `sha256=` followed by an HMAC-SHA256 over `<timestamp>.<body>` using your secret. Compare with a constant-time equality check, and reject timestamps older than a few minutes.

#### Can I monitor different sets of pages independently?

Yes. Give each watchlist its own `stateStoreName`. Snapshots and learned noise are kept per store, so the lists never interfere.

### Support

Open an issue on the **Issues** tab. Response within one working day.

# Actor input Schema

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

Pages to watch. Either a plain list of addresses, or objects with url, an optional label used in output and webhooks, and an optional per-page CSS selector. Duplicate URLs are removed so one page is never charged twice in a run.

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

text compares visible page text and is the right default for almost everything. selector compares only the part matching your CSS selector. html compares raw markup and is the most sensitive — use it only when you need to catch attribute or tag changes.

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

Required when mode is 'selector'. The part of the page to compare, e.g. '.pricing-table' or 'main'. If it matches nothing the page is reported as an error rather than silently compared as empty.

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

CSS selectors removed before comparing — ad slots, cookie banners, 'related posts' rails. Optional: the Actor learns recurring noise by itself, this just skips the learning period for regions you already know about.

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

JavaScript regular expressions whose matches are blanked before comparing, e.g. 'Last updated: .\*' or 'Visitors: \d+'. Timestamps, clocks, UUIDs, session tokens and cache-busting URLs are already handled automatically and do not need to be listed.

## `learningRuns` (type: `integer`):

How many comparisons the Actor observes before it will judge a region as noise. Lower learns faster but risks silencing a region that only looked noisy; higher is more cautious. These comparisons are free — you are not charged for changes reported during the learning window.

## `volatilityThreshold` (type: `number`):

Share of observed comparisons in which a region must change before it is treated as noise. 0.6 means 'changes in at least 6 of every 10 checks'. Raise it towards 1 to suppress only relentlessly noisy regions; lower it to be more aggressive.

## `minChangedWords` (type: `integer`):

Changes smaller than this are treated as noise: not reported, not charged. Leave at 1 to catch single-word edits such as a price. Raise it if you only care about substantial rewrites.

## `confirmRuns` (type: `integer`):

Set above 1 to require a change to appear identically on this many consecutive checks before it is reported. This is how you filter A/B tests and rotating content, which otherwise flip back and forth. Costs you one check of delay per extra run.

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

If set, a POST fires for each confirmed change. Never fires for noise, baselines or unchanged pages.

## `webhookSecret` (type: `string`):

If set, each POST carries an x-wcm-signature header: sha256 HMAC over '<timestamp>.<body>' using this secret, with the timestamp in x-wcm-timestamp. Lets your endpoint prove the request came from this Actor and has not been replayed.

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

Off by default so the dataset contains only things that happened. Turn on if you want a complete audit trail of every check, including the quiet ones.

## `stateStoreName` (type: `string`):

Where page snapshots and learned noise are kept between runs. Every schedule that should share memory must use the same name. Use different names to keep separate watchlists independent.

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

Raise for large watchlists, lower if a target site rate-limits you.

## `timeoutSecs` (type: `integer`):

How long to wait for one page before giving up on it. A timed-out page is reported as an error and leaves its baseline untouched, so nothing is lost.

## `disableUniversalNoise` (type: `boolean`):

Advanced. Turns off automatic handling of timestamps, clocks, relative times, UUIDs, CSRF tokens and cache-busting URLs. Only enable if you specifically need to detect changes in those values.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://news.ycombinator.com/",
      "label": "Hacker News front page"
    }
  ],
  "mode": "text",
  "ignoreSelectors": [],
  "ignorePatterns": [],
  "learningRuns": 3,
  "volatilityThreshold": 0.6,
  "minChangedWords": 1,
  "confirmRuns": 1,
  "includeUnchanged": false,
  "stateStoreName": "website-change-state",
  "maxConcurrency": 5,
  "timeoutSecs": 45,
  "disableUniversalNoise": false
}
```

# Actor output Schema

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

One row per page checked. Rows with changeType 'changed' are confirmed real changes — those are the ones that fired a webhook and cost you an event. Rows marked 'noise-only' are movements suppressed as learned noise and were free.

## `changesView` (type: `string`):

The same rows through the 'changes' dataset view: detection time, page, percentage changed, and the exact words added and removed. The shortest path to 'what actually happened since the last run'.

## `runSummary` (type: `string`):

Totals for this run: pages checked, how they broke down across changed, unchanged, noise-only, baseline, pending and error, how many noise regions were filtered, and the charging totals.

# 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": [
        {
            "url": "https://news.ycombinator.com/",
            "label": "Hacker News front page"
        }
    ],
    "ignoreSelectors": [],
    "ignorePatterns": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("drkiwi/website-change-detection").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": [{
            "url": "https://news.ycombinator.com/",
            "label": "Hacker News front page",
        }],
    "ignoreSelectors": [],
    "ignorePatterns": [],
}

# Run the Actor and wait for it to finish
run = client.actor("drkiwi/website-change-detection").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": [
    {
      "url": "https://news.ycombinator.com/",
      "label": "Hacker News front page"
    }
  ],
  "ignoreSelectors": [],
  "ignorePatterns": []
}' |
apify call drkiwi/website-change-detection --silent --output-dataset

```

## MCP server setup

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

```

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/6bMPA3WSv1gfu8emG/builds/np0cLCuz67GJNZPvE/openapi.json
