# Website Visual Change Monitor (`automation-lab/website-visual-change-monitor`) Actor

📸 Capture public webpages, compare them with persistent visual baselines, quantify changed pixels, and review highlighted PNG diffs with crop and mask controls.

- **URL**: https://apify.com/automation-lab/website-visual-change-monitor.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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/platform/actors/running/actors-in-store#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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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 Visual Change Monitor

**Website Visual Change Monitor** captures public webpages in a controlled Chromium browser, compares each screenshot with its previous baseline, and produces visual evidence that automations can act on.

Track pricing pages, policy notices, landing pages, product layouts, competitor campaigns, and public web QA surfaces without reviewing every page manually.

Each URL produces a dataset record with change metrics plus links to the current screenshot, baseline screenshot, and a highlighted PNG diff.

### What does Website Visual Change Monitor do?

The Actor turns a repeated screenshot workflow into structured monitoring data.

- 🌐 Opens up to 20 public HTTP or HTTPS pages per run.
- 📸 Captures the viewport or the full scrollable page.
- 🧭 Uses a fixed viewport, locale, timezone, color scheme, and device scale.
- 🧹 Masks or hides noisy elements such as clocks, ads, and cookie notices.
- ✂️ Crops monitoring to a chosen CSS selector when only one region matters.
- 🔍 Compares PNG pixels with configurable color tolerance.
- 📊 Reports changed pixels, total pixels, and change percentage.
- 🚨 Marks whether the meaningful-change threshold was exceeded.
- 🟥 Generates a highlighted visual overlay showing changed pixels.
- 📦 Stores images in a key-value store and metadata in the default dataset.
- ♻️ Updates, freezes, or resets the persistent baseline.

The Actor is a visual monitor, not a text scraper. It detects rendered changes even when the underlying HTML structure is unfamiliar.

### Who is it for?

#### Competitive intelligence teams

Watch public competitor pricing, feature, campaign, and positioning pages. Send only threshold-exceeding results into Slack, email, or a review queue.

#### Ecommerce and merchandising teams

Record visual changes to public category pages, offers, promotional banners, and product layouts on a recurring schedule.

#### Compliance and regulatory teams

Keep timestamped visual evidence of public policy, disclosure, terms, or regulatory pages and inspect highlighted differences.

#### Brand and marketing teams

Monitor important public landing pages for accidental visual changes, removed claims, broken layouts, or campaign replacements.

#### Web QA and engineering teams

Use stable browser settings, crop selectors, and masks for lightweight visual regression checks against deployed public pages.

### Why use this visual change monitor?

A screenshot by itself is difficult to automate.

This Actor adds the state and metadata needed for recurring decisions:

1. A persistent baseline identifies what the page looked like before.
2. Pixel comparison quantifies the size of the change.
3. A configurable threshold separates noise from meaningful updates.
4. Changed regions point reviewers toward the affected area.
5. PNG artifacts preserve visual evidence.
6. Dataset records work with exports, webhooks, schedules, and APIs.

You can run it manually, but the highest-value workflow is a scheduled Actor task with a webhook that reacts only when `thresholdExceeded` is `true`.

### How the comparison works

Chromium renders every page with these deterministic defaults:

- viewport: 1280 × 800 CSS pixels;
- device scale factor: 1;
- locale: `en-US`;
- timezone: UTC;
- color scheme: light;
- reduced motion enabled;
- CSS animations and transitions disabled.

After navigation and an optional settling delay, the Actor captures a PNG.

When a baseline exists, both images are normalized to the same canvas dimensions and compared with `pixelmatch`.

Changed pixels are highlighted in red on a copy of the current image.

If a page grows or shrinks, the added or removed area contributes to the visual change percentage.

### Persistent baselines

A baseline is isolated by normalized URL and scheduled Actor task ID.

For ad-hoc Actor runs, `monitorId` supplies the stable namespace. Choose a specific identifier and reuse it on later runs.

A successful capture becomes the next baseline when `updateBaseline` is enabled.

Set `updateBaseline` to `false` when every future capture should be compared against one fixed reference.

Set `resetBaseline` to `true` after an intentional redesign or when starting a new monitoring series.

Baseline metadata uses capture timestamps to avoid replacing an observed newer baseline with an older capture.

### Reduce false positives with masks and crops

Dynamic content can create visual noise even when the important page content has not changed.

