# Visual Website Change Detection: Screenshot Diff & Webhook (`russellpitt/ralt-page-watch`) Actor

Website change monitor that compares what a page looks like. Saves screenshots and a visual diff image, reports the percent of pixels changed, and can call your webhook. Watch a full page or one CSS section, on desktop or mobile. Failed captures are free.

- **URL**: https://apify.com/russellpitt/ralt-page-watch.md
- **Developed by:** [russell tellez](https://apify.com/russellpitt) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 successful page captures

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

## Visual Website Change Detection: Screenshot Diff & Webhook

RALT Page Watch is a website change monitor that compares what a page looks like. Each run takes a screenshot, compares it with the previous one, and saves an image of the differences. If the page changed more than your threshold, it can call your webhook.

Use it to watch a whole page or only one section. It needs no code, and it works on public pages that don't require a login.

### What you can watch

- **Competitor pages:** pricing, landing pages and homepages.
- **Your own website:** spot layout changes after a release, on desktop or mobile.
- **Policies and terms:** keep dated screenshots of each visible version.
- **Stock and product pages:** see when a page or one section looks different.

### How to use it

1. Add the URLs you want to watch.
2. Optionally, add a CSS selector to watch one section, such as `#pricing`.
3. Run it once. The first run saves the starting screenshot, so it reports no change.
4. Create a [schedule](https://docs.apify.com/platform/schedules) to run it hourly, daily or weekly.
5. Optionally, add a webhook URL to send change alerts to your own system or to an automation tool that accepts webhooks.

#### Input example

```json
{
  "urls": ["https://example.com/pricing"],
  "viewports": "desktop",
  "selector": "#pricing",
  "changeThresholdPercent": 0.5,
  "webhookUrl": "https://your-server.example/hook"
}
```

### Input

| Parameter | Default | Description |
|---|---|---|
| `urls` | Required | Up to 100 public HTTP or HTTPS URLs to watch per run. |
| `viewports` | `desktop` | `desktop` (1280x800) or `mobile` (375x812). |
| `fullPage` | `false` | Capture the full page, up to 5,000 pixels high. |
| `selector` | None | CSS selector of the section to watch. |
| `waitForMs` | `1000` | Extra wait after the page loads, up to 15,000 ms. |
| `changeThresholdPercent` | `0.5` | Percent of changed pixels needed to report a change. |
| `webhookUrl` | None | Public URL that receives an alert when a page changes. |
| `retainSnapshots` | `5` | Snapshots and diffs to keep per URL, from 1 to 50. |

### Output

Each URL produces one record:

```json
{
  "url": "https://example.com",
  "finalUrl": "https://example.com/",
  "status": "success",
  "httpStatus": 200,
  "capturedAt": "2026-11-05T15:03:25.754Z",
  "viewport": "desktop",
  "screenshotKey": "snap_state_072eeb777b9444213950f256afc1f632_1793891006398.png",
  "sha256": "d3a6e2e8ffecec9157b528f99dab3200cb9f75a2eb190e927bbbf81d43bdcbe1",
  "previousSha256": "d3a6e2e8ffecec9157b528f99dab3200cb9f75a2eb190e927bbbf81d43bdcbe1",
  "changed": false,
  "changePercent": 0,
  "diffKey": null,
  "title": "Example Domain",
  "errorCode": null,
  "charged": true
}
```

The webhook sends a JSON message with `event: "page_changed"`, the URL, page title, capture time, viewport, hashes and `changePercent`. It doesn't include the images.

#### Where the images are stored

Screenshots and diffs are saved in a key-value store named `ralt-page-watch-store` in your Apify account, so the next run can compare against them. Apify storage charges for that store apply to your account. Use `screenshotKey` and `diffKey` to find each image, and `retainSnapshots` to limit how many are kept.

#### Error codes

| Code | Meaning |
|---|---|
| `blocked` | Blocked by robots.txt, an HTTP 403, a rate limit or a redirect to a forbidden address. |
| `timeout` | The page took too long to load. |
| `invalid_url` | The URL is malformed, contains credentials or uses another protocol. |
| `forbidden_target` | The URL points to a local, private or cloud metadata address. |
| `http_error` | The website returned a 4xx or 5xx status. |
| `selector_not_found` | The CSS selector matched nothing on the page. |
| `event_charge_limit_reached` | The run stopped at your maximum cost per run. The record has `status: "halted"`. |

### Pricing

- **Successful capture:** $0.005, or $5.00 per 1,000.
- **Run start:** $0.00005 per GB of memory, or $0.0001 per run at the default 2 GB.
- **Failed capture:** free.

For example, watching 10 pages once a day for 30 days is 300 captures and 30 runs, which costs about $1.50. Apify storage charges are separate. Set a maximum cost per run if you want a spending limit: the run stops when it reaches it.

### Limitations

- **Visual comparison only:** it compares pixels, not HTML or text, so it won't tell you which words changed.
- **Changing content can trigger alerts:** ads, carousels, clocks and cookie banners can look like changes. Use a CSS selector or a higher threshold to reduce them.
- **Public pages only:** it doesn't log in, solve CAPTCHAs, use proxies or try to get around blocks. Some websites will return `blocked`.
- **Robots.txt:** it follows the site's robots.txt. If that file can't be read, the capture continues.
- **Slow pages:** pages that don't load in time return `timeout` and aren't charged.

### FAQ

**Why does the first run show no change?**
There's nothing to compare yet. The first run saves the starting screenshot, and it's charged like any other successful capture.

**Can I use the screenshots as legal evidence?**
Don't rely on them for that. The SHA-256 hash shows whether two saved files are identical. It isn't certified evidence or proof from a notary.

**Is it allowed to monitor any website?**
You're responsible for having the right to capture each URL and for following that website's terms. Don't submit URLs that contain passwords, tokens or personal data.

**Does it block internal addresses?**
Yes. It rejects localhost, private networks and cloud metadata addresses, including when a redirect points to them.

# Actor input Schema

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

List of up to 100 public HTTP/HTTPS URLs to capture and inspect for changes.

## `viewports` (type: `string`):

Select device viewport for the browser capture.

## `fullPage` (type: `boolean`):

Capture the entire scrollable height of the webpage (clamped to max 5000px).

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

Optional CSS selector to isolate and watch only a specific section/element.

## `waitForMs` (type: `integer`):

Additional time in milliseconds to wait after page load before taking screenshot (max 15000ms).

## `changeThresholdPercent` (type: `number`):

Percentage of visual pixel difference required to flag a change event (default 0.5%).

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

Optional public webhook endpoint to receive notifications when changes are detected.

## `retainSnapshots` (type: `integer`):

Number of historical snapshots and diffs to retain in the store per URL (default 5).

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "viewports": "desktop",
  "fullPage": false,
  "waitForMs": 1000,
  "changeThresholdPercent": 0.5,
  "retainSnapshots": 5
}
```

# Actor output Schema

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

One record per URL, with change status, change percent, SHA-256 hash and the keys of the stored images.

# 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://example.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("russellpitt/ralt-page-watch").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://example.com"] }

# Run the Actor and wait for it to finish
run = client.actor("russellpitt/ralt-page-watch").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://example.com"
  ]
}' |
apify call russellpitt/ralt-page-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,russellpitt/ralt-page-watch"
        }
    }
}
```

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/EUeOUq3opKqt0Aebo/builds/bisuWBhu7sCVO36mf/openapi.json
