# Website Change Monitor: New Items & Telegram Alerts (`gouguah/reliable-page-change-monitor`) Actor

Monitor web pages and get Telegram, Slack or webhook alerts only on real changes. Watch a CSS selector per page, or get only new items from job boards, feeds and tenders. Errors never count as changes. No browser: fast and cheap. You pay only for pages checked successfully.

- **URL**: https://apify.com/gouguah/reliable-page-change-monitor.md
- **Developed by:** [Hafid Gougua](https://apify.com/gouguah) (community)
- **Stats:** 2 total users, 1 monthly users, 33.3% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 page checks

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

## Reliable Page Change Monitor

This website change monitor watches web pages and sends a Telegram, Slack or webhook alert **only when the content you care about really changes**: page change detection that ignores noise. Point it at a CSS selector, choose "new items only" for feeds and listings, and stop getting alerts for reordered lists, whitespace edits or a site that was down for a minute.

### Step-by-step tutorial

New to website monitoring or Telegram bots? Read the full setup guide (about 15 minutes, no code): [How to Get Telegram Alerts When Any Website Changes](https://medium.com/@gougua.hafid/how-to-get-telegram-alerts-when-any-website-changes-3b13712c09b2)

### Who it is for

- **Job seekers and recruiters**: get the new postings on a careers page or job board, not the whole page every time it reshuffles.
- **E-commerce and pricing teams**: watch a competitor's price or stock block (`#price`, `.availability`) and nothing else.
- **Developers**: follow changelogs, API docs and release notes.
- **Ops and support teams**: watch public status pages for new incidents.
- **Procurement and sales**: be the first to see new public tenders and calls for proposals.

### Why it is reliable

- **New items only** (`newItems` mode): with a CSS selector, every matched element is one item, however many lines it has (shown on one line in alerts); without one, every line is an item. You are alerted only about items never seen before. Reordering, items dropping off the bottom or coming back do not trigger alerts.
- **A CSS selector per page**: monitor just the price, the job list or the incident list. Ads, dates and counters elsewhere on the page are ignored.
- **Errors are never changes**: an HTTP 500, a timeout, a block page or a missing selector is reported as an error status. The saved version stays untouched, so you never get a "page changed" alert followed by a "changed back" alert.
- **Retries**: server errors and network errors are retried once; notifications are retried up to 2 more times in the same run (network errors, 5xx, 429 with `Retry-After`). If every channel still fails, the alert is retried on the next run, and never sent twice.
- **Whitespace-insensitive**: extra spaces and blank lines are normalized away.
- **No browser**: plain HTTP requests, so runs are fast (seconds) and cheap (256 MB memory).
- **You never pay for failures**: only pages that were fetched and compared are billed.

**Tested on the Apify platform**: 47 consecutive hourly scheduled runs over 48 hours, 10 pages each, with no failed run.

### Example Telegram alert (`newItems` mode)

```text
New items: BBC News headlines — https://www.bbc.com/news
Checked at: 2026-10-27T06:00:24.756Z

+ Israel heads to polls for pivotal election defined by security and democracy
+ New Zealand election 2026: What you need to know
```

Long alerts stay within Telegram's 4,096-character limit (Slack: 3,000). Lines are never cut in the middle; the message ends with `…and N more new items` when not everything fits. The webhook always receives every item as JSON.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `pages` | array of objects | | Pages to monitor, each `{ url, cssSelector?, label?, compareMode? }`. |
| `urls` | array of strings | `[]` | Pages that share the `cssSelector` below (kept for older tasks). |
| `cssSelector` | string | `""` | Selector for every entry of `urls`. Empty means the whole `<body>` text. |
| `compareMode` | `text` or `newItems` | `text` | Default mode for pages that do not set their own. |
| `webhookUrl` | string (secret) | | Receives a JSON `POST` per change. |
| `slackWebhookUrl` | string (secret) | | Slack incoming webhook. |
| `telegramBotToken` | string (secret) | | Telegram bot token (from @BotFather). |
| `telegramChatId` | string | | Telegram chat that receives the alerts. |
| `notifyOnError` | boolean | `false` | Also alert when a page cannot be checked. |
| `requestTimeoutSecs` | integer | `30` | Per-request timeout (1–300). |
| `stateStore` | key-value store | | **Recommended.** The store that keeps the last version of each page between runs. See [Save state between runs](#save-state-between-runs). |
| `stateStoreName` | string | `page-change-monitor-state` | Fallback when no `stateStore` is selected: a named key-value store for the state. |
Up to 50 pages per run. The same URL + selector pair is checked once.

```json
{
    "pages": [
        { "url": "https://news.ycombinator.com/newest", "cssSelector": ".titleline", "label": "HN newest", "compareMode": "newItems" },
        { "url": "https://www.rfc-editor.org/rfc/rfc2606.html", "cssSelector": "pre.newpage", "label": "RFC 2606" }
    ],
    "telegramBotToken": "<your bot token>",
    "telegramChatId": "<your chat id>"
}
```

#### Save state between runs

Change detection needs the previous version of each page. Give the Actor a key-value store to keep it in:

1. In Apify Console open **Storage → Key-value stores** and click **Create new**. Give it a name, for example `my-price-monitor`.
2. In this Actor's input (or your task), open **State store**, click **Select**, and choose that store.
3. **Save** and run. The first run records a baseline; later runs alert on changes.

Use one store per independent monitor (for example one per task). Without a selected store the Actor falls back to the named store in `stateStoreName`. If the platform refuses access to it (for example because a store of that name already exists in your account from another tool), the run still succeeds and writes every page to the dataset, but:

- every page is a `BASELINE` and nothing is saved for the next run,
- **no `page-check` is billed and no alert is sent**,
- each dataset item has `stateWarning`: `State is not saved between runs. Select a key-value store in the 'stateStore' input to enable change detection.` The same warning is in the run log.

#### Compare modes

- **`text`**: a line diff of the monitored text, added lines (`+`) first, then removed lines (`-`). Best for a price block, terms of service, a single document.
- **`newItems`**: alerts list only lines never seen before, in any order. Removals or reordering alone give `CHANGED_NO_NEW_ITEMS` with no alert. Best for job boards, feeds, tenders, incident lists. Up to 5,000 item fingerprints are kept per page. Switching a page from `text` to `newItems` records a silent baseline first, so you are not flooded.

### Statuses

| Status | Meaning | Billed | Alert |
| --- | --- | --- | --- |
| `BASELINE` | First check of this URL + selector; version saved. | Yes | No |
| `UNCHANGED` | Same content as last time. | Yes | No |
| `CHANGED` | Content changed and the alert was delivered (or no channel is set). | Yes | Yes |
| `CHANGED_NO_NEW_ITEMS` | `newItems` mode: only removals or reordering. | Yes | No |
| `NOTIFY_FAILED` | Content changed but every channel failed after retries. Retried next run. | No | Retried |
| `SELECTOR_MISSING` | The selector matched no text. | No | If `notifyOnError` |
| `HTTP_ERROR` | 4xx/5xx or network error. | No | If `notifyOnError` |
| `TIMEOUT` | The request timed out twice. | No | If `notifyOnError` |
| `BLOCKED` | 401, 403, 429 or a CAPTCHA / bot challenge. Not bypassed. | No | If `notifyOnError` |

Only the error statuses leave the saved version untouched, so the next successful check compares against the last good content.

### Output

One dataset item per page per run. This is the first run of the default input:

```json
{
    "url": "https://www.rfc-editor.org/rfc/rfc2606.html",
    "label": "RFC 2606",
    "selector": "pre.newpage",
    "compareMode": "text",
    "status": "BASELINE",
    "checkedAt": "2026-10-28T21:52:03.365Z",
    "changed": false,
    "hash": "4463f85dbea3cbd2ad28d34e68355ea98040640887fa3c9adef807acb8c139d5",
    "diffPreview": null,
    "addedCount": 0,
    "removedCount": 0,
    "httpStatus": 200,
    "errorMessage": null,
    "stateWarning": null
}
```

On a change, `changed` is `true`, `diffPreview` holds the lines shown in the alert, and `addedCount` / `removedCount` give the counts.

For every change the webhook receives a JSON `POST` with `type` (`PAGE_CHANGED`), `url`, `label`, `status`, `compareMode`, `checkedAt`, `diffPreview`, `added`, `removed`, `totalAdded`, `totalRemoved`, `addedTruncated` and `removedTruncated`. `added` and `removed` hold at most 500 entries each; `totalAdded` / `totalRemoved` give the real counts. In `newItems` mode, `added` holds the new items. Errors send `"type": "PAGE_ERROR"` with `errorMessage`.

### Pricing

You pay **$1.00 per 1,000 successful page checks** (one `page-check` event per page fetched and compared), plus Apify's standard run-start fee ($0.00005 per run). Alerts are free. Failed checks (`HTTP_ERROR`, `TIMEOUT`, `BLOCKED`, `SELECTOR_MISSING`, `NOTIFY_FAILED`) are free.

| You monitor, every hour for 30 days | Checks | Monthly cost |
| --- | --- | --- |
| 1 page | 720 | about $0.76 ($0.72 in checks) |
| 10 pages | 7,200 | about $7.24 |
| 50 pages | 36,000 | about $36.04 |
| 100 pages (two tasks of 50) | 72,000 | about $72.07 |

Checking every 6 hours instead of every hour costs one sixth. If a run reaches your maximum cost per run, it stops cleanly: the pages already checked are saved, the rest keep their previous version and are checked next run.

### Scheduling

1. On this Actor's page click **Create task**. Fill in the pages and notification channels, then **Save**.
2. Select a **State store** in the task input (see [Save state between runs](#save-state-between-runs)).
3. Open **Schedules → Create new**, pick a frequency (for example hourly, `0 * * * *`) and add the task under **Actors and tasks**.
4. The first run records a baseline without alerts; later runs alert only on changes.

> **Edit task input as a whole.** If you change a task's input through the API, send the **full** input object, not just the field you changed. A partial input replaces the saved one and can drop your pages, selectors or notification settings. Keep the same `stateStore` (or `stateStoreName`), or the monitor starts over from a new baseline.

### Limitations

- **No JavaScript.** The Actor reads the HTML the server sends. Content rendered in the browser by JavaScript is not seen; pick a selector that exists in the page source (View Source, not DevTools).
- **No login.** Pages behind a login, a paywall or cookies are not supported.
- **No bypassing.** Sites that block automated requests (403, 429, CAPTCHA, bot challenges) are reported as `BLOCKED` and are not retried or circumvented.
- `newItems` works line by line: an item spanning several lines counts as several items. Choose a selector that gives one line per item.
- `text` diffs use the first 20,000 characters of the monitored text; the change detection hash covers the full text.

### Privacy and responsible use

- The Actor stores only the normalized text of what you monitor (the selector's text or the page body), its hash and item fingerprints, in your own key-value store. No cookies, screenshots or full HTML are kept.
- It sends ordinary HTTP requests and does not bypass any protection.
- You are responsible for making sure that monitoring a site complies with its terms of service and robots.txt, and with applicable law. Avoid monitoring pages that contain personal data.

# Actor input Schema

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

Pages to monitor, each with optional CSS selector, label and compare mode: \[{ "url": "https://...", "cssSelector": "#price", "label": "Shop A", "compareMode": "newItems" }]. A page without compareMode uses the global "Compare mode". Checked before the URLs below; the same URL + selector pair is checked only once. Up to 50 pages in total.

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

Pages to monitor (http or https) that share the CSS selector below. Kept for existing tasks; prefer "Pages" for per-page selectors.

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

Optional. Selector used for every URL in "Page URLs". Only text inside matching elements is compared. When empty, the whole <body> text is used (without script, style and noscript).

## `compareMode` (type: `string`):

Default mode for every page that does not set its own compareMode in "Pages" (and for all "Page URLs"). "text": line diff of the whole text (added lines first). "newItems": every element matched by the CSS selector is one item (every non-empty line without a selector); alerts list only items never seen before, ignoring order. Removals and reordering alone update the state without an alert (status CHANGED_NO_NEW_ITEMS). Best for feeds and listings.

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

Optional. Receives a JSON POST for every change (and errors if enabled).

## `slackWebhookUrl` (type: `string`):

Optional. Slack incoming webhook that receives a text message per change.

## `telegramBotToken` (type: `string`):

Optional. Token of your Telegram bot. Requires Telegram chat ID too.

## `telegramChatId` (type: `string`):

Optional. Chat that receives the messages.

## `notifyOnError` (type: `boolean`):

Also send a separate notification when a page cannot be checked (HTTP error, timeout, blocked, selector missing).

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

Timeout for each page request. A timed-out request is retried once.

## `stateStore` (type: `string`):

Key-value store that keeps the last seen version of each page between runs. Create one in Storage → Key-value stores and select it here. Use one store per independent monitor. Without a usable store, every run is a free baseline with no alerts.

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

Used only when no "State store" is selected: named key-value store for the page state. If the platform does not allow access to it, the run continues without saved state (free, no alerts).

## Actor input object example

```json
{
  "pages": [
    {
      "url": "https://news.ycombinator.com/newest",
      "cssSelector": ".titleline",
      "label": "HN newest",
      "compareMode": "newItems"
    },
    {
      "url": "https://www.rfc-editor.org/rfc/rfc2606.html",
      "cssSelector": "pre.newpage",
      "label": "RFC 2606"
    }
  ],
  "urls": [],
  "cssSelector": "",
  "compareMode": "text",
  "notifyOnError": false,
  "requestTimeoutSecs": 30,
  "stateStoreName": "page-change-monitor-state"
}
```

# Actor output Schema

## `results` (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://news.ycombinator.com/newest",
            "cssSelector": ".titleline",
            "label": "HN newest",
            "compareMode": "newItems"
        },
        {
            "url": "https://www.rfc-editor.org/rfc/rfc2606.html",
            "cssSelector": "pre.newpage",
            "label": "RFC 2606"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("gouguah/reliable-page-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://news.ycombinator.com/newest",
            "cssSelector": ".titleline",
            "label": "HN newest",
            "compareMode": "newItems",
        },
        {
            "url": "https://www.rfc-editor.org/rfc/rfc2606.html",
            "cssSelector": "pre.newpage",
            "label": "RFC 2606",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("gouguah/reliable-page-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://news.ycombinator.com/newest",
      "cssSelector": ".titleline",
      "label": "HN newest",
      "compareMode": "newItems"
    },
    {
      "url": "https://www.rfc-editor.org/rfc/rfc2606.html",
      "cssSelector": "pre.newpage",
      "label": "RFC 2606"
    }
  ]
}' |
apify call gouguah/reliable-page-change-monitor --silent --output-dataset

```

## MCP server setup

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