Use `maskSelectors` for elements that should keep their layout but not their changing pixels.

Examples:

- `.current-time`
- `[data-testid="ad-slot"]`
- `.cookie-banner`
- `.rotating-testimonial`

Masked elements become a stable black rectangle in both screenshots.

Use `hideSelectors` when removing an element will not cause unwanted layout reflow.

Use `cropSelector` to capture only one area, such as:

- `#pricing`
- `.product-details`
- `main article`
- `[data-testid="offer-card"]`

A crop selector that matches nothing produces an explicit error record instead of silently capturing the wrong area.

### Input

| Field | Type | Default | Purpose |
|---|---|---:|---|
| `startUrls` | array | required | Public webpage URLs, maximum 20 |
| `monitorId` | string | required | Stable namespace for ad-hoc baselines |
| `viewportWidth` | integer | 1280 | Browser width in CSS pixels |
| `viewportHeight` | integer | 800 | Browser height in CSS pixels |
| `fullPage` | boolean | true | Capture the complete scrollable page |
| `waitUntil` | string | `networkidle` | Navigation lifecycle condition |
| `waitAfterLoadMs` | integer | 1000 | Additional render settling delay |
| `navigationTimeoutSecs` | integer | 45 | Per-page navigation timeout |
| `cropSelector` | string | empty | Capture one matching element |
| `maskSelectors` | string[] | `[]` | Stable masks for noisy elements |
| `hideSelectors` | string[] | `[]` | Elements removed before capture |
| `pixelThreshold` | number | 0.1 | Pixel color tolerance from 0 to 1 |
| `changeThresholdPercent` | number | 0.1 | Meaningful-change percentage |
| `resetBaseline` | boolean | false | Start a new baseline |
| `updateBaseline` | boolean | true | Advance baseline after success |
| `baselineStoreName` | string | provided | Advanced named KVS override |
| `proxyConfiguration` | object | direct | Optional Apify Proxy settings |

### Example input

```json
{
  "startUrls": [
    { "url": "https://distill.io/pricing/" }
  ],
  "monitorId": "distill-pricing-monitor",
  "viewportWidth": 1280,
  "viewportHeight": 800,
  "fullPage": true,
  "waitUntil": "networkidle",
  "waitAfterLoadMs": 1000,
  "maskSelectors": [".cookie-banner"],
  "changeThresholdPercent": 0.1,
  "updateBaseline": true
}
````

The first run initializes a baseline and reports zero changed pixels.

A later run with the same task or `monitorId` performs the comparison.

### Output data

One dataset row represents one requested webpage.

| Field | Description |
|---|---|
| `url` | Normalized requested URL |
| `finalUrl` | URL after public redirects |
| `status` | `success` or `error` |
| `title` | Rendered document title |
| `capturedAt` | Current capture timestamp |
| `baselineCapturedAt` | Compared baseline timestamp |
| `baselineInitialized` | Whether this was the first baseline |
| `baselineUpdated` | Whether current capture advanced the baseline |
| `viewport` | Rendering dimensions and full-page mode |
| `imageWidth`, `imageHeight` | Actual PNG dimensions |
| `currentScreenshotUrl` | Current PNG in KVS |
| `baselineScreenshotUrl` | Baseline PNG in KVS |
| `diffScreenshotUrl` | Red highlighted diff overlay |
| `changedPixels` | Number of changed pixels |
| `totalPixels` | Total compared canvas pixels |
| `changePercent` | Changed pixels as a percentage |
| `changeThresholdPercent` | Configured meaningful threshold |
| `thresholdExceeded` | Whether the threshold was reached |
| `changeRegions` | Up to 20 largest changed bounding regions |
| `error` | Per-URL failure explanation |

### Example output

```json
{
  "url": "https://distill.io/pricing/",
  "finalUrl": "https://distill.io/pricing/",
  "status": "success",
  "title": "Start free. Stay free. Scale when ready. - Distill",
  "capturedAt": "2026-07-23T04:00:00.000Z",
  "baselineCapturedAt": "2026-07-22T04:00:00.000Z",
  "baselineInitialized": false,
  "baselineUpdated": true,
  "changedPixels": 4812,
  "totalPixels": 5004800,
  "changePercent": 0.096147,
  "changeThresholdPercent": 0.1,
  "thresholdExceeded": false,
  "changeRegions": [
    { "x": 318, "y": 940, "width": 202, "height": 84 }
  ],
  "currentScreenshotUrl": "https://api.apify.com/v2/key-value-stores/.../records/...",
  "baselineScreenshotUrl": "https://api.apify.com/v2/key-value-stores/.../records/...",
  "diffScreenshotUrl": "https://api.apify.com/v2/key-value-stores/.../records/..."
}
```

Values above illustrate the output shape; actual metrics depend on the page and baseline.

### Schedule a recurring monitor

1. Save a successful Actor run as an Apify task.
2. Keep the URL, viewport, crop, mask, and wait settings stable.
3. Add an hourly, daily, or weekly schedule to the task.
4. Add a run-succeeded webhook.
5. Filter the dataset result on `thresholdExceeded` downstream.
6. Route the diff image to the responsible reviewer.

Scheduled task IDs automatically isolate baseline namespaces.

Use separate tasks for independent monitoring policies, even when they target the same URL.

### Webhooks and alert workflows

The Actor deliberately emits structured state instead of sending alerts to one fixed destination.

This makes it suitable for:

- Slack alerts through Make or Zapier;
- email summaries after scheduled runs;
- compliance evidence pipelines;
- dataset monitoring dashboards;
- custom HTTP webhooks;
- incident or QA ticket creation.

A webhook consumer should check both `status` and `thresholdExceeded`.

A successful run can include a per-URL error when other URLs were captured correctly.

### How much does it cost to monitor website visual changes?

The Actor uses pay-per-event pricing:

- **Start:** $0.005 per run.
- **Visual comparison result:** tiered pricing; current BRONZE price is $0.0094431 per URL result.

Higher Apify plans receive lower per-result tier prices.

Browser runtime depends on page load speed, full-page height, and settling delay.

Keep URL batches focused and avoid unnecessary long waits.

The Apify Console shows the exact maximum charge before a run starts.

### API usage with JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/website-visual-change-monitor').call({
  startUrls: [{ url: 'https://distill.io/pricing/' }],
  monitorId: 'distill-pricing-monitor',
  changeThresholdPercent: 0.1,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items[0].thresholdExceeded, items[0].diffScreenshotUrl);
```

