# Website Screenshot & PDF Capture + Change Alerts (`maliksaid/website-screenshot-pdf`) Actor

Take website screenshots of any URL list: full page, viewport or one page element, in PNG, JPEG or WebP at desktop, tablet and phone sizes. Optional URL to PDF. Each row has a file link. Visual change detection compares with the last run and returns a diff image.

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

## Pricing

from $3.20 / 1,000 captures

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Capture + Change Alerts

Take screenshots of a list of web pages. Each URL is captured at the screen sizes you choose: desktop, laptop, tablet, phone or a custom size. You get full-page or above-the-fold images as PNG, JPEG or WebP, and an optional PDF of each page (URL to PDF). It can also turn raw HTML into an image or a PDF. Every file gets a download link in the dataset.

Turn on visual change detection and schedule the Actor. Each run compares every screenshot with the one from the last run. The row tells you whether the page changed and by how much. Changed pages also get a diff image with the changed pixels in red.

### What you can use it for

- Website monitoring: see when a competitor, a partner or your own site changes its look.
- Visual regression checks after a deploy, at several screen sizes in one run.
- Archiving pages as PDF or images for records, reports or compliance files.
- Thumbnails and previews for directories, link lists or SEO reports.

### How to use it

1. Paste your URLs, one per line.
2. Pick the viewports. The default is desktop (1440x900) and phone (390x844).
3. Choose full page or viewport only, and the image format.
4. Optional: turn on "Also save a PDF".
5. Optional: turn on "Visual change detection" and schedule the Actor daily or weekly.

Each URL and viewport pair is one capture. Ten URLs at two viewports make 20 captures. A PDF counts as one more capture per URL.

### Input example

```json
{
    "urls": ["https://nodejs.org", "https://www.python.org"],
    "viewports": ["desktop", "phone"],
    "fullPage": true,
    "format": "webp",
    "savePdf": true,
    "compareWithPrevious": true,
    "stateKey": "my-watch-list"
}
```

Other options:

- **Hide cookie banners** (on by default). The Actor hides common consent banners with CSS. It does not click or accept anything.
- **Hide elements.** CSS selectors of extra elements to hide, such as chat widgets.
- **Block ads and trackers** (off by default). The Actor blocks requests to common ad and tracker hosts of other sites before the page loads. Pages load faster, and the row says how many requests were blocked in `adsBlocked`.
- **Proxy with country choice** (off by default). Use Apify Proxy and pick a country to see a page as visitors there see it: local prices, language or region banners. Through a proxy, video and audio are not loaded, and a page stops loading new files after about 15 MB. Each capture through a proxy adds a "Proxy capture" charge (see Pricing). The proxy is for country choice only: pages with a bot check are still skipped.
- **Pixel density (scale).** 1, 2 or 3. Use 2 or 3 for sharp retina-style images, for example phone captures for app store pages or marketing. Files are larger. `imageWidth` and `imageHeight` are in real pixels. To keep runs stable, a full page at 2 or 3 stops at the pixel count of a default desktop capture: about 2,000 page pixels tall for desktop at 2, and about 3,280 for a phone at 3. Such rows say `truncated: true`.
- **Colour scheme.** Light or dark. Sites that support dark mode show their dark theme.
- **Capture one element.** A CSS selector such as `#pricing`. The Actor captures only the first matching element. If it is missing, the Actor captures the page and the row says `elementFound: false`. PDFs ignore it.
- **Wait for element.** A CSS selector to wait for, up to 15 seconds, before the capture. Use it for pages that render late. If the element never appears, the capture goes ahead and the row says `waitSelectorFound: false`.
- **Extra wait.** Seconds to wait before the capture, for animations.
- **Scroll to load lazy images.** For full-page captures.
- **Maximum page height.** Very long pages are cut at this height (default 8,000 pixels). The row then says `truncated: true`.
- **Change threshold.** The share of pixels that must differ before a page counts as changed. The default is 0.1%.
- **Maximum captures.** The default is 100. Set 0 for no limit.

### Output example

This row comes from a real run. It is the second run with change detection on the same state key.

