# Website Screenshot, PDF & Visual Diff (`hanielazevedo/website-screenshot-pdf-visual-diff`) Actor

Screenshot or PDF any list of URLs: desktop/tablet/mobile, full page or viewport, PNG/JPEG/WebP. Hides cookie banners, blocks ads, dark mode, lazy-load scroll. Visual diff flags changed pages with mismatch %, diff image and changed regions. Pay only for successful captures.

- **URL**: https://apify.com/hanielazevedo/website-screenshot-pdf-visual-diff.md
- **Developed by:** [Haniel Azevedo](https://apify.com/hanielazevedo) (community)
- **Categories:** Developer tools, Automation, SEO tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.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, PDF & Visual Diff

Give it a list of URLs. It returns clean screenshots (full page or viewport, PNG / JPEG / WebP) on desktop, tablet and mobile, optional PDFs, and an optional **visual diff**: was the page changed since last time, by how much (%), and where.

**Pricing:** $0.003 per screenshot, $0.003 per PDF and $0.002 per visual diff, plus a $0.00005 start fee per GB of memory. Failed URLs are free.

It runs a real Chromium browser. It hides cookie banners, can block ads and trackers, scrolls to load lazy images, waits for fonts, and switches off animations so captures stay stable from run to run.

### Who it is for

- **Agencies and QA teams**: check that a deploy did not break the layout (staging vs production) on 3 devices at once.
- **Marketing and SEO**: archive landing pages and competitor pages and get a flag when one changes.
- **Compliance and legal**: PDF + PNG snapshots of terms, pricing or ad pages, with a capture time.
- **Developers and AI agents**: one API call returns image URLs you can put into reports, tickets or LLM prompts.

### What you get

| Feature | Details |
|---|---|
| Devices | Desktop 1440×900, Desktop HD 1920×1080, Laptop 1280×800, Tablet 820×1180 @2x, Mobile 390×844 @3x, Small mobile 360×740 @2x, or a custom viewport |
| Area | Full page (capped by `maxHeight`, default 16,000 px), viewport only, or one element (`elementSelector`) |
| Formats | PNG, JPEG, WebP (quality setting); PDF (A4/Letter/Legal/A3/A5/Tabloid, margins, landscape, scale, screen or print CSS) |
| Clean captures | Cookie banners hidden or accepted (OneTrust, Cookiebot, Didomi, Usercentrics, iubenda and generic patterns), your own selectors to hide or remove, ad/tracker blocking, dark mode, custom CSS, animations off |
| Waiting | `load` / `domcontentloaded` / `networkidle` (capped), wait for a CSS selector, extra delay, lazy-load scrolling |
| Visual diff | Compare with the **previous run** (baseline saved in a named key-value store) or with a **baseline URL / image** (staging vs production). Returns mismatch %, changed pixels, changed regions (boxes) and a red-overlay diff image |
| Reliability | Retries with backoff on network errors, clear error codes, one bad URL never stops the run |
| Output | One dataset row per URL × device, plus the files in the run's key-value store and a run summary (`OUTPUT`) |

### How to use it

1. Paste your URLs into **URLs** (or upload a text/CSV file of URLs).
2. Pick **Devices** and choose **Full page** or viewport.
3. Optional: turn on **PDF**, **Block ads and trackers** or **Dark mode**.
4. Optional, change monitoring: set **Visual diff** to `previousRun`. The first run saves the baselines. Each later run reports `diff.changed` and `diff.mismatchPercent`.
5. Click **Start**. Open the **Overview** tab of the dataset to see thumbnails, or download it as JSON / CSV / Excel.

#### Example input

```json
{
  "urls": [{ "url": "https://example.com" }, { "url": "https://apify.com" }],
  "devices": ["desktop", "mobile"],
  "fullPage": true,
  "format": "png",
  "cookieBanners": "hide"
}
```

#### Example output row

```json
{
  "url": "https://en.wikipedia.org/wiki/Screenshot",
  "device": "desktop",
  "ok": true,
  "statusCode": 200,
  "title": "Screenshot - Wikipedia",
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/…/records/en-wikipedia-org-wiki-Screenshot-8c8873f3-desktop.png?signature=…",
  "width": 1440,
  "height": 4098,
  "truncated": false,
  "cookieBannersHidden": 0,
  "timings": { "navigationMs": 455, "stabiliseMs": 1287, "screenshotMs": 362, "totalMs": 2491 },
  "diff": {
    "changed": true,
    "mismatchPercent": 16.9,
    "changedRegions": [{ "x": 0, "y": 412, "width": 1440, "height": 3010 }],
    "diffImageUrl": "https://api.apify.com/v2/key-value-stores/…-diff.png?signature=…"
  }
}
```

A failed URL still gets a row, with `ok: false`, an `errorCode` and a readable `errorMessage`:

| errorCode | Meaning |
|---|---|
| `INVALID_URL` | The URL could not be parsed (nothing is charged) |
| `DNS`, `CONNECTION`, `SSL` | The site could not be reached |
| `HTTP_4XX`, `HTTP_5XX` | The site answered with an error. Set `captureErrorPages: true` to capture it anyway |
| `TIMEOUT` | The page or the capture took longer than `timeoutSecs` |
| `BLOCKED` | The site served a bot check or a rate limit. **It is not bypassed** (see Limitations) |
| `WAIT_SELECTOR_TIMEOUT`, `SELECTOR_NOT_FOUND` | Your `waitForSelector` / `elementSelector` did not appear |
| `NOT_HTML` | The URL is a download (zip, etc.), not a page |
| `BUDGET_EXCEEDED` | Your maximum charge per run was reached |

### Monitoring pages for changes (schedule)

1. Create a **Task** from this Actor with your URLs and `diffMode: "previousRun"`. Give it its own **Baseline store name** (for example `pricing-pages-baseline`), so separate monitors never mix.
2. Add a **Schedule** to the task (for example daily at 08:00).
3. Add an **Integration** (email, Slack, webhook, Zapier, Make, n8n) that fires when a run finishes. The run summary (`OUTPUT`) holds `changedCount` and `changedUrls`, so you can alert only when something changed.
4. Tune the noise. `diffThresholdPercent` (default 0.1 %) is how much of the page must change before `changed` is true. `pixelThreshold` (0–1) ignores small colour shifts from anti-aliasing. Hide rotating banners or dates with `hideSelectors`.

`updateBaseline: "always"` (default) compares each run with the run before it. `"never"` keeps comparing against the first ("golden") capture.

### API, integrations and AI agents (MCP)

Run it and get the results in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/hanielazevedo~website-screenshot-pdf-visual-diff/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls":[{"url":"https://example.com"}],"devices":["mobile"],"fullPage":false}'
```

Python:

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_TOKEN")
run = client.actor("hanielazevedo/website-screenshot-pdf-visual-diff").call(run_input={"urls": [{"url": "https://example.com"}]})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["url"], row["screenshotUrl"])
```

AI agents (Claude, Cursor, ChatGPT and others) can call it as a tool through the Apify MCP server (`https://mcp.apify.com`). Add this Actor to the server's tools, then ask "screenshot these 5 pages on mobile and tell me which ones changed".

The image links in the output are signed, so they open without your token. They point to the run's storage, which Apify keeps for your plan's data retention period. Download the files if you need them for longer.

### Pricing

Pay per event. Platform usage is included, and you are charged only for files that were actually saved:

| Event | Price | When |
|---|---|---|
| `screenshot` | **$0.003** | Each saved screenshot (one per URL and device) |
| `pdf` | **$0.003** | Each saved PDF |
| `diff` | **$0.002** | Each visual comparison against a baseline |
| Actor start | $0.00005 per GB of memory | Once per run ($0.0001 at the default 2 GB) |

Examples: 1 URL on desktop costs about $0.0031. 100 URLs × 3 devices costs about $0.90. A daily change check of 50 pages (screenshot + diff) costs about $0.25 per run. Failed URLs and invalid input are not charged. Set a maximum total charge per run, and the Actor stops before it goes over.

### Tips for cost and speed

- Viewport captures (`fullPage: false`) and JPEG/WebP are the fastest and smallest.
- Very long pages are cut at `maxHeight` (default 16,000 px) and flagged with `truncated: true`. Lower it for faster runs.
- The default memory is 2 GB. Big batches of long pages run faster with 4–8 GB, because the platform adds CPU with memory and the Actor renders one capture per CPU core.
- `maxConcurrency` (default 3) sets how many pages load at the same time.

### FAQ

**Does it work on pages behind a login?** No. It captures public pages only. It does not log in, solve captchas or get around bot protection.

**Why is my screenshot cut off?** The page is taller than `maxHeight`, and the row shows `truncated: true`. Raise `maxHeight` (up to 30,000 px), or use a PDF for very long documents.

**A cookie banner still shows up.** Add its CSS selector to `hideSelectors`, or try `cookieBanners: "accept"`.

**Why does the diff flag a page as changed every time?** Rotating content (ads, carousels, timestamps) changes on every load. Hide those elements with `hideSelectors`, turn on `blockAdsAndTrackers`, or raise `diffThresholdPercent`.

**Can I use my own proxy?** Yes. Use `proxyUrl` (stored as a secret input).

**What am I charged for?** Only successful work: each screenshot, each PDF and each diff comparison. Failed URLs and invalid input are not charged. Platform usage is included. See [Pricing](#pricing).

### Limitations

- Public pages only: no login, captcha solving or bot-protection bypass. Sites that serve a challenge page return `BLOCKED`.
- Full-page captures stop at `maxHeight`. Infinite-scroll feeds are captured up to that height.
- Video, canvas/WebGL animations and some cross-origin iframes may render as their first frame or as blank.
- Diffs compare pixels. A page that moves everything down by 1 line shows a large change even when the text barely changed.
- Please respect each website's terms of use and robots rules, and capture only content you are allowed to store.

### Compared with the most-used free screenshot Actor (checked 31 Oct 2026)

| | This Actor | apify/screenshot-url |
|---|---|---|
| Device presets (desktop / tablet / mobile in one run) | ✅ | ❌ (viewport width only) |
| Output formats | PNG, JPEG, WebP + PDF (paper size, margins, landscape) | PNG or PDF |
| Visual diff with mismatch %, regions and diff image | ✅ | ❌ |
| Cookie banner handling | ✅ automatic | ❌ (manual `selectorsToHide`) |
| Ad / tracker blocking, dark mode, custom CSS | ✅ | ❌ |
| Element-only capture | ✅ | ❌ |
| Lazy-load scrolling, hide selectors | ✅ | ✅ |

# Actor input Schema

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

Pages to capture. One row per URL × device. Bare domains get https:// added.

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

One capture per URL per device.

## `captureScreenshot` (type: `boolean`):

Save an image of the page.

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

On: the whole scrollable page (up to Max height). Off: only the first screen (viewport).

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

PNG is lossless; JPEG/WebP are much smaller.

## `quality` (type: `integer`):

Ignored for PNG.

## `maxHeight` (type: `integer`):

Full-page captures are cut at this height (row gets truncated: true). Stops infinite-scroll pages from producing giant images.

## `elementSelector` (type: `string`):

CSS selector: capture only this element (e.g. #pricing). Fails with SELECTOR_NOT_FOUND if missing.

## `capturePdf` (type: `boolean`):

Save a print PDF of each page (charged as a separate 'pdf' event).

## `pdfFormat` (type: `string`):

Paper size for PDFs.

## `pdfLandscape` (type: `boolean`):

Landscape orientation for PDFs.

## `pdfPrintBackground` (type: `boolean`):

Include background colours and images in PDFs.

## `pdfMargin` (type: `string`):

CSS length for all sides: 0, 10mm, 0.5in, 20px.

## `pdfMedia` (type: `string`):

Which CSS the PDF uses: the normal screen look or the site's print stylesheet.

## `pdfScale` (type: `number`):

Zoom of the PDF render (1 = 100%).

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

networkidle waits up to the network-idle cap and then captures anyway, so chatty sites never hang.

## `networkIdleTimeoutMs` (type: `integer`):

Longest wait for network idle before capturing anyway.

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

CSS selector that must appear before capture (fails with WAIT_SELECTOR_TIMEOUT).

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

Fixed wait right before capture, for animations or late widgets.

## `scrollToLoad` (type: `boolean`):

Scrolls down the page to trigger lazy loading, then back to the top before capture.

## `timeoutSecs` (type: `integer`):

Per-URL timeout (s).

## `maxRetries` (type: `integer`):

Retries with exponential backoff for timeouts, connection errors, 5xx and crashes. 4xx and blocked pages are not retried.

## `maxConcurrency` (type: `integer`):

3 is right for 2 GB memory; raise memory before raising this.

## `cookieBanners` (type: `string`):

Cookie banners.

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

CSS selectors made invisible (layout kept), e.g. chat widgets, sticky headers.

## `removeSelectors` (type: `array`):

CSS selectors removed from layout (display:none).

## `blockAdsAndTrackers` (type: `boolean`):

Blocks requests to common ad/analytics hosts. Faster, cleaner shots.

## `darkMode` (type: `boolean`):

Emulates prefers-color-scheme: dark (only sites that support it change).

## `customCss` (type: `string`):

Injected into every page before capture.

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

Stops CSS animations/transitions and the caret for stable, diff-friendly shots.

## `diffMode` (type: `string`):

previousRun: keeps the last screenshot per URL+device+settings in a named key-value store and compares the next run against it. The first run only creates baselines.

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

Changed if mismatch above (%).

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

0 = any colour change counts; 0.1 ignores anti-aliasing/compression noise.

## `updateBaseline` (type: `string`):

Update baseline.

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

Named key-value store this Actor creates in your account to keep baselines. Use different names for different monitors.

## `baselineStore` (type: `string`):

Optional. Overrides the name above. The Actor gets read/write access to this store only.

## `comparePairs` (type: `array`):

\[{"url":"https://staging.example.com","baselineUrl":"https://example.com"}]. baselineUrl may be a page or an image URL (.png/.jpg/.webp).

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

Width for the 'custom' device.

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

Height for the 'custom' device (also the viewport-capture height).

## `deviceScaleFactor` (type: `number`):

Pixel ratio for the 'custom' device (2 = retina).

## `isMobile` (type: `boolean`):

Emulate a touch phone (mobile layout, mobile user agent) for the 'custom' device.

## `userAgent` (type: `string`):

Replace the browser user agent for all devices.

## `locale` (type: `string`):

Browser language, e.g. pt-BR. Sent as Accept-Language.

## `timezoneId` (type: `string`):

e.g. Europe/Lisbon

## `captureErrorPages` (type: `boolean`):

Off: error pages fail (free). On: they are captured and charged.

## `ignoreHttpsErrors` (type: `boolean`):

Capture sites with expired or self-signed certificates.

## `proxyUrl` (type: `string`):

Optional http://user:pass@host:port of your own proxy.

## `maxUrls` (type: `integer`):

Safety cap on the number of URLs processed.

## Actor input object example

```json
{
  "urls": [
    {
      "url": "https://example.com"
    }
  ],
  "devices": [
    "desktop"
  ],
  "captureScreenshot": true,
  "fullPage": true,
  "format": "png",
  "quality": 80,
  "maxHeight": 16000,
  "capturePdf": false,
  "pdfFormat": "A4",
  "pdfLandscape": false,
  "pdfPrintBackground": true,
  "pdfMargin": "10mm",
  "pdfMedia": "screen",
  "pdfScale": 1,
  "waitUntil": "load",
  "networkIdleTimeoutMs": 5000,
  "delayMs": 0,
  "scrollToLoad": true,
  "timeoutSecs": 60,
  "maxRetries": 2,
  "maxConcurrency": 3,
  "cookieBanners": "hide",
  "blockAdsAndTrackers": false,
  "darkMode": false,
  "disableAnimations": true,
  "diffMode": "off",
  "diffThresholdPercent": 0.1,
  "pixelThreshold": 0.1,
  "updateBaseline": "always",
  "baselineStoreName": "screenshot-baselines",
  "viewportWidth": 1440,
  "viewportHeight": 900,
  "deviceScaleFactor": 1,
  "isMobile": false,
  "locale": "en-US",
  "captureErrorPages": false,
  "ignoreHttpsErrors": false,
  "maxUrls": 1000
}
```

# Actor output Schema

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

No description

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

No description

## `summary` (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"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hanielazevedo/website-screenshot-pdf-visual-diff").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("hanielazevedo/website-screenshot-pdf-visual-diff").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 hanielazevedo/website-screenshot-pdf-visual-diff --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hanielazevedo/website-screenshot-pdf-visual-diff"
        }
    }
}
```

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/nCMWMrxE0v7uHaSob/builds/rKSAypNqDFlakGRuu/openapi.json