Reuse an Actor task for scheduled baseline continuity.

### API usage with Python

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('automation-lab/website-visual-change-monitor').call(run_input={
    'startUrls': [{'url': 'https://distill.io/pricing/'}],
    'monitorId': 'distill-pricing-monitor',
    'changeThresholdPercent': 0.1,
})

for item in client.dataset(run['defaultDatasetId']).iterate_items():
    print(item['status'], item.get('changePercent'))
```

### API usage with cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~website-visual-change-monitor/runs?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "startUrls": [{"url": "https://distill.io/pricing/"}],
    "monitorId": "distill-pricing-monitor",
    "changeThresholdPercent": 0.1
  }'
```

Fetch results from the run's `defaultDatasetId` after it succeeds.

### Use with Apify MCP

#### Claude Code setup

```bash
claude mcp add --transport http apify "https://mcp.apify.com?tools=automation-lab/website-visual-change-monitor"
```

#### Claude Desktop, Cursor, and VS Code setup

Add the same HTTP MCP server in Claude Desktop, Cursor, or VS Code. A compatible MCP JSON configuration is:

```json
{
  "mcpServers": {
    "apify-website-visual-monitor": {
      "type": "http",
      "url": "https://mcp.apify.com?tools=automation-lab/website-visual-change-monitor"
    }
  }
}
```

Example prompts:

- “Run the website visual change monitor for this public pricing page and summarize any changed regions.”
- “Show me the latest highlighted diff for my compliance-page monitor.”
- “Compare these three public campaign pages and list only threshold-exceeding results.”

MCP can start the Actor and inspect its dataset records; image links remain in the result.

### Integrations

#### Google Sheets

Export capture timestamps, change percentages, threshold flags, and artifact URLs for a lightweight audit log.

#### Slack

Use an Apify webhook plus Make to post the page title, percentage, and highlighted diff when the threshold is exceeded.

#### Zapier

Trigger a Zap from completed runs and branch on `thresholdExceeded` before sending notifications.

#### GitHub or Jira

Create a visual regression issue only when a monitored production page changes beyond policy.

#### S3 or data warehouses

Copy dataset metadata and referenced image artifacts into long-term evidence storage according to your retention policy.

### Tips for reliable visual monitoring

