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

Full-page screenshots and PDFs of any website with device presets, lazy-load scrolling and cookie-banner auto-dismiss. Optionally compares every capture with the previous run and reports which pixels and regions changed.

- **URL**: https://apify.com/codeclouds/website-screenshot-monitor.md
- **Developed by:** [Dennis](https://apify.com/codeclouds) (community)
- **Categories:** Automation, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 screenshots

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

## Website Screenshot & Visual Change Monitor

Accurate full-page screenshots and PDFs of any website — with the things that usually ruin a screenshot handled for you: lazy-loaded content is scrolled into view, cookie banners are dismissed, and rotating ads and chat widgets are blocked out. Turn on the visual diff and every run also tells you **what changed** compared to the previous run, down to the region of the page that moved.

### When should an AI agent use this?

- "Take a full-page screenshot of https://example.com/pricing and tell me the page height."
- "Capture our landing page at mobile, tablet and desktop width and store the images."
- "Monitor these 12 URLs daily and only wake me up when a page actually looks different."
- "How much of the homepage changed since yesterday? Give me the percentage and which sections."
- "Turn these five web pages into A4 PDFs I can attach to a report."
- "Screenshot this page with the newsletter popup and the cookie bar hidden."

### What this Actor does

- **True full-page capture** — the whole scrollable page, not just the first viewport.
- **Lazy-load aware** — scrolls the page in steps before capturing, so images and infinite-scroll sections that only load on approach are actually in the picture.
- **Cookie-banner auto-dismiss** — clicks the accept button of the common consent platforms (OneTrust, Cookiebot, Didomi, TrustArc, Usercentrics, Sourcepoint, CMPs and more) in a dozen languages, then hides anything that survives. It only ever *accepts*; it never clicks "reject" for you. If a banner refuses to go away, the record says so instead of quietly lying.
- **Device presets** — desktop (1920×1080), laptop (1366×768), tablet (768×1024) and mobile (390×844), with a real mobile user agent so responsive sites actually serve their mobile layout. Or set your own width, height and pixel ratio.
- **Deterministic captures** — blocks ad, analytics and chat-widget requests, freezes CSS animations and video, and hides scrollbars. Without this, a rotating banner or a mid-animation carousel makes every screenshot differ from the last one.
- **Page preparation** — click anything first (a "Show more" link, a language switcher, a tab), wait for a specific CSS selector, or hide sticky headers and overlays.
- **PNG, JPEG or PDF** — PNG for lossless screenshots and the best diffing, JPEG for small files, or a printable A4 PDF.
- **Visual change detection** — compares each capture with the baseline of the previous run, reports the percentage of changed pixels, the bounding regions that changed, and writes a diff image with the page faded out and the changes highlighted in red.
- **Per-URL error isolation** — one unreachable or broken URL produces an error record for that URL; the rest of the run continues.
- **Apify Proxy** — optional country and residential proxy configuration for sites that block datacenter IPs.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `startUrls` | array of strings | `["https://example.com"]` | Absolute page URLs to capture. Duplicates are removed, invalid entries come back as an error record. |
| `viewport` | enum | `desktop` | `desktop`, `laptop`, `tablet`, `mobile` or `custom`. |
| `viewportWidth` / `viewportHeight` | integer | `1280` / `800` | Only used with `custom`. |
| `deviceScaleFactor` | integer | `1` | Only used with `custom`. `2` gives a retina-density image. |
| `mobileUserAgent` | boolean | `true` | Send a real mobile/tablet user agent for those presets. |
| `fullPage` | boolean | `true` | Capture the whole scrollable page instead of one viewport. |
| `format` | enum | `png` | `png`, `jpeg` or `pdf`. |
| `imageQuality` | integer | `80` | JPEG quality, 1–100. |
| `waitUntil` | enum | `networkidle` | `load`, `domcontentloaded`, `networkidle` or `commit`. |
| `waitForSelector` | string | – | Wait for this CSS selector before capturing. |
| `waitForTimeout` | integer | `0` | Extra fixed wait in ms. |
| `navigationTimeout` | integer | `45000` | Per-URL navigation timeout in ms. |
| `scrollToBottom` | boolean | `true` | Scroll in steps to trigger lazy-loaded content. |
| `scrollStepDelay` | integer | `300` | Pause between scroll steps in ms. |
| `maxScrollSteps` | integer | `40` | Safety cap; a truncated scroll is logged as a warning. |
| `dismissCookieBanners` | boolean | `true` | Auto-dismiss known consent banners. |
| `cookieBannerSelectors` | array of strings | `[]` | Extra selectors to try after the built-in list. |
| `clickSelectors` | array of strings | `[]` | Elements to click before capturing. |
| `hideSelectors` | array of strings | `[]` | Elements to make invisible before capturing. |
| `blockTracking` | boolean | `true` | Block ad, analytics and chat-widget requests. |
| `disableAnimations` | boolean | `true` | Freeze CSS animations, transitions and video. |
| `hideScrollbars` | boolean | `true` | Hide browser scrollbars. |
| `visualDiff` | boolean | `false` | Compare with the baseline of the previous run. |
| `diffThreshold` | number | `0.1` | Per-pixel colour tolerance, 0–1. |
| `diffMinChangePercentage` | number | `0.1` | Minimum share of changed pixels (%) before a page counts as changed. |
| `diffRegionCellSize` | integer | `32` | Grid cell size in px used to group changed pixels into regions. |
| `diffMaxRegions` | integer | `10` | Maximum number of reported regions per page. |
| `updateBaseline` | boolean | `true` | Store this capture as the new baseline. |
| `maxPages` | integer | `20` | Stop after this many URLs. |
| `proxy` | object | – | Apify Proxy settings, e.g. `{"useApifyProxy": true, "apifyProxyCountry": "NL"}`. |

### Output

One dataset record per URL. The captured file lives in the run's key-value store; `screenshotUrl` is a direct link to it.

```json
{
  "startUrl": "https://example.com/pricing",
  "url": "https://example.com/pricing",
  "status": "ok",
  "httpStatus": 200,
  "error": null,
  "capturedAt": "2026-09-28T19:33:12.157Z",
  "title": "Pricing — Example",
  "loadTimeMs": 1840,
  "pageWidth": 1280,
  "pageHeight": 4310,
  "imageWidth": 1280,
  "imageHeight": 4310,
  "bytes": 412330,
  "format": "png",
  "fullPage": true,
  "viewport": { "preset": "desktop", "width": 1920, "height": 1080, "deviceScaleFactor": 1, "isMobile": false },
  "screenshotKey": "capture_6f1c2a90b7d4e5f6_example.com_0.png",
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/AbCdEf123/records/capture_...?signature=...",
  "cookieBannerDismissed": true,
  "cookieBannerStillVisible": false,
  "cookieBannerClicked": ["#onetrust-accept-btn-handler"],
  "clickedSelectors": [],
  "hiddenSelectors": [".sticky-header"],
  "blockedRequests": 14,
  "scrollSteps": 9,
  "scrollReachedBottom": true,
  "scrollTruncated": false,
  "hasBaseline": true,
  "changed": true,
  "changePercentage": 1.84,
  "diffReason": "changed",
  "diffKey": "diff_6f1c2a90b7d4e5f6_example.com_0.png",
  "diffUrl": "https://api.apify.com/v2/key-value-stores/AbCdEf123/records/diff_...?signature=...",
  "changedRegions": [
    { "x": 320, "y": 1184, "width": 640, "height": 256, "changePercentage": 22.4 }
  ],
  "baselineScreenshotUrl": "https://api.apify.com/v2/key-value-stores/XyZ789/records/baseline_...?signature=...",
  "baselineCapturedAt": "2026-09-27T06:00:04.221Z"
}
```

`diffReason` is one of `no-baseline` (first run, baseline stored), `identical`, `changed`, `size-mismatch` (the page height or width changed, so a pixel comparison would be meaningless), `image-too-large` (the capture is above 25 megapixels, where a full decode would risk running the container out of memory), `unreadable-baseline` or `disabled`. The diff fields are only present when `visualDiff` is on, so an agent can branch on key presence.

`scrollTruncated` is `true` when `maxScrollSteps` cut the scroll short, which means a very long page may not be fully loaded in the capture. `cookieBannerStillVisible` is `true` when a consent overlay survived the auto-dismiss. `cookieBannerDismissed` is `true` only when something was actually dismissed and nothing is left — a page without any banner reports `false` together with `cookieBannerStillVisible: false`.

### Visual change monitoring

Set `visualDiff` to `true` and schedule the Actor. Each run compares the fresh capture with the baseline stored by the previous run, then updates the baseline. The baseline lives in a dedicated named key-value store, so it survives across runs and is keyed by URL **and** viewport — a mobile baseline is never compared against a desktop capture.

Noise is the enemy of change detection, which is why `blockTracking`, `disableAnimations` and `hideScrollbars` default to on. If a page still contains a live counter or a rotating testimonial, raise `diffMinChangePercentage` rather than turning the diff off. Because the comparison is pixel-based, a page that grows taller is reported as `size-mismatch` instead of being force-cropped into a misleading percentage, and a capture above 25 megapixels is reported as `image-too-large` rather than risking an out-of-memory crash — the screenshot itself is still stored in both cases.

### Use cases

- **Visual regression monitoring** — schedule a set of URLs and get a `changed` flag plus a diff image only when something moved.
- **Price and content change alerts** — combined with a price monitor, catch a competitor quietly rewriting their pricing page.
- **Design review and archiving** — full-page captures of your own pages over time, as a searchable visual archive.
- **Responsive QA** — capture the same page at four device presets in one run.
- **PDF reports and archiving** — turn web pages into A4 PDFs.
- **Content extraction for AI datasets** — screenshots of pages for vision-model training or evaluation.
- **Client deliverables** — a visual snapshot of a website for a review or handover document.

### Pricing

This Actor uses Apify's Pay-Per-Event (PPE) pricing model.

- **Actor Start:** $0.00005 (Apify default)
- **screenshot:** $0.005 per captured page
- **visual\_diff:** $0.002 per pixel comparison against the previous run
- **changed\_page:** $0.005 per page flagged as visually changed

A run that only captures screenshots therefore costs $0.005 per page. Enabling the diff adds $0.002 per page, plus $0.005 for each page that actually changed.

### Legal

This Actor renders and photographs **only the pages you give it**. It does not crawl, does not discover additional pages, does not extract personal data, and does not bypass authentication, paywalls or bot protection. You are responsible for having the right to capture the pages you submit, and for what you do with the resulting images — the same position you are in with any browser's screenshot tool. Consent banners are only ever *accepted* automatically, never rejected; if a site requires a specific privacy choice, turn `dismissCookieBanners` off and handle it yourself. Respect the terms of service of the sites you capture and the rights of the people depicted in them.

### FAQ

**Does the visual diff work on the very first run?**
No. The first run stores a baseline and reports `diffReason: "no-baseline"`. Every run after that compares against it.

**Why did a page come back as `size-mismatch` instead of a percentage?**
The page rendered at a different width or height than the baseline. Padding one image to match the other would invent pixels, so the Actor reports the finding instead of a misleading number.

**The cookie banner is still in my screenshot.**
Check `cookieBannerStillVisible` in the record. If it is `true`, the click did not take effect — add the banner's accept button to `cookieBannerSelectors`, or set `dismissCookieBanners` to `false` to keep the dialog in the picture on purpose.

**Can I capture a site that blocks datacenter IPs?**
Yes — pass an Apify Proxy configuration, for example `{"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US"}`.

**Will screenshots differ slightly between two runs of the same page?**
With the defaults (`blockTracking`, `disableAnimations`, `hideScrollbars` all on) they should not. Live counters, stock tickers and rotating content are the usual culprits — raise `diffMinChangePercentage` or hide them with `hideSelectors`.

**How large can a full-page capture get?**
A long page at a high pixel ratio produces a large image. Lower `deviceScaleFactor` or use `format: "jpeg"` if file size matters more than losslessness.

### Related Actors

- **[Bulk Image Scraper](https://apify.com/codeclouds/bulk-image-scraper)** — when you need the actual image files *inside* a page rather than a picture of the page.
- **[Universal Price Monitor](https://apify.com/codeclouds/universal-price-monitor)** — structured price and stock monitoring; pair it with this Actor to also see when the surrounding page layout changed.
- **[Google Ads Transparency Monitor](https://apify.com/CodeClouds/google-ads-transparency-monitor)** — track the ads your competitors are running.

### Keywords

website screenshot, screenshot generator, full page screenshot, website change monitor, visual regression, pixel diff, website archive, pdf from website, mobile screenshot, page snapshot, visual change detection, competitor monitoring, screenshot api, chrome screenshot

### Changelog

#### 0.1.0

- Initial release: full-page screenshots and PDFs, device presets, lazy-load scrolling, cookie-banner auto-dismiss, ad/analytics blocking, animation freezing, and pixel-diff change detection against the previous run.

# Actor input Schema

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

One or more absolute page URLs to capture, e.g. https://example.com. Duplicates are removed; entries that are not valid http(s) URLs come back as an error record instead of stopping the run.

## `viewport` (type: `string`):

Device viewport to render with. Use a preset for a correct mobile/tablet render, or 'custom' to set the width, height and scale yourself. The preset sizes are fixed so a screenshot taken today has the same pixel dimensions on the next run.

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

Viewport width in CSS pixels. Only used when the device preset is 'custom'. Example: 1280

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

Viewport height in CSS pixels. Only used when the device preset is 'custom'. Example: 800

## `deviceScaleFactor` (type: `integer`):

Pixel density for 'custom' viewports: 1 for standard, 2 for retina. Higher values produce larger files. Example: 2

## `mobileUserAgent` (type: `boolean`):

Use a real mobile/tablet user agent for the mobile and tablet presets, so sites serve their responsive layout. Turn off to render the mobile viewport with the desktop user agent.

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

Capture the entire scrollable page instead of only the visible viewport. Recommended together with 'Scroll to bottom' so lazy-loaded content is included.

## `format` (type: `string`):

PNG for lossless screenshots and the best visual diff, JPEG for smaller files, or PDF for a printable A4 document.

## `imageQuality` (type: `integer`):

Compression quality for JPEG output, 1-100. Only used when the format is JPEG. Example: 80

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

When to consider the page loaded. 'networkidle' is the safest for screenshots; use 'load' for pages that keep a connection open forever (chat widgets, analytics).

## `waitForSelector` (type: `string`):

Wait until this element appears before capturing, e.g. '#results .product'. Leave empty to skip. Example: .pricing-table

## `waitForTimeout` (type: `integer`):

Additional fixed wait after the page is ready, in milliseconds. Use for animations or charts that need a moment. Example: 2000

## `navigationTimeout` (type: `integer`):

How long to wait for the page to load before recording a per-URL error. Example: 45000

## `scrollToBottom` (type: `boolean`):

Scroll through the page in steps before capturing, which triggers lazy-loaded images and infinite-scroll content. Strongly recommended for full-page captures.

## `scrollStepDelay` (type: `integer`):

Pause between scroll steps, giving lazy-loaded content time to appear. Example: 300

## `maxScrollSteps` (type: `integer`):

Safety cap for very long pages. If the cap is hit the record is still returned, with scrollTruncated set to true. Example: 40

## `dismissCookieBanners` (type: `boolean`):

Click the accept button of well-known consent platforms and hide any banner that survives, in multiple languages. Only accepts, never rejects. Turn off if you want the consent dialog in the picture.

## `cookieBannerSelectors` (type: `array`):

Additional CSS selectors to click, tried after the built-in consent-platform list. Example: #accept-cookies

## `clickSelectors` (type: `array`):

CSS selectors to click before the capture, e.g. a 'Show more' link, a language switcher or a tab that must be opened first. Use for elements that do not navigate away. Example: button.expand

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

CSS selectors for elements to make invisible before the capture, e.g. cookie bars, chat widgets or sticky headers. Example: .sticky-header

## `blockTracking` (type: `boolean`):

Block requests to known ad, analytics and chat-widget hosts, and to tracking paths on the page's own host. Makes captures smaller, faster and far more stable between runs - recommended when diffing.

## `disableAnimations` (type: `boolean`):

Disable CSS animations, transitions and video/audio playback before capturing, so a carousel cannot be caught mid-frame. Recommended when diffing.

## `hideScrollbars` (type: `boolean`):

Hide browser scrollbars so they do not appear in full-page captures.

## `visualDiff` (type: `boolean`):

Compare each capture with the stored baseline of the previous run of this actor and report the percentage of changed pixels plus the regions that changed. The first run has no baseline and only stores one. This only works across separate runs: on a one-off run it just seeds the baseline.

## `diffThreshold` (type: `number`):

Per-pixel colour tolerance, 0 (any difference counts) to 1 (only extreme differences count). Lower is more sensitive, which also flags more noise. Example: 0.1

## `diffMinChangePercentage` (type: `number`):

Minimum share of changed pixels before a page counts as changed, in percent. Raise this to ignore rotating banners and live counters. Example: 0.5

## `diffRegionCellSize` (type: `integer`):

Grid cell size used to group changed pixels into reported regions. A bigger grid groups small changes together. Example: 32

## `diffMaxRegions` (type: `integer`):

How many changed regions to report per page, largest first. Example: 10

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

Store this capture as the new baseline after comparing. Turn off to compare against a fixed reference without moving the goalposts.

## `maxPages` (type: `integer`):

Stop after this many URLs. Extra URLs are dropped and the run logs a warning. Example: 20

## `proxy` (type: `object`):

Apify Proxy settings, e.g. {"useApifyProxy": true, "apifyProxyGroups": \["RESIDENTIAL"], "apifyProxyCountry": "NL"}. Leave empty to connect directly.

## Actor input object example

```json
{
  "startUrls": [
    "https://example.com"
  ],
  "viewport": "desktop",
  "viewportWidth": 1280,
  "viewportHeight": 800,
  "deviceScaleFactor": 1,
  "mobileUserAgent": true,
  "fullPage": true,
  "format": "png",
  "imageQuality": 80,
  "waitUntil": "networkidle",
  "waitForTimeout": 0,
  "navigationTimeout": 45000,
  "scrollToBottom": true,
  "scrollStepDelay": 300,
  "maxScrollSteps": 40,
  "dismissCookieBanners": true,
  "cookieBannerSelectors": [],
  "clickSelectors": [],
  "hideSelectors": [],
  "blockTracking": true,
  "disableAnimations": true,
  "hideScrollbars": true,
  "visualDiff": false,
  "diffThreshold": 0.1,
  "diffMinChangePercentage": 0.1,
  "diffRegionCellSize": 32,
  "diffMaxRegions": 10,
  "updateBaseline": true,
  "maxPages": 20,
  "proxy": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

One record per captured URL, in the default dataset of this run.

# 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": [
        "https://example.com"
    ],
    "cookieBannerSelectors": [],
    "clickSelectors": [],
    "hideSelectors": [],
    "proxy": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("codeclouds/website-screenshot-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": ["https://example.com"],
    "cookieBannerSelectors": [],
    "clickSelectors": [],
    "hideSelectors": [],
    "proxy": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("codeclouds/website-screenshot-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 '{
  "startUrls": [
    "https://example.com"
  ],
  "cookieBannerSelectors": [],
  "clickSelectors": [],
  "hideSelectors": [],
  "proxy": {
    "useApifyProxy": true
  }
}' |
apify call codeclouds/website-screenshot-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,codeclouds/website-screenshot-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/CS06I4h76wQb9Pngu/builds/9zryHtvW7UHucfCSN/openapi.json
