# Website Screenshot & Visual Change Monitor (`automa-flow/website-screenshot-visual-monitor`) Actor

Monitor website visual changes and capture stable PNG screenshots in bulk. Compare every successful render with the last good baseline, get pixel-change ratios, highlighted diffs and changed regions, and send webhook-ready results for QA, ecommerce, compliance and competitor tracking.

- **URL**: https://apify.com/automa-flow/website-screenshot-visual-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Developer tools, Automation, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 successful 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?

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 Screenshot & Visual Change Monitor

Monitor website visual changes between runs with pixel-level comparisons,
highlighted diffs, changed regions and webhook-ready events. The same Actor is
a website screenshot API for stable PNG captures of one public page or a batch.

Save a monitor input as an Apify Task and run it on a schedule. The first
successful run creates a `BASELINE`; every later complete capture returns
`UNCHANGED` or `VISUAL_CHANGE` against the last successful capture.

A timeout, blocked page, incomplete render or storage failure never becomes
“unchanged” and never replaces a good baseline.

### What you get

- PNG viewport or full-page website screenshots;
- stable desktop, mobile, locale, timezone and motion settings;
- bounded lazy-load scrolling, font/image settling and animation suppression;
- `ignoreSelectors` for noisy content without layout reflow;
- `hideSelectors` for banners or widgets that should be removed;
- deterministic pixel-change ratio and changed-region bounding boxes;
- optional highlighted diff PNG on meaningful changes;
- one explicit result for every requested URL, including invalid, failed and
  blocked pages;
- only the two rolling screenshot slots and one diff per monitoring
  configuration—no hidden screenshot-history database.

### Who uses this website visual change monitor?

- QA and engineering teams comparing production pages after a release;
- ecommerce teams tracking product, pricing and checkout-page changes;
- compliance teams keeping timestamped visual evidence of public disclosures;
- agencies monitoring client landing pages and campaign assets;
- competitive-intelligence workflows that need a machine-readable change event,
  not just an image.

### Ready-made example Tasks