- Keep viewport settings identical across baseline and comparison runs.
- Prefer crop selectors when only one component matters.
- Mask timestamps, ads, rotating content, live counters, and personalization.
- Add enough render delay for fonts and client-side layout, but no more than needed.
- Use `networkidle` for application pages and `load` for pages with perpetual analytics requests.
- Treat font or browser-version upgrades as a reason to reset a sensitive baseline.
- Use one scheduled task per independent monitor.
- Choose a threshold from observed page noise instead of defaulting every page to zero.
- Review a diff before automating a high-impact action.

### Errors and troubleshooting

#### Why did the first run report no changes?

The first successful run initializes the baseline. Run the same scheduled task or reuse the same `monitorId` to compare later.

#### Why is every run a large change?

Check viewport consistency and inspect the page for ads, timestamps, cookie banners, animations, or personalized content. Add mask selectors or crop to a stable region.

#### Why did navigation time out?

Try `waitUntil: "load"` or `"domcontentloaded"`, reduce the extra delay, or increase `navigationTimeoutSecs` within the schema limit.

#### Why did a crop fail?

The selector matched no elements after rendering. Verify it in browser developer tools and ensure the target is not inside a closed shadow root or authenticated page.

#### Why did the Actor reject the page?

The Actor detects common login redirects, CAPTCHAs, and challenge pages. It supports anonymously accessible public pages only.

### Security and privacy

Do not submit private dashboard URLs, session cookies, passwords, or authenticated browser state.

`monitorId` should be specific and not contain secrets.

Screenshot artifacts can contain whatever is publicly rendered at the requested URL.

Apply an appropriate KVS retention policy and limit access to run storage when captures are sensitive to your organization.

The Actor does not attempt to bypass login requirements.

### Legal and ethical use

Only monitor pages you are legally permitted to access and process.

Respect website terms, applicable copyright, privacy rules, and reasonable request frequency.

Visual monitoring does not grant rights to republish captured content.

Use schedules that avoid unnecessary load on target websites.

This tool is designed for public-page observation, QA, compliance, and business intelligence—not access-control circumvention.

### Limitations

Rendering can vary because of upstream personalization, A/B tests, fonts, geography, consent state, or browser changes.

Pixel comparison identifies visual difference; it does not explain business meaning automatically.

Changed regions are compact bounding bands, not semantic DOM elements.

The Actor does not monitor authenticated or private pages.

A maximum of 20 URLs is accepted per run to keep browser workloads controlled.

Concurrent runs use timestamp-aware baseline updates, but users should avoid overlapping schedules for the same monitor when strict ordering matters.

### Related actors