```json
{
    "id": "8cbaec05ec",
    "url": "https://nodejs.org/",
    "status": "captured",
    "reason": null,
    "finalUrl": "https://nodejs.org/en",
    "pageTitle": "Node.js — Run JavaScript Everywhere",
    "httpStatus": 200,
    "type": "screenshot",
    "viewport": "desktop",
    "viewportWidth": 1440,
    "viewportHeight": 900,
    "fullPage": true,
    "format": "webp",
    "fileKey": "nodejs-org-desktop-8cbaec05ec.webp",
    "fileUrl": "https://api.apify.com/v2/key-value-stores/pIgZAp3VJh6Th6gCR/records/nodejs-org-desktop-8cbaec05ec.webp?signature=oJg1sTEQxCTugwpPyqw4",
    "fileSizeBytes": 76604,
    "imageWidth": 1440,
    "imageHeight": 985,
    "pageHeight": 985,
    "truncated": false,
    "previousCapturedAt": "2026-10-21T14:45:51.091Z",
    "changed": true,
    "diffPercent": 0.106,
    "diffImageUrl": "https://api.apify.com/v2/key-value-stores/pIgZAp3VJh6Th6gCR/records/nodejs-org-desktop-8cbaec05ec-diff.png?signature=J486WGbkLgMZui6H6yIQ",
    "scrapedAt": "2026-10-21T14:51:34.290Z"
}
```

A URL that the Actor did not capture gets a free row with the reason:

```json
{
    "id": "b65c46d974",
    "url": "https://www.reuters.com/world/",
    "status": "skipped",
    "reason": "robots.txt disallows this page.",
    "httpStatus": null,
    "viewport": "desktop",
    "type": "screenshot",
    "scrapedAt": "2026-10-21T14:47:23.691Z"
}
```

The files are in the run's key-value store. The `fileUrl` link opens the file without an API token. The dataset has two views: "Captures" and "Changed pages".

#### Can I capture one part of a page?

Yes. Set **Capture one element** to a CSS selector. This example watches one pricing box for changes:

```json
{
    "urls": ["https://example.com/pricing"],
    "captureSelector": "#pricing",
    "waitForSelector": "#pricing",
    "compareWithPrevious": true,
    "stateKey": "pricing-box"
}
```

#### HTML to PDF or PNG

Send raw HTML when the page has no public URL, such as an invoice, a report or a social card. Clear the URLs list to capture only the HTML. Each document is up to 2 MB, and 5 MB in total per run. Remote images and fonts in it load as usual, and each remote host's robots.txt applies. Local files never load.

```json
{
    "urls": [],
    "htmlDocuments": ["<html><body><h1>Invoice 1042</h1><p>Total: $49</p></body></html>"],
    "viewports": ["desktop"],
    "savePdf": true
}
```

The row's `url` is `html:0` for the first document, and `source` is `html`.

#### Local language, time zone and country

Many sites change language, date format or opening hours by visitor. Set all three to see the page as a local visitor sees it:

```json
{
    "urls": ["https://example.com"],
    "locale": "de-DE",
    "timezoneId": "Europe/Berlin",
    "proxyConfiguration": { "useApifyProxy": true, "apifyProxyCountry": "DE" }
}
```

### Fields

| Field | Meaning |
|---|---|
| `status` | `captured`, `skipped` (robots.txt or a bot check) or `failed` (load error or HTTP error) |
| `type`, `format` | `screenshot` or `pdf`; `png`, `jpeg`, `webp` or `pdf` |
| `viewport`, `viewportWidth`, `viewportHeight` | Screen size used |
| `fileUrl`, `fileKey`, `fileSizeBytes` | The saved file |
| `imageWidth`, `imageHeight`, `pageHeight`, `truncated` | Image and page size |
| `finalUrl`, `pageTitle`, `httpStatus` | Page details after redirects |
| `changed`, `diffPercent`, `diffImageUrl`, `previousCapturedAt` | Change detection results. Null on the first run |
| `captureSelector`, `elementFound` | The element captured, and whether it was found. Null when you capture the whole page |
| `waitSelectorFound` | Whether the wait-for element appeared. Null when you set no wait selector |
| `scale`, `colorScheme` | Pixel density (1, 2 or 3; null for PDFs) and light or dark mode |
| `adsBlocked` | Ad and tracker requests blocked on the page. Null when ad blocking is off |
| `proxyCountry` | Proxy country used, `any` for Apify Proxy without a country, `custom` for your own proxy URLs. Null without a proxy |
| `maskedCount` | Page elements covered by a grey box (Mask elements). 0 when none |
| `locale`, `timezoneId` | Browser language and time zone used. Null when not set |
| `source` | `url` for a web page, `html` for an HTML document from the input |

### How change detection works