- [Monitor website visual changes with screenshot diffs](https://apify.com/automa-flow/website-screenshot-visual-monitor/examples/monitor-website-visual-changes)
  for a reusable baseline and highlighted visual changes;
- [Monitor competitor product page visual changes](https://apify.com/automa-flow/website-screenshot-visual-monitor/examples/monitor-ecommerce-product-page-changes)
  for ecommerce and competitive-intelligence workflows;
- [Capture full-page website screenshots in bulk](https://apify.com/automa-flow/website-screenshot-visual-monitor/examples/capture-bulk-website-screenshots)
  for URL lists and downloadable PNG evidence;
- [Capture mobile website screenshots for responsive QA](https://apify.com/automa-flow/website-screenshot-visual-monitor/examples/capture-mobile-website-screenshots)
  with a fixed 390 × 844 viewport.

### Quick start

#### 1. Basic screenshot

```json
{
  "urls": [{"url": "https://example.com", "externalId": "homepage"}],
  "mode": "snapshot",
  "fullPage": false
}
```

#### 2. Full-page screenshot

```json
{
  "urls": [{"url": "https://example.com/pricing"}],
  "mode": "snapshot",
  "fullPage": true,
  "scrollForLazyLoad": true
}
```

#### 3. Batch screenshots

```json
{
  "urls": [
    {"url": "https://example.com", "externalId": "home"},
    {"url": "https://example.com/pricing", "externalId": "pricing"},
    {"url": "https://example.com/terms", "externalId": "terms"}
  ],
  "mode": "snapshot"
}
```

One bad URL does not stop the others. Three inputs always produce three dataset
records unless the whole Actor runtime cannot start.

#### 4. Mobile viewport

```json
{
  "urls": [{"url": "https://example.com"}],
  "mode": "snapshot",
  "viewport": {"width": 390, "height": 844},
  "fullPage": true
}
```

Viewport and capture settings are part of the monitor state identity. Mobile and
desktop captures never share a baseline.

#### 5. Monitor mode

```json
{
  "urls": [{"url": "https://example.com/pricing", "externalId": "competitor-pricing"}],
  "mode": "monitor",
  "fullPage": true,
  "changeThreshold": 0.01
}
```

Run the same saved Task again. `changeThreshold` is a fraction: `0.01` means one
percent of the normalized common image canvas.

#### 6. Visual diff

```json
{
  "urls": [{"url": "https://example.com/landing"}],
  "mode": "monitor",
  "changeThreshold": 0.005,
  "saveDiffImage": true
}
```

When the ratio reaches the threshold, the dataset row contains
`VISUAL_CHANGE`, `changed_regions`, and a `diff_image_url` with changed pixels
highlighted in red.

#### 7. Ignore a cookie banner or live counter

```json
{
  "urls": [{"url": "https://example.com"}],
  "mode": "monitor",
  "ignoreSelectors": [".live-clock", "[data-testid='ad-slot']"],
  "hideSelectors": ["#cookie-banner", ".chat-widget"]
}
```

Ignored elements keep their layout space but their content is invisible. Hidden
elements use `display:none` and can cause layout reflow. Prefer a precise selector
over a high global threshold. The Actor intentionally has no fragile built-in
database of banner selectors.

#### 8. Daily schedule

1. Run a `monitor` input successfully once.
2. In Apify Console, choose **Save as task**.
3. Open **Schedules**, create a daily schedule, and select that Task.
4. Keep URL, viewport, full-page, wait, delay and selector settings stable.

Monitor mode uses the persistent named KVS
`website-screenshot-visual-monitor-state`, so independent Task runs find the
last successful baseline. Snapshot images and each run's `RUN_SUMMARY` remain in
that run's default storage. A new capture configuration safely creates a
separate `BASELINE` instead of comparing unlike screenshots.

#### 9. Webhook on `VISUAL_CHANGE`

Attach an `ACTOR.RUN.SUCCEEDED` webhook to the scheduled Task. In the receiver,
fetch the run dataset and act only on rows satisfying both conditions:

```javascript
const changed = datasetItems.filter(
  (item) => item.status === 'SUCCESS' && item.change_type === 'VISUAL_CHANGE',
);
```

Never branch on `changed` alone: failure rows deliberately use `changed: null`.
The diff URL and changed regions make each matching row usable as an alert
payload without comparing two exports downstream.

### Input reference

| Field | Default | Meaning |
| --- | --- | --- |
| `urls` | required | 1–500 public HTTP(S) pages, with optional `externalId` |
| `mode` | `snapshot` | `snapshot` does not touch monitor state; `monitor` compares successful captures |
| `fullPage` | `true` | Full scrollable page or viewport only |
| `viewport` | `1440 × 900` | Deterministic CSS-pixel viewport |
| `waitUntil` | `networkidle` | `networkidle`, `load`, or `domcontentloaded` |
| `delayMs` | `500` | Extra render-settling time, 0–30 seconds |
| `navigationTimeoutMs` | `45000` | Hard per-attempt navigation limit |
| `scrollForLazyLoad` | `true` | Bounded scroll before full-page capture |
| `ignoreSelectors` | `[]` | Preserve layout while hiding selected content |
| `hideSelectors` | `[]` | Remove selected elements before capture |
| `changeThreshold` | `0.01` | Changed-pixel fraction required for `VISUAL_CHANGE` |
| `saveDiffImage` | `true` | Save one highlighted PNG for a meaningful change |
| `proxyConfiguration` | direct | Optional user-controlled Apify/custom proxy |

Browser pool size, concurrency, Chromium arguments and retry timing are not Store
inputs. They are conservative internal runtime choices.

### Output and failure semantics

Every row carries `source`, stable `source_id`, `source_url`, `scraped_at`,
`schema_version` and `fingerprint`, plus screenshot/change fields.

Statuses:

- `SUCCESS` — navigation, rendering, screenshot and KVS write all completed;
- `PARTIAL` — something rendered, but completeness or storage could not be
  verified;
- `FAILED` — navigation/browser/source failed after bounded retry where sensible,
  or the check was rejected before capture by a too-low run spending limit;
- `BLOCKED` — HTTP 401/403/407/429, CAPTCHA or a recognizable challenge page;
- `INVALID_URL` — rejected before browser access, including private/local targets.

Change types exist only on successful monitor captures:

- `BASELINE` — first success for this URL and capture configuration;
- `UNCHANGED` — changed-pixel ratio is below `changeThreshold`;
- `VISUAL_CHANGE` — ratio reached the threshold.

Full-page dimension changes are normalized onto a common canvas. Added or
removed width/height counts as change even when the added region is blank.

See [`examples/sample_output.json`](examples/sample_output.json) for a complete
change event.

### API use

Use the standard Apify Actor API with your own Actor ID and token. Never place a
token in source control.

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("YOUR_USERNAME/website-screenshot-visual-monitor").call(
    run_input={
        "urls": [{"url": "https://example.com", "externalId": "homepage"}],
        "mode": "monitor",
        "changeThreshold": 0.01,
    }
)
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
```

### Use with AI agents through Apify MCP

Expose the Actor as a typed MCP tool:

```text
https://mcp.apify.com?tools=automa-flow/website-screenshot-visual-monitor
```

Example prompt:

```text
Run automa-flow/website-screenshot-visual-monitor in monitor mode for these
public product-page URLs. Report only VISUAL_CHANGE rows above a 1% threshold,
include changed regions and screenshot/diff URLs, and keep failed captures
separate from unchanged pages.
```

The first successful monitor run creates a `BASELINE`. A blocked or partial
capture never becomes `UNCHANGED` and never replaces the last good baseline.

### Pricing

The publication price is **$0.002 per successful page check** ($2 per 1,000).
A page check includes the persisted PNG, and in `monitor` mode it also includes
comparison with the last successful baseline, changed-region detection and the
optional highlighted diff image. There is no start fee.

| Successful page checks | Actor charge |
| ---: | ---: |
| 1 | $0.002 |
| 100 | $0.20 |
| 1,000 | $2.00 |

An unchanged monitor check is billable because the page was rendered, persisted
and compared. Invalid URLs, blocked pages, partial captures, failed captures and
retries are not charged. Platform usage is included in this pay-per-event price;
the Apify Console price shown at run time is canonical. If a run's maximum total
charge cannot cover every potentially successful URL in the batch, the Actor
stops before opening Chromium, writes one free `BILLING_LIMIT_TOO_LOW` result per
otherwise valid URL and fails the run. No monitor baseline is advanced without a
deliverable paid result. A batch containing only invalid URLs succeeds with one
explicit free `INVALID_URL` row per input because those are per-item outcomes,
not a system failure.

### Reliability, proxy and measured infrastructure cost

The Actor reuses one Chromium process, creates an isolated context per URL and
runs at internal concurrency 2. Every HTTP(S) navigation and page resource is
checked against local, private and reserved networks, and service workers are
disabled so they cannot bypass that guard. Direct access is the default. Enable
a proxy only when you are permitted to access the page and direct anonymous
access is insufficient. The Actor does not automatically escalate to
residential proxies and never attempts universal anti-bot bypass.

The 2026-09-01 30-site runtime probe in the pinned Apify image measured:

- Chromium startup: 0.934 seconds;
- average viewport capture: 5.406 seconds; p95: 11.752 seconds;
- peak container memory: 1.01 GiB;
- average PNG: 241.6 KiB; maximum: 729.8 KiB;
- 24 complete public pages and 6 explicit 401/403 blocked pages;
- one false change above 1% among 24 accessible pages (4.17%), caused by a highly
  dynamic Booking.com render.

The deterministic 10/100/500 benchmark in the final image measured these 500-URL
cases at concurrency 2:

| Scenario | Wall time | Peak RSS | Core compute at 2 GiB | Bounded KVS bytes |
| --- | ---: | ---: | ---: | ---: |
| Viewport snapshots | 38.3 s | 824 MiB | 0.0213 CU | 8.44 MB |
| Full-page snapshots | 139.0 s | 946 MiB | 0.0772 CU | 18.00 MB |
| Unchanged monitor checks | 58.2 s | 828 MiB | 0.0324 CU | 8.62 MB |
| Changed checks with diff | 199.8 s | 877 MiB | 0.1110 CU | 23.33 MB |

At the current Free/Starter platform rate of $0.20/CU, the measured core compute
scales to about $0.009, $0.031, $0.013 and $0.044 per 1,000 URLs respectively.
Those are lower bounds: KVS/Dataset operations, timed storage, external website
traffic, browser startup, retries and proxy usage are separate. At current
operation rates, an exact unchanged check additionally needs two KVS reads, one
state write and one Dataset row; a changed+diff check needs two reads, three KVS
writes and one Dataset row. See [Apify pricing](https://apify.com/pricing).

The default memory is therefore 2 GiB. Use selectors for dynamic sites and
review the first diff before automating a high-impact response. Run-level metrics
are stored in `RUN_SUMMARY`, including billable and charged page-check counts.

### Security, legal use and retention

This Actor is for public webpages only. Do not submit private dashboards,
credentials, cookies, authenticated profiles or URLs carrying secrets. It does
not bypass login, paywalls, CAPTCHA, account permissions or access controls.

Website terms and robots policies differ. You are responsible for having the
right to access, capture, process and retain each page. A screenshot can contain
public personal data or copyrighted material; visual monitoring grants no right
to republish it.

Monitoring storage is bounded per URL/configuration to two rolling screenshots,
one diff and compact state in the persistent named monitoring KVS. Dataset
observations remain history-ready, but the Actor does not build a
screenshot-history store. Pixel-identical checks reuse the existing PNG and
update only successful-check metadata, avoiding a duplicate KVS artifact write.

### Known limitations

- Pixel comparison detects visible differences, not their business meaning.
- Personalization, A/B tests, ads, live counters and geography can create genuine
  pixel changes; use precise selectors and an observed threshold.
- Font/browser upgrades can change rendering. The pinned image reduces this
  risk; an intentional runtime/configuration change re-baselines safely.
- Full-page captures above 50,000 CSS pixels in height or 30 million rendered
  pixels return `PARTIAL/PAGE_TOO_LARGE` instead of risking an out-of-memory run.
- Closed shadow DOM, cross-origin frames and canvas animations cannot always be
  selectively ignored with CSS selectors.
- Login-required and strongly protected pages are intentionally unsupported.

### FAQ and troubleshooting

#### Can I use this as a website screenshot API?

Yes. Run it through the Apify API with one URL or a batch of up to 500 URLs. Each
input produces an explicit dataset row on an ordinary run, and screenshot files
are stored in the Actor's key-value store.

#### How does visual change detection work?

The first successful `monitor` run creates a baseline. Later runs normalize the
two PNG canvases, compare pixels and report `UNCHANGED` or `VISUAL_CHANGE` based
on `changeThreshold`. Failed or incomplete checks never replace the baseline.

#### Why does a page report changes every day?

Ads, clocks, cookie banners, rotating content, personalization and A/B tests are
real pixel changes. Add precise `ignoreSelectors` or `hideSelectors`, keep the
viewport stable and raise the threshold only after reviewing representative
diffs.

#### Why is a screenshot blocked or partial?

`BLOCKED` means the site returned an authorization/rate-limit response or a
recognizable challenge. `PARTIAL` means the Actor could not prove a complete,
persisted capture. The Actor does not bypass login, CAPTCHA or access controls;
use a permitted proxy only when direct access is insufficient.

### Local development

```powershell
docker build --target test -t website-screenshot-visual-monitor:test .
docker run --rm website-screenshot-visual-monitor:test
docker build -t website-screenshot-visual-monitor .
apify run
```

CI tests use only generated images and a local HTML fixture server. The separate
manual live benchmark is under
`experiments/website-screenshot-visual-monitor/` and never contacts third-party
sites from CI.

# Actor input Schema

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

Public HTTP(S) webpages, up to 500 per run. externalId is echoed into the result so it can join back to your watchlist.

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

Snapshot captures the current page without reading or changing monitoring state. Monitor compares with the last successful capture; its first successful run is BASELINE.

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

Capture the complete scrollable page. Disable for a faster above-the-fold viewport screenshot.

## `viewport` (type: `object`):

Deterministic browser viewport in CSS pixels. Desktop and mobile settings always use separate monitor baselines.

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

networkidle waits for load and then gives background requests a bounded 8-second settling window. Use load or domcontentloaded on sites with perpetual analytics requests.

## `delayMs` (type: `integer`):

Additional wait after fonts, viewport images, lazy-load scroll and selectors are stabilized.

## `navigationTimeoutMs` (type: `integer`):

Hard per-attempt navigation limit. Transient navigation failures are retried once and retries are never separate output rows.

## `scrollForLazyLoad` (type: `boolean`):

For full-page captures, scroll down in bounded steps to load images and sections before returning to the top.

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

CSS selectors whose contents are made invisible while their layout space remains. Use for clocks, ads, random recommendations and rotating content.

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

CSS selectors removed with display:none before capture. Use only when layout reflow is desirable, for example cookie banners or chat widgets.

## `changeThreshold` (type: `number`):

Fraction of changed pixels required for VISUAL\_CHANGE. 0.01 means 1% of the normalized common canvas.

## `saveDiffImage` (type: `boolean`):

Store one highlighted PNG when a monitor result exceeds the meaningful-change threshold.

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

Optional user-controlled Apify or custom proxy. Direct Chromium access is the default; the Actor never escalates automatically to residential proxy.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com",
      "externalId": "homepage"
    }
  ],
  "mode": "snapshot",
  "fullPage": true,
  "viewport": {
    "width": 1440,
    "height": 900
  },
  "waitUntil": "networkidle",
  "delayMs": 500,
  "navigationTimeoutMs": 45000,
  "scrollForLazyLoad": true,
  "ignoreSelectors": [],
  "hideSelectors": [],
  "changeThreshold": 0.01,
  "saveDiffImage": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "urls": [
        {
            "url": "https://example.com",
            "externalId": "homepage"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/website-screenshot-visual-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 = { "urls": [{
            "url": "https://example.com",
            "externalId": "homepage",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/website-screenshot-visual-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 '{
  "urls": [
    {
      "url": "https://example.com",
      "externalId": "homepage"
    }
  ]
}' |
apify call automa-flow/website-screenshot-visual-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/website-screenshot-visual-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/f4avoyY8bHwu0bPqV/builds/dI3TGSnS3VIuvBZz3/openapi.json