Use [Webpage Structured Data Monitor](https://apify.com/automation-lab/webpage-structured-data-monitor) when you need typed text or numeric field changes rather than pixel-level visual evidence.

Browse other public automation tools from [automation-lab](https://apify.com/automation-lab) for extraction and monitoring workflows.

### FAQ

#### Can I monitor more than one URL?

Yes. Add up to 20 URL entries. The Actor emits one result per URL.

#### Can I compare against a permanent design reference?

Yes. Initialize the desired baseline, then set `updateBaseline` to `false`.

#### Can I force a new baseline after an intentional release?

Yes. Run once with `resetBaseline: true`.

#### Can I monitor only the pricing table?

Yes. Set `cropSelector` to a stable selector such as `#pricing` or `.pricing-grid`.

#### Can I ignore a live counter?

Yes. Add its CSS selector to `maskSelectors`.

#### Does it send alerts itself?

It produces automation-ready results. Use Apify schedules and webhooks to send alerts to your chosen system.

#### Are screenshots downloadable?

Yes. Dataset records contain KVS URLs for current, baseline, and highlighted diff PNG files.

#### Does it work with single-page applications?

Usually, if the public page renders in Chromium. Adjust `waitUntil` and `waitAfterLoadMs` so the layout settles.

#### Does it support logins?

No. V1 intentionally supports public anonymous pages only.

#### What happens when one URL fails?

That URL emits an error record while other URLs continue. If all URLs fail, the run exits non-zero so monitoring systems do not mistake failure for no change.

# Actor input Schema

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

Up to 20 anonymous HTTP/HTTPS pages to capture and compare.

## `monitorId` (type: `string`):

A stable, private identifier for this monitor. Scheduled Actor tasks are additionally isolated by task ID.

## `viewportWidth` (type: `integer`):

Browser viewport width in CSS pixels.

## `viewportHeight` (type: `integer`):

Browser viewport height in CSS pixels.

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

Capture the complete scrollable page instead of only the viewport.

## `waitUntil` (type: `string`):

Browser lifecycle event to await before the additional delay.

## `waitAfterLoadMs` (type: `integer`):

Additional time for late layout and fonts to settle.

## `navigationTimeoutSecs` (type: `integer`):

Maximum wait for each page navigation.

## `cropSelector` (type: `string`):

Optional selector for monitoring one page region, such as #pricing. The run reports an error if it matches nothing.

## `maskSelectors` (type: `array`):

Elements replaced with a stable black mask in screenshots.

## `hideSelectors` (type: `array`):

Elements hidden before capture. Use masks when hiding would cause layout reflow.

## `pixelThreshold` (type: `number`):

Pixelmatch color-distance tolerance from 0 (strict) to 1 (lenient).

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

Set thresholdExceeded when at least this percentage of pixels changed.

## `resetBaseline` (type: `boolean`):

Treat this run as a fresh baseline instead of comparing with the stored image.

## `updateBaseline` (type: `boolean`):

Store each successful current capture as the baseline for the next run. Disable to compare repeatedly against a fixed baseline.

## `baselineStoreName` (type: `string`):

Advanced: named key-value store used for baseline images and metadata.

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

Optional Apify Proxy settings for public pages that require a particular geography.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://distill.io/pricing/"
    }
  ],
  "monitorId": "distill-pricing-monitor",
  "viewportWidth": 1280,
  "viewportHeight": 800,
  "fullPage": true,
  "waitUntil": "networkidle",
  "waitAfterLoadMs": 1000,
  "navigationTimeoutSecs": 45,
  "maskSelectors": [],
  "hideSelectors": [],
  "pixelThreshold": 0.1,
  "changeThresholdPercent": 0.1,
  "resetBaseline": false,
  "updateBaseline": true,
  "baselineStoreName": "website-visual-change-monitor-baselines"
}
```

# Actor output Schema

## `overview` (type: `string`):

One typed capture and pixel-comparison result per requested public webpage URL.

# 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://distill.io/pricing/"
        }
    ],
    "monitorId": "distill-pricing-monitor",
    "viewportWidth": 1280,
    "viewportHeight": 800,
    "fullPage": true,
    "waitUntil": "networkidle",
    "waitAfterLoadMs": 1000,
    "navigationTimeoutSecs": 45,
    "maskSelectors": [],
    "hideSelectors": [],
    "pixelThreshold": 0.1,
    "changeThresholdPercent": 0.1,
    "resetBaseline": false,
    "updateBaseline": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/website-visual-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://distill.io/pricing/" }],
    "monitorId": "distill-pricing-monitor",
    "viewportWidth": 1280,
    "viewportHeight": 800,
    "fullPage": True,
    "waitUntil": "networkidle",
    "waitAfterLoadMs": 1000,
    "navigationTimeoutSecs": 45,
    "maskSelectors": [],
    "hideSelectors": [],
    "pixelThreshold": 0.1,
    "changeThresholdPercent": 0.1,
    "resetBaseline": False,
    "updateBaseline": True,
}

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/website-visual-change-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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://distill.io/pricing/"
    }
  ],
  "monitorId": "distill-pricing-monitor",
  "viewportWidth": 1280,
  "viewportHeight": 800,
  "fullPage": true,
  "waitUntil": "networkidle",
  "waitAfterLoadMs": 1000,
  "navigationTimeoutSecs": 45,
  "maskSelectors": [],
  "hideSelectors": [],
  "pixelThreshold": 0.1,
  "changeThresholdPercent": 0.1,
  "resetBaseline": false,
  "updateBaseline": true
}' |
apify call automation-lab/website-visual-change-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=automation-lab/website-visual-change-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Website Visual Change Monitor",
        "description": "📸 Capture public webpages, compare them with persistent visual baselines, quantify changed pixels, and review highlighted PNG diffs with crop and mask controls.",
        "version": "0.1",
        "x-build-id": "K1Ty2L4ajEytX85Ob"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/automation-lab~website-visual-change-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-automation-lab-website-visual-change-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/automation-lab~website-visual-change-monitor/runs": {
            "post": {
                "operationId": "runs-sync-automation-lab-website-visual-change-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/automation-lab~website-visual-change-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-automation-lab-website-visual-change-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "startUrls",
                    "monitorId"
                ],
                "properties": {
                    "startUrls": {
                        "title": "🌐 Public webpage URLs",
                        "type": "array",
                        "description": "Up to 20 anonymous HTTP/HTTPS pages to capture and compare.",
                        "items": {
                            "type": "object",
                            "required": [
                                "url"
                            ],
                            "properties": {
                                "url": {
                                    "type": "string",
                                    "title": "URL of a web page",
                                    "format": "uri"
                                }
                            }
                        }
                    },
                    "monitorId": {
                        "title": "🔑 Monitor ID",
                        "minLength": 3,
                        "maxLength": 80,
                        "type": "string",
                        "description": "A stable, private identifier for this monitor. Scheduled Actor tasks are additionally isolated by task ID.",
                        "default": "distill-pricing-monitor"
                    },
                    "viewportWidth": {
                        "title": "Viewport width",
                        "minimum": 320,
                        "maximum": 2560,
                        "type": "integer",
                        "description": "Browser viewport width in CSS pixels.",
                        "default": 1280
                    },
                    "viewportHeight": {
                        "title": "Viewport height",
                        "minimum": 240,
                        "maximum": 2160,
                        "type": "integer",
                        "description": "Browser viewport height in CSS pixels.",
                        "default": 800
                    },
                    "fullPage": {
                        "title": "Capture full page",
                        "type": "boolean",
                        "description": "Capture the complete scrollable page instead of only the viewport.",
                        "default": true
                    },
                    "waitUntil": {
                        "title": "Navigation wait condition",
                        "enum": [
                            "load",
                            "domcontentloaded",
                            "networkidle"
                        ],
                        "type": "string",
                        "description": "Browser lifecycle event to await before the additional delay.",
                        "default": "networkidle"
                    },
                    "waitAfterLoadMs": {
                        "title": "Extra render delay (ms)",
                        "minimum": 0,
                        "maximum": 30000,
                        "type": "integer",
                        "description": "Additional time for late layout and fonts to settle.",
                        "default": 1000
                    },
                    "navigationTimeoutSecs": {
                        "title": "Navigation timeout (seconds)",
                        "minimum": 5,
                        "maximum": 180,
                        "type": "integer",
                        "description": "Maximum wait for each page navigation.",
                        "default": 45
                    },
                    "cropSelector": {
                        "title": "✂️ Crop CSS selector",
                        "type": "string",
                        "description": "Optional selector for monitoring one page region, such as #pricing. The run reports an error if it matches nothing."
                    },
                    "maskSelectors": {
                        "title": "Mask CSS selectors",
                        "type": "array",
                        "description": "Elements replaced with a stable black mask in screenshots.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "hideSelectors": {
                        "title": "Hide CSS selectors",
                        "type": "array",
                        "description": "Elements hidden before capture. Use masks when hiding would cause layout reflow.",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "pixelThreshold": {
                        "title": "Pixel color tolerance",
                        "minimum": 0,
                        "maximum": 1,
                        "type": "number",
                        "description": "Pixelmatch color-distance tolerance from 0 (strict) to 1 (lenient).",
                        "default": 0.1
                    },
                    "changeThresholdPercent": {
                        "title": "Meaningful change threshold (%)",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "number",
                        "description": "Set thresholdExceeded when at least this percentage of pixels changed.",
                        "default": 0.1
                    },
                    "resetBaseline": {
                        "title": "♻️ Reset baseline",
                        "type": "boolean",
                        "description": "Treat this run as a fresh baseline instead of comparing with the stored image.",
                        "default": false
                    },
                    "updateBaseline": {
                        "title": "Update baseline after capture",
                        "type": "boolean",
                        "description": "Store each successful current capture as the baseline for the next run. Disable to compare repeatedly against a fixed baseline.",
                        "default": true
                    },
                    "baselineStoreName": {
                        "title": "Baseline KVS name",
                        "type": "string",
                        "description": "Advanced: named key-value store used for baseline images and metadata.",
                        "default": "website-visual-change-monitor-baselines"
                    },
                    "proxyConfiguration": {
                        "title": "🌍 Proxy configuration",
                        "type": "object",
                        "description": "Optional Apify Proxy settings for public pages that require a particular geography."
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