The Actor keeps the last screenshot of each URL and viewport in a named key-value store. The store is called `website-screenshot-pdf-state`, and the screenshots are kept under your state key. On the next run it compares the new screenshot with the stored one, pixel by pixel. A pixel counts as changed when a colour channel moves by more than 24 of 255. This ignores tiny rendering noise. A page that got taller or shorter counts the extra area as changed. Then the new screenshot becomes the baseline for the next run.

Use one state key per watch list. Pages with rotating banners, live counters or ads will show small changes on every run. Turn on "Block ads and trackers", or cover moving parts with "Mask elements". A masked part becomes a grey box in every capture, so it never counts as a change, and the layout below it does not move. "Hide elements" removes an element instead, which can shift the page. A capture at another scale, in dark mode, or with another locale or time zone keeps its own baseline.

### Pricing

You pay per event:

- $0.005 when the run starts, for each GB of memory (one event at the default 1 GB).
- $0.004 per capture (one screenshot or one PDF), which is $4 per 1,000 captures. On the Gold, Platinum and Diamond plans it is $0.0032, which is $3.20 per 1,000. Change detection and diff images are included.
- Skipped and failed URLs are free.
- $0.05 per capture through a proxy (optional), on top of the capture price, which is $50 per 1,000. Proxy traffic has its own cost. Captures without a proxy never pay it.

Example: 50 URLs at desktop and phone size make 100 captures. That run costs $0.405 ($0.005 + 100 x $0.004), or $0.325 on Gold. The same run through a proxy costs $5.405.

### FAQ

**How do I stop false change alerts?** Add the moving parts to **Mask elements**, for example `["time", ".ticker", "#last-updated"]`. Turn on **Block ads and trackers** for pages with ads.

**Why was a page skipped?** Its robots.txt disallows it, or it showed a bot check. Skipped and failed pages are free and listed with a reason.

**How long can a full page be?** Up to **Maximum page height** (8,000 pixels by default, 16,000 at most). Longer pages are cut and marked `truncated: true`.

**Can it log in to a page?** No. It captures public pages only.

**Why does a page look different from my browser?** Cookie banners are hidden by default. Language, time zone and country can differ: set **Locale**, **Time zone** and a proxy country.

**How do I get change alerts by email or Slack?** Schedule the Actor with change detection on. Then add an Apify integration (email, Slack or a webhook) and use the "Changed pages" view.

**Does masking hide personal data?** Yes. A masked element is a solid box in the file.

### Use it from the API or an AI agent

One call runs the Actor and returns the rows, each with a `fileUrl` you can open without a token:

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

AI agents can call it through the Apify MCP server (mcp.apify.com) by its name, `maliksaid/website-screenshot-pdf`.

### Good to know

- The Actor reads each site's robots.txt first and skips pages it disallows. This cannot be turned off.
- It uses a normal headless Chromium browser. It does not solve CAPTCHAs, log in or hide that it is a browser. Sites that show a bot check or answer 403 are listed as skipped or failed, at no charge.
- Phone and tablet captures use the screen width, so pages show their responsive layout. The browser does not pretend to be a phone.
- Only capture pages you are allowed to copy. Some sites' terms limit how you may reuse their content.
- Default memory is 1 GB. For long lists, 2 GB runs more pages at once.

# Actor input Schema

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

Web pages to capture, one per line. A URL without https:// gets it added. Pages that robots.txt disallows are skipped and listed with a reason.

## `htmlDocuments` (type: `array`):

Optional. Raw HTML to capture as an image or PDF, one document per entry, up to 2 MB each and 5 MB in total per run. Use it for invoices, reports or social cards that have no public URL. To capture only HTML, clear the URLs list. Remote images and fonts load as usual, and each remote host's robots.txt applies. Local files never load.

## `viewports` (type: `array`):

Screen sizes to capture each URL at. Use desktop (1440x900), laptop (1280x800), tablet (768x1024), phone (390x844) or a size such as 1920x1080. Each URL and viewport pair is one capture.

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

On: capture the whole scrolling page. Off: capture only the visible viewport (above the fold).

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

File type of each screenshot. PNG is lossless. JPEG and WebP files are smaller.

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

Image quality from 1 to 100 for JPEG and WebP. PNG ignores it.

## `scale` (type: `integer`):

