# Clean Screenshot & Change Monitor (`loris-barce/clean-screenshot-monitor`) Actor

Clean website screenshots without cookie banners, popups or chat widgets. Desktop, mobile and tablet in one run, PDF export, and a change monitor that tells you when a page changed (pixels + text).

- **URL**: https://apify.com/loris-barce/clean-screenshot-monitor.md
- **Developed by:** [Loris Barce](https://apify.com/loris-barce) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 screenshot takens

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

## Clean Screenshot & Change Monitor

Take **clean screenshots** of any public web page: no cookie banners, newsletter popups or chat bubbles. Capture **desktop, mobile and tablet** in a single run, export **PDFs**, and turn on the **change monitor** to find out when a page changed since the last run, with a pixel diff, a visible-text diff and a diff image that marks changes in red.

- 🧹 **Clean pages.** The Actor clicks *Reject* (or *Accept*) on the most common consent managers: OneTrust, Cookiebot, Didomi, Iubenda, Quantcast, TrustArc, Usercentrics, CookieYes, Complianz and Osano. It then hides leftover overlays and the Intercom, Drift, Tawk, Zendesk, HubSpot, Crisp and LiveChat widgets.
- 📱 **Desktop + mobile + tablet in one run.** Desktop is 1920×1080; mobile uses an iPhone 15 profile and tablet an iPad profile, both with real device pixel ratio, user agent and touch.
- 📄 **PDF export.** The whole page becomes one tall PDF page that looks exactly like the screenshot.
- 🔔 **Change monitor.** Each run is compared with the previous one: `changed`, `% pixels changed`, `textChanged`, a text diff snippet and a red-highlighted diff image. Dates, counters and rotating banners can be ignored with CSS selectors.
- 💸 **Pay only for results.** You pay per saved screenshot and per change check. Failed URLs cost nothing.

### Use cases

| Use case | How |
|---|---|
| **Monitor competitor prices and landing pages** | Schedule daily with `monitorMode: true` and get notified when `changedCount > 0`. |
| **Web page archive / compliance evidence** | Save PDFs or full-page PNGs of terms, pricing pages or ads on a schedule. |
| **Visual QA** | Capture your site on desktop + mobile after each deploy and diff it against the previous release. |
| **Clean screenshots for reports and decks** | No cookie walls or chat bubbles covering the content. |

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `urls` | array | (required) | Pages to capture, max 500 per run. Also accepts a remote text file of URLs (`requestsFromUrl`). |
| `devices` | array | `["desktop"]` | Any of `desktop` (1920×1080), `mobile` (iPhone), `tablet` (iPad). |
| `fullPage` | boolean | `true` | Whole scrollable page instead of the viewport only. |
| `format` | string | `png` | `png`, `jpeg` or `pdf` (PDF is desktop only). |
| `cleanPage` | boolean | `true` | Remove cookie banners, popups and chat widgets. |
| `hideSelectors` | array | `[]` | Extra CSS selectors to hide (always applied). |
| `waitFor` | string | `networkidle` | `load`, `networkidle` or `selector`. |
| `waitSelector` | string | | Selector to wait for when `waitFor` is `selector`. |
| `delayMs` | integer | `1000` | Extra wait before capturing (max 15000). |
| `monitorMode` | boolean | `false` | Compare with the previous run. |
| `monitorStoreName` | string | `screenshot-monitor` | Named key-value store holding the reference screenshots. Use different names for separate monitors. |
| `diffThresholdPercent` | number | `1.0` | Pixel changes below this % count as "no change". A text change always counts. |
| `ignoreSelectors` | array | `[]` | Areas to ignore in the comparison (both pixels and text). |
| `proxyConfiguration` | object | no proxy | Optional Apify Proxy or your own proxies. |

#### Example: clean screenshots on desktop and mobile

```json
{
    "urls": [{ "url": "https://www.example.com" }, { "url": "https://www.example.org/pricing" }],
    "devices": ["desktop", "mobile"]
}
```

#### Example: PDF archive

```json
{
    "urls": [{ "url": "https://www.example.com/terms" }],
    "format": "pdf"
}
```

#### Example: daily price monitor

```json
{
    "urls": [{ "url": "https://competitor.example.com/pricing" }],
    "devices": ["desktop", "mobile"],
    "monitorMode": true,
    "monitorStoreName": "competitor-pricing",
    "ignoreSelectors": [".last-updated", "#visitor-counter", ".testimonial-carousel"],
    "diffThresholdPercent": 0.5
}
```

### Output

One dataset row per URL × device:

```json
{
    "url": "https://competitor.example.com/pricing",
    "device": "desktop",
    "format": "png",
    "imageUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/3f2a9c1d7e4b8a60-desktop.png",
    "width": 1920,
    "height": 4210,
    "bytes": 812345,
    "title": "Pricing – Competitor",
    "status": "ok",
    "error": null,
    "changed": true,
    "pixelChangePercent": 2.84,
    "textChanged": true,
    "textDiffSnippet": "-$49 / month\n+$59 / month",
    "diffImageUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/3f2a9c1d7e4b8a60-desktop-diff.png"
}
```

- The monitor fields (`changed` … `diffImageUrl`) are only present when `monitorMode` is on. On the **first run** there is nothing to compare with yet, so `changed` is `null` and the screenshot becomes the reference.
- The reference is replaced **only when a change is detected**, so slow drifts below the threshold still add up and get reported.
- A failed URL gives `status: "failed"` with the reason in `error`. It never stops the other URLs.

The run summary is stored in the default key-value store under the `OUTPUT` key:

```json
{ "total": 4, "succeeded": 4, "failed": 0, "changedCount": 1, "durationSec": 21.4, "skipped": 0, "stoppedBySpendLimit": false }
```

`total`, `succeeded` and `failed` count URL × device captures. The run **succeeds if at least one capture succeeded**.

### Schedule a daily monitor with notifications

1. Save your input as a **task** (e.g. *Competitor pricing monitor*).
2. In **Schedules**, create a schedule (e.g. `0 8 * * *`, every day at 8:00) and add the task.
3. Get notified about changes. Pick one:
   - **Slack / email integration** (task → *Integrations*): notifies on every successful run. Open the dataset's *Change monitor* view to see what changed.
   - **Only when something changed:** add a **webhook** on `ACTOR.RUN.SUCCEEDED` pointing to Make, Zapier, n8n or your own endpoint, with this payload template:

     ```json
     {
         "runId": {{resource.id}},
         "outputUrl": "https://api.apify.com/v2/key-value-stores/{{resource.defaultKeyValueStoreId}}/records/OUTPUT",
         "datasetUrl": "https://api.apify.com/v2/datasets/{{resource.defaultDatasetId}}/items?clean=true"
     }
     ```

     In your scenario, fetch `outputUrl`, continue only if `changedCount > 0`, then post the rows where `changed` is `true` (with their `diffImageUrl`) to Slack, email or a ticket.

### Pricing

Pay-per-event pricing: you pay only for successful results.

| Event | Price | When |
|---|---|---|
| `screenshot-taken` | **$0.001** | Each image or PDF saved successfully |
| `change-check` | **$0.003** | Each comparison in monitor mode (not charged on the first run, when the reference is created) |

Examples:

- 1,000 clean screenshots: **$1.00**
- Monitoring 50 pages × desktop + mobile, every day for 30 days: 3,000 × ($0.001 + $0.003) = **$12.00**

Failed URLs are never charged. If you set a **maximum cost per run**, the Actor stops cleanly once the next capture would exceed it. It saves `OUTPUT` with `stoppedBySpendLimit: true`, reports the `skipped` captures, and the run still finishes as **SUCCEEDED**.

### FAQ

**Can it capture pages behind a login?**
No. Login, sessions and user cookies are not supported, by design. Only public pages are captured.

**Does it solve CAPTCHAs or bypass anti-bot protection or paywalls?**
No. If a site shows a CAPTCHA or blocks automated browsers, the screenshot will show that page. A proxy (`proxyConfiguration`) can help with geo-restricted content, but the Actor doesn't try to defeat bot protection.

**Why does the Actor click "Reject" instead of "Accept"?**
Rejecting is privacy-friendly and usually loads fewer third-party scripts, so the screenshot shows the page as a new visitor sees it. If there is no reject button, it clicks accept. Any banner still left is hidden.

**A banner or popup is still visible on my site.**
Add its CSS selector to `hideSelectors`. Consent managers change often, and the built-in list (`src/consent_selectors.py`) is updated regularly.

**Why is the mobile screenshot 1179 px wide?**
Mobile and tablet use the real device pixel ratio (iPhone 15: 393 CSS px × 3), so images are as sharp as on the device.

**Is there a limit on page length?**
Yes. A full-page capture is limited to about 40 megapixels: roughly 20,800 px of height on desktop and about 11,000 CSS px on mobile (at 3× pixel density). Longer pages, such as endless news homepages, are cut at that height, and the run log says so. Very tall pages are captured in slices and stitched together, so they don't crash the browser.

**My monitor reports changes every day, but nothing important changed.**
Add the dynamic areas (dates, counters, carousels, ads) to `ignoreSelectors`, or raise `diffThresholdPercent`. Text changes always count, so put elements with changing text in `ignoreSelectors`.

**Where are the reference screenshots?**
In the named key-value store `monitorStoreName` in your account. Delete or rename the store to start from scratch. Changing `fullPage` also resets the reference.

**How long are the screenshot URLs valid?**
Screenshots are saved in the run's default key-value store, which follows your plan's data-retention period. Download or export what you want to keep.

**Which URLs are refused?**
Only `http`/`https` URLs on public addresses are captured. Local files, `localhost`, private networks and cloud metadata addresses are refused for security.

***

### 🇮🇹 In breve (italiano)

**Clean Screenshot & Change Monitor** cattura screenshot **puliti** di qualsiasi pagina pubblica: senza banner dei cookie, popup e chat. In un solo run ottieni **desktop, mobile e tablet**, puoi esportare in **PDF** e attivare il **monitoraggio delle modifiche**.

- **Pulizia automatica:** clicca "Rifiuta" (o "Accetta") sui consent manager più diffusi (OneTrust, Cookiebot, Didomi, Iubenda, Quantcast, TrustArc, Usercentrics…) e nasconde gli overlay rimasti e i widget di chat.
- **Monitor:** con `monitorMode: true` ogni run viene confrontato con il precedente. Ottieni `changed`, la percentuale di pixel cambiati, `textChanged`, un estratto delle differenze di testo e un'immagine con le modifiche evidenziate in rosso. Usa `ignoreSelectors` per escludere date, contatori e caroselli.
- **Casi d'uso:** monitorare prezzi e landing dei concorrenti, archiviare pagine (PDF), QA visuale dopo i deploy, screenshot puliti per report.
- **Prezzi:** $0,001 per screenshot salvato e $0,003 per confronto. Gli URL falliti non si pagano. Al raggiungimento del limite di spesa il run si ferma in modo pulito e risulta comunque SUCCEEDED.
- **Limiti:** niente pagine con login, niente risoluzione di CAPTCHA, nessun aggiramento di paywall o protezioni anti-bot.

Per un monitoraggio giornaliero: salva l'input come task, crea uno **Schedule** (es. ogni mattina alle 8) e aggiungi un'integrazione Slack/email oppure un webhook che invia una notifica solo quando `changedCount > 0`.

# Actor input Schema

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

Pages to capture (max 500 per run). You can also load a list of URLs from a remote text file.

## `devices` (type: `array`):

Capture each URL on these devices. Desktop is 1920x1080, mobile is an iPhone 15 profile, tablet is an iPad (7th gen) profile.

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

Capture the whole scrollable page instead of just the visible viewport.

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

Image format, or PDF. PDF is only produced for the desktop device; other devices are skipped when PDF is selected.

## `cleanPage` (type: `boolean`):

Dismiss cookie consent banners (OneTrust, Cookiebot, Didomi, Iubenda, Quantcast, TrustArc, Usercentrics and more), hide popups/overlays and chat widgets before capturing.

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

CSS selectors of additional elements to hide before capturing (always applied, even when 'Clean page' is off).

## `waitFor` (type: `string`):

When the page counts as ready. 'Network idle' is the safest default; 'Selector' waits for the element in 'Wait selector'.

## `waitSelector` (type: `string`):

CSS selector to wait for when 'Wait for' is set to 'Selector'.

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

Additional wait after the page is ready, for animations and late content.

## `monitorMode` (type: `boolean`):

Compare each screenshot with the one from the previous run and report whether the page changed (pixel diff + visible-text diff). Schedule the Actor to monitor pages over time.

## `monitorStoreName` (type: `string`):

Name of the persistent key-value store that keeps the reference screenshots. Use different names to keep separate monitors apart.

## `diffThresholdPercent` (type: `number`):

Pixel changes below this percentage of the page count as 'no change'. A text change always counts as a change.

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

CSS selectors of areas to ignore in the comparison (e.g. dates, counters, rotating banners). Both their pixels and their text are excluded.

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

Optional proxy. No proxy is used by default.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com"
    }
  ],
  "devices": [
    "desktop"
  ],
  "fullPage": true,
  "format": "png",
  "cleanPage": true,
  "hideSelectors": [],
  "waitFor": "networkidle",
  "delayMs": 1000,
  "monitorMode": false,
  "monitorStoreName": "screenshot-monitor",
  "diffThresholdPercent": 1,
  "ignoreSelectors": [],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset rows: url, device, format, imageUrl, width, height, bytes, title, status, error and (monitor mode) changed, pixelChangePercent, textChanged, textDiffSnippet, diffImageUrl.

## `summary` (type: `string`):

OUTPUT record: total, succeeded, failed, changedCount, durationSec, skipped, stoppedBySpendLimit.

## `files` (type: `string`):

Key-value store with the captured images/PDFs ({hash}-{device}.{ext}) and diff images.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

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

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        {
            "url": "https://example.com"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("loris-barce/clean-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 = { "urls": [{ "url": "https://example.com" }] }

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,loris-barce/clean-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/5nB2slTgrtsXUd5HI/builds/S5bwcUCbIVRJMfWD2/openapi.json
