# Bulk Website Screenshots + PDF + Visual Change Detection (`lindenbyte/bulk-website-screenshots`) Actor

Screenshot hundreds of URLs per run: full page or viewport, desktop/laptop/tablet/mobile, PNG/JPEG/WebP, optional PDF. Hides cookie banners and chat widgets. Schedule it to detect visual changes since the last run.

- **URL**: https://apify.com/lindenbyte/bulk-website-screenshots.md
- **Developed by:** [Lindenbyte Studio](https://apify.com/lindenbyte) (community)
- **Categories:** Developer tools, SEO tools
- **Features:** Limited permissions
- **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.

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

## Bulk Website Screenshots + PDF + Visual Change Detection

Take **screenshots of hundreds of websites in one run**: full page or above the fold, on **desktop, laptop, tablet and mobile**, as **PNG, JPEG or WebP**, with an optional **PDF** of each page. Cookie banners and chat bubbles are hidden automatically. Schedule it and it becomes a **visual website monitor**: each screenshot is compared with the previous run and flagged when the page changed.

### Why this one

- **Many URLs per run**: paste a list of 10 or 10,000 URLs. No need to start one run per URL.
- **Every device in one go**: Desktop 1920x1080, Laptop 1366x768, Tablet 820x1180 @2x (iPad-like), Mobile 390x844 @2x (iPhone-like user agent and touch), or any custom size and pixel ratio.
- **Clean screenshots**: hides common cookie/consent popups (OneTrust, Cookiebot, Usercentrics, Didomi, Quantcast, Google consent, Sourcepoint, CookieYes, Complianz and others) and chat widgets (Intercom, HubSpot, Drift, Crisp, Zendesk, Tidio and others). Add your own CSS selectors or custom CSS. Optional ad and tracker blocking.
- **Lazy images load**: the page is scrolled to the bottom before a full-page capture, so lazy-loaded images and sections are actually there.
- **Full page or viewport**, PNG (lossless), JPEG or WebP with a quality setting.
- **PDF export**: A4, Letter, Legal, A3 or Tabloid, portrait or landscape, screen or print styles.
- **Visual change detection**: `changePercent` and `isChanged` per URL and device versus the previous run, with a threshold. Turn on "only changed pages" and a scheduled run outputs (and charges) only the pages that changed.
- **Fair billing**: failed pages and HTTP error pages (404, 403, 5xx) are not charged.

### Use cases

- **Website monitoring**: see when a competitor changes pricing, homepage, landing pages or terms. Get a screenshot of the new version and of the previous one.
- **Visual QA and regression checks**: capture every page of your site on every device after a deploy.
- **Agencies and SEO**: client reports, before/after redesign snapshots, mobile-friendliness checks.
- **Archiving and compliance**: dated full-page screenshots and PDFs of pages, ads and policies.
- **Thumbnails and previews** for directories, link previews and dashboards.
- **AI and data pipelines**: page images for vision models, alongside the title, final URL and HTTP status.

### Input

| Field | What it does |
|---|---|
| `urls` | Pages to capture, one per line (`https://` added if missing) |
| `devices` | `desktop`, `laptop`, `tablet`, `mobile`, `custom` (one screenshot per URL per device) |
| `fullPage` | Whole scrollable page (default) or only the visible viewport |
| `format`, `quality` | `png`, `jpeg` or `webp`; quality 1-100 for JPEG/WebP |
| `savePdf`, `pdfFormat`, `pdfLandscape`, `pdfMedia` | Also print each URL to PDF (one PDF per URL) |
| `waitUntil`, `delayMs`, `waitForSelector` | When to capture: load / DOM ready / network idle, extra delay, wait for an element |
| `scrollToBottom` | Scroll first so lazy images load (full page) |
| `hideCookieBanners`, `hideChatWidgets` | Hide known consent popups and chat widgets (best effort) |
| `hideSelectors`, `customCss` | Your own CSS selectors to hide and CSS to inject |
| `blockAds` | Block ~45 common ad and analytics hosts |
| `compareWithPrevious`, `changeThreshold`, `onlyChanged`, `stateKey` | Monitoring mode (see below) |
| `customWidth`, `customHeight`, `customDeviceScaleFactor`, `customIsMobile` | The `custom` device |
| `maxFullPageHeight` | Cut very long pages (default 15,000 px) |
| `concurrency`, `maxRetries`, `navigationTimeoutSecs` | Speed and robustness |
| `maxUrls` | Cost cap per run |
| `proxyConfiguration` | Optional Apify Proxy (off by default) |

#### Example: desktop + mobile screenshots and PDFs

```json
{
  "urls": ["https://example.com", "https://crawlee.dev", "https://www.theguardian.com/international"],
  "devices": ["desktop", "mobile"],
  "format": "png",
  "savePdf": true
}
```

#### Example: daily visual change monitor

```json
{
  "urls": ["https://competitor.com/pricing", "https://competitor.com/"],
  "devices": ["desktop"],
  "onlyChanged": true,
  "changeThreshold": 1,
  "hideSelectors": [".rotating-banner", "#live-clock"]
}
```

Create a schedule (Console > Schedules) and add an integration (email, Slack, webhook) to be notified with the changed pages.

### Output

One row per URL and device (real example from a test run; on Apify the URLs contain your run's key-value store ID):

```json
{
  "url": "https://example.com/",
  "device": "desktop",
  "finalUrl": "https://example.com/",
  "statusCode": 200,
  "title": "Example Domain",
  "width": 1920,
  "height": 1080,
  "viewportWidth": 1920,
  "viewportHeight": 1080,
  "deviceScaleFactor": 1,
  "pageHeight": 1080,
  "truncated": false,
  "format": "png",
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/example-com-desktop-b559c7ed.png",
  "screenshotKey": "example-com-desktop-b559c7ed.png",
  "bytes": 90088,
  "pdfUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/example-com-print-b559c7ed.pdf",
  "pdfKey": "example-com-print-b559c7ed.pdf",
  "pdfBytes": 91727,
  "changePercent": 0,
  "isChanged": false,
  "previousCapturedAt": "2026-11-13T10:36:30.781Z",
  "previousScreenshotUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/example-com-desktop-b559c7ed.png",
  "warnings": [],
  "error": null,
  "capturedAt": "2026-11-13T10:41:16.940Z"
}
```

- `screenshotUrl` and `pdfUrl` open the files directly. The images are also in the run's key-value store, and the dataset table shows a preview.
- The PDF is attached to the first device's row of each URL. Its print layout depends on the paper size, not the device.
- The monitoring fields (`changePercent`, `isChanged`, `previousCapturedAt`, `previousScreenshotUrl`) appear only in monitoring mode. On the first capture of a URL they are `null` (baseline).
- Failed pages produce a row with `error` set (not charged). Pages that answer with HTTP 4xx/5xx are captured for inspection, flagged with `error: "HTTP 404"` and not charged.
- A run summary (counts, failures) is saved to the key-value store record `SUMMARY`.

### Monitoring mode (visual change detection)

With `compareWithPrevious` (or `onlyChanged`) on, each screenshot is reduced to a small grayscale thumbnail and compared with the previous run's thumbnail for the same URL, device and capture mode. `changePercent` is the share of thumbnail pixels that changed. Extra or missing page length counts as changed. `isChanged` is true when it reaches `changeThreshold` (default 1%).

- History is kept in your account's named key-value store `bulk-screenshots-state`. Use different `stateKey` names for independent monitors. Change the name to start a fresh baseline.
- With `onlyChanged`, unchanged pages are compared but not saved or output.
- Animations are frozen and the caret is hidden for stable captures. Pages with rotating carousels, live tickers, ads or clocks still change between runs. Hide those parts with `hideSelectors` or raise the threshold.

### Pricing

Pay per result: $0.003 per screenshot saved, $0.0015 per PDF, $0.0025 per GB of run memory at start ($0.005 at the default 2 GB). Failed pages and HTTP error pages are free. In "only changed" mode an unchanged page costs $0.001 (it was still captured and compared). Set a maximum charge per run in the run options to cap your cost.

### Limits (honest)

- **Cookie banner hiding is best effort.** It covers the major consent platforms and common patterns, but custom banners on some sites remain. Add their selectors to `hideSelectors`. Banners are hidden, never accepted or rejected.
- **Bot protection**: sites behind aggressive bot walls (some news, ticketing and retail sites) can return 401/403 or a challenge page. Those rows are flagged and not charged. Apify Proxy (residential) helps for some sites. CAPTCHAs are not solved.
- **Very long pages** are cut at `maxFullPageHeight` (default 15,000 px). Retina devices are also limited to 16,384 image pixels, which is 8,192 CSS px at @2x. Endless-scroll feeds are always cut.
- **Login-only pages** are not supported (no cookies or credentials input in this version).
- Full-page screenshots of heavy sites are large: a 15,000 px PNG can be 5-10 MB. Use JPEG or WebP to save storage.
- Screenshots are rendered by headless Chromium. Fonts, video frames and WebGL can differ slightly from a desktop browser.
- Each run's files live in that run's key-value store and follow your account's data retention. Old `previousScreenshotUrl` links can expire.

### Legal

You choose the URLs. Capture only pages you are allowed to access, and respect the websites' terms and copyright when you reuse the images. This Actor does not bypass logins or paywalls and does not solve CAPTCHAs. Not affiliated with any website it captures.

# Actor input Schema

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

Web pages to capture, one per line (https:// is added if missing). Duplicates are removed. Each URL is captured once per selected device.

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

One screenshot per URL per device. Desktop 1920x1080, Laptop 1366x768, Tablet 820x1180 @2x (iPad-like), Mobile 390x844 @2x (iPhone-like, mobile user agent and touch). Custom uses the size fields below.

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

On = capture the whole scrollable page (up to the max height below). Off = only the visible viewport (above the fold).

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

PNG is pixel-perfect but large for full pages; JPEG and WebP use the quality setting below.

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

Compression quality for JPEG and WebP (1-100). Ignored for PNG.

## `savePdf` (type: `boolean`):

Print each URL to a PDF (one PDF per URL, not per device; print layout depends on the paper size, not the device). Charged per PDF.

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

Paper size of the PDF.

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

Landscape orientation for the PDF.

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

Screen = the PDF looks like the website; Print = uses the site's print stylesheet (often hides menus and ads).

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

When the page counts as loaded. If the event is not reached in time the page is captured anyway, with a warning.

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

Wait this long after loading (and scrolling) before taking the screenshot, for animations and late content.

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

Optional: wait until an element matching this CSS selector is visible (for example #main or .product-grid). If it never appears, the page is captured anyway with a warning.

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

For full-page shots: scroll down step by step (max 15 s) so lazy-loaded images and sections render, then scroll back to the top.

## `hideCookieBanners` (type: `boolean`):

Hide common cookie/consent popups (OneTrust, Cookiebot, Usercentrics, Didomi, Quantcast, Google consent, CookieYes, Complianz and more) with CSS and unlock page scrolling. Best effort: unknown or custom banners may remain; add their selectors below.

## `hideChatWidgets` (type: `boolean`):

Hide common chat bubbles (Intercom, HubSpot, Drift, Crisp, Tawk, Zendesk, Tidio, LiveChat and more). Best effort.

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

Your own CSS selectors to hide before capture, one per line (for example .newsletter-popup or #promo-bar). Also useful in monitoring mode to hide rotating banners or clocks that would count as changes.

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

Optional CSS added to every page before capture, for example: header { position: static !important; }

## `blockAds` (type: `boolean`):

Block requests to ~45 common ad and analytics hosts (Google ads/analytics/tag manager, Meta pixel, Hotjar, Taboola, Outbrain, Criteo...). Faster, cleaner screenshots; the page may look slightly different from what visitors see.

## `compareWithPrevious` (type: `boolean`):

Compare each screenshot with the previous run's screenshot of the same URL and device. Adds changePercent, isChanged, previousCapturedAt and previousScreenshotUrl. The comparison data lives in your named key-value store 'bulk-screenshots-state'. Schedule the Actor to monitor pages.

## `changeThreshold` (type: `integer`):

A page counts as changed when at least this percent of (downscaled) pixels differ from the previous run. 0 = any difference. Raise it for pages with small rotating content.

## `onlyChanged` (type: `boolean`):

Monitoring mode: save and output only pages that changed since the previous run (plus the first capture of a new URL). Unchanged pages are compared but not saved or output. Turns on change detection automatically.

## `stateKey` (type: `string`):

Monitoring mode: separate monitors (for example one per client or schedule) keep separate history when they use different names. Change it to start a fresh baseline.

## `customWidth` (type: `integer`):

Viewport width for the 'custom' device.

## `customHeight` (type: `integer`):

Viewport height for the 'custom' device (also the screenshot height when full page is off).

## `customDeviceScaleFactor` (type: `integer`):

Device pixel ratio for the 'custom' device: 2 = retina (image is twice the width and height).

## `customIsMobile` (type: `boolean`):

Emulate a mobile browser (mobile user agent, touch, meta viewport) for the 'custom' device.

## `maxFullPageHeight` (type: `integer`):

Full-page screenshots are cut at this height (CSS pixels; for @2x/@3x devices also limited to 16,384 image pixels). Endless-scroll pages are always cut here.

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

Maximum time to load a page.

## `waitForSelectorTimeoutSecs` (type: `integer`):

How long to wait for the selector above.

## `concurrency` (type: `integer`):

How many pages are captured at the same time. 2 is safe for the default 2 GB memory with heavy full-page sites; give 4 GB for 3-5 and 8 GB for 6-10.

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

Retry a page this many times if it fails to load.

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

Cost cap: only the first N URLs are captured. You can also set a max charge per run in the run options.

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

Optional. Off by default (direct connection). Use Apify Proxy if sites block data-center traffic or you need a specific country.

## Actor input object example

```json
{
  "urls": [
    "https://example.com",
    "https://crawlee.dev"
  ],
  "devices": [
    "desktop"
  ],
  "fullPage": true,
  "format": "png",
  "quality": 80,
  "savePdf": false,
  "pdfFormat": "A4",
  "pdfLandscape": false,
  "pdfMedia": "screen",
  "waitUntil": "load",
  "delayMs": 1000,
  "scrollToBottom": true,
  "hideCookieBanners": true,
  "hideChatWidgets": true,
  "blockAds": false,
  "compareWithPrevious": false,
  "changeThreshold": 1,
  "onlyChanged": false,
  "stateKey": "default",
  "customWidth": 1280,
  "customHeight": 800,
  "customDeviceScaleFactor": 1,
  "customIsMobile": false,
  "maxFullPageHeight": 15000,
  "navigationTimeoutSecs": 45,
  "waitForSelectorTimeoutSecs": 15,
  "concurrency": 2,
  "maxRetries": 1,
  "maxUrls": 1000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (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": [
        "https://example.com",
        "https://crawlee.dev"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lindenbyte/bulk-website-screenshots").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": [
        "https://example.com",
        "https://crawlee.dev",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("lindenbyte/bulk-website-screenshots").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": [
    "https://example.com",
    "https://crawlee.dev"
  ]
}' |
apify call lindenbyte/bulk-website-screenshots --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lindenbyte/bulk-website-screenshots"
        }
    }
}
```

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/nUZX1FYyh52zsklhI/builds/KINftPRhHFGTK28Qa/openapi.json