1 is a normal screen. 2 gives sharp images like a retina screen, and 3 like a high-end phone. Files are larger. To keep memory safe, a full page at 2 or 3 stops at the same pixel count as a default desktop capture: about 2,000 page pixels tall for desktop at 2, and about 3,280 for a phone at 3. The row then says truncated: true.

## `colorScheme` (type: `string`):

Show the page in light or dark mode. Sites that support dark mode switch to their dark theme.

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

Optional browser language and region, such as de-DE, fr-FR or ar-SA. It also sets the Accept-Language header. Combine it with a proxy country to see a page as local visitors see it.

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

Optional browser time zone, such as Europe/Berlin or Asia/Riyadh. Pages that show local times or opening hours use it.

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

Save one PDF of each URL as well, printed with backgrounds. Each PDF counts as one capture.

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

Paper size of the PDF.

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

Print the PDF in landscape orientation.

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

Hide common cookie consent banners (OneTrust, Cookiebot, Usercentrics, Didomi, CookieYes and others) with CSS before the capture. Nothing is clicked or accepted.

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

CSS selectors of elements to hide before the capture, for example chat widgets or ads. Example: #intercom-container

## `maskSelectors` (type: `array`):

CSS selectors of moving parts to cover with a grey box, such as a clock, a rotating banner or a price ticker. The layout stays the same, so masked parts never count as a change. Masking also hides personal data in a capture.

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

Block requests to common ad and tracker hosts of other sites before the page loads. Pages load faster, and change detection reports fewer false changes from rotating ads.

## `captureSelector` (type: `string`):

Capture only the first element that matches this CSS selector, for example #pricing. Empty: capture the page. PDFs ignore it.

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

Wait up to 15 seconds for this element to appear before the capture. If it never appears, the capture goes ahead and the row says so.

## `waitSecs` (type: `integer`):

Wait this long after the page loads and before the capture, for animations and late content.

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

For full-page captures, scroll down the page first so lazy-loaded images appear.

## `maxPageHeight` (type: `integer`):

Full-page captures stop at this height. Very long pages are cut, and the row says truncated: true.

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

Compare each screenshot with the one from the last run that used the same state key. Each row gets changed, diffPercent and, for changed pages, a diff image with changed pixels in red. Schedule the Actor with this on to watch pages for visual changes.

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

Name of the stored baseline screenshots. Use a different key for each separate watch list.

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

A page counts as changed when more than this share of pixels differs, in percent. Example: 0.1 means 1 pixel in 1,000.

## `maxCaptures` (type: `integer`):

Stop after this many files (screenshots plus PDFs). The default keeps test runs short. Set 0 for no limit.

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

Optional. Use Apify Proxy and pick a country to see the page as visitors in that country see it, for example local prices, language or region banners. Each capture made through a proxy adds the "Proxy capture" charge on top of the normal capture price. Off by default. The proxy is for country choice only: a page with a bot check is still skipped.

## Actor input object example

```json
{
  "urls": [
    "https://crawlee.dev",
    "https://books.toscrape.com"
  ],
  "htmlDocuments": [],
  "viewports": [
    "desktop",
    "phone"
  ],
  "fullPage": true,
  "format": "png",
  "quality": 80,
  "scale": 1,
  "colorScheme": "light",
  "savePdf": false,
  "pdfFormat": "A4",
  "pdfLandscape": false,
  "hideCookieBanners": true,
  "hideSelectors": [],
  "maskSelectors": [],
  "blockAds": false,
  "captureSelector": "",
  "waitForSelector": "",
  "waitSecs": 1,
  "scrollToLoad": true,
  "maxPageHeight": 8000,
  "compareWithPrevious": false,
  "stateKey": "default",
  "changeThreshold": 0.1,
  "maxCaptures": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `files` (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://crawlee.dev",
        "https://books.toscrape.com"
    ],
    "viewports": [
        "desktop",
        "phone"
    ],
    "maxCaptures": 100,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("maliksaid/website-screenshot-pdf").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://crawlee.dev",
        "https://books.toscrape.com",
    ],
    "viewports": [
        "desktop",
        "phone",
    ],
    "maxCaptures": 100,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("maliksaid/website-screenshot-pdf").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://crawlee.dev",
    "https://books.toscrape.com"
  ],
  "viewports": [
    "desktop",
    "phone"
  ],
  "maxCaptures": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call maliksaid/website-screenshot-pdf --silent --output-dataset

```

## MCP server setup

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

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/BtgmFHvHv4nIf6zBn/builds/et3F6gzyN7yaytnxV/openapi.json
