# Website Screenshot API: full page, mobile, PDF (`pxdata/website-screenshot-api`) Actor

Screenshots of any public URL as PNG, JPEG, WebP or PDF. Full page with lazy-load scrolling, device presets, cookie banner removal, dark mode, CSS selector crop. Charged only for successful captures.

- **URL**: https://apify.com/pxdata/website-screenshot-api.md
- **Developed by:** [Marcio Motta](https://apify.com/pxdata) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 screenshot captureds

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 API: full page, mobile, PDF

Give it a list of URLs and it returns one image (PNG, JPEG or WebP) or PDF per URL, stored in the run's key-value store, plus a dataset row with the public link, HTTP status, final URL after redirects, page title, pixel size, file size and SHA-256. A URL that fails produces a row with `ok: false` and an error message. It never stops the rest of the batch and you are not charged for it.

It is a general-purpose screenshot tool: monitoring pages, link previews and Open Graph images, archiving evidence, QA across devices, thumbnails for a directory, visual input for an AI agent. There is no target site; the URL is yours.

### What you get that the basic screenshot actors skip

- **Full page that actually works.** Scrolls the page in steps first so lazy-loaded images and sections render, then captures. Pages taller than 16,383 device pixels are cut at that limit and flagged with `truncated: true`.
- **Cookie banners removed.** Clicks "reject" on the common consent managers (selector lists for OneTrust, Cookiebot, Didomi, Quantcast, Usercentrics, Klaro and others, plus a text match on buttons inside overlays in English, Portuguese, Spanish, French, German and Italian). If a banner only offers "accept", it clicks that. Whatever is left gets hidden and page scrolling is restored. Checked end to end on The Guardian, BBC, Le Monde, Der Spiegel and UOL.
- **Sticky headers do not repeat.** In full-page mode, `position: fixed` and `sticky` elements are pinned to their place instead of showing up in the middle of the image.
- **Crop to an element or a rectangle** with a CSS selector or `{x, y, width, height}`.
- **Emoji and CJK text render.** The image ships with Noto Color Emoji and Noto CJK, so emoji do not turn into empty squares.
- **Per-URL errors.** DNS failure, TLS error, timeout, HTTP 404, missing selector: each one becomes a row with a readable `error`.
- **Charged only on success.** See Pricing.

### Quick start

Input in the Console, or via API:

```json
{
  "urls": ["https://www.bbc.com/news"],
  "device": "desktop-1366",
  "fullPage": true,
  "format": "jpeg",
  "quality": 80
}
```

Output row from a real run. The local run stores files on disk, so `screenshotUrl` is shown here in the form it takes on the platform:

```json
{
  "url": "https://www.bbc.com/news",
  "finalUrl": "https://www.bbc.com/news",
  "ok": true,
  "httpStatus": 200,
  "error": null,
  "title": "BBC News - Breaking news, video and the latest top stories from the U.S. and around the world",
  "device": "desktop-1366",
  "format": "jpeg",
  "fullPage": true,
  "truncated": false,
  "width": 1366,
  "height": 6601,
  "fileSizeBytes": 1058915,
  "sha256": "caac18061616f8bdedda6dc9d7b553bfc66cdea29cbc76c5f0e31e985153377d",
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/001-www-bbc-com-news.jpg",
  "kvKey": "001-www-bbc-com-news.jpg",
  "loadTimeMs": 1427,
  "totalTimeMs": 5323,
  "capturedAt": "2026-10-17T09:48:55.164Z"
}
```

`loadTimeMs` is navigation time. `totalTimeMs` includes the settle wait, cookie handling, scrolling and encoding. For PDFs `width` and `height` are `null`.

A failed URL looks like this (this one is a real 404; the image of the error page is kept for debugging but is not charged):

```json
{
  "url": "https://github.com/this-page-does-not-exist-davi-test",
  "ok": false,
  "httpStatus": 404,
  "error": "HTTP 404",
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/<storeId>/records/008-github-com-this-page-does-not-exist-davi-test.png"
}
```

### Input reference

| Field | Type | Default | Notes |
|---|---|---|---|
| `urls` | array | `["https://apify.com"]` | Required. Up to 1,000. A URL without a scheme gets `https://`. Only `http` and `https`; localhost and private ranges are rejected. |
| `device` | enum | `desktop-1366` | `desktop-1366`, `desktop-1920`, `laptop-1440`, `iphone-15` (393x852 at 3x), `pixel-8` (412x915 at 2.6x), `ipad` (820x1180 at 2x). Mobile presets send a mobile user agent and touch. |
| `viewportWidth`, `viewportHeight` | integer | from device | Override the preset. Width 200 to 3840. |
| `fullPage` | boolean | `true` | `false` captures only the first viewport. |
| `format` | enum | `png` | `png`, `jpeg`, `webp`, `pdf`. PDF uses the print layout (A4), not a screenshot of the screen. |
| `quality` | integer | `80` | JPEG and WebP only. |
| `selector` | string | none | Capture only the first match. No match: the URL fails, not charged. |
| `clip` | object | none | `{"x":0,"y":0,"width":1200,"height":630}` in CSS pixels. Ignored if `selector` is set. |
| `waitUntil` | enum | `load` | `load` (plus 1.5 s settle), `domcontentloaded`, `networkidle`. |
| `waitForSelector` | string | none | Must become visible before capture, within the page timeout. |
| `delayMs` | integer | `0` | Extra wait before capture, max 30,000. |
| `scrollToLoad` | boolean | `true` | Lazy-load scroll before a full-page capture. |
| `removeCookieBanners` | boolean | `true` | See above. |
| `hideSelectors` | array | none | CSS selectors set to `display:none`. |
| `blockAds` | boolean | `false` | Aborts requests to common ad and tracker domains. |
| `fixStickyHeaders` | boolean | `true` | Full page only. |
| `darkMode` | boolean | `false` | Sets `prefers-color-scheme: dark`. Only sites that support it change. |
| `pageTimeoutSecs` | integer | `45` | Per URL, 10 to 180. |
| `maxConcurrency` | integer | `3` | Pages in parallel. |
| `returnBase64` | boolean | `false` | Adds `imageBase64` to the row when the file is 2 MB or less. For agents that cannot fetch a URL. |
| `proxyConfiguration` | object | off | Apify Proxy. Choosing the `RESIDENTIAL` group adds the `residential-page` charge. |

### Calling it from code

JavaScript:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('pxdata/website-screenshot-api').call({
    urls: ['https://example.com', 'https://apify.com'],
    format: 'webp',
    quality: 75,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) console.log(item.ok, item.url, item.screenshotUrl);
```

Python:

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("pxdata/website-screenshot-api").call(
    run_input={"urls": ["https://example.com"], "device": "iphone-15", "fullPage": True}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["ok"], item["url"], item["screenshotUrl"])
```

cURL, synchronous, returns the dataset rows:

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

Files in the key-value store are stored under `kvKey` and also reachable from `screenshotUrl`. Unnamed storages are removed after the retention period of your plan, so copy anything you need to keep.

### Pricing

Pay per event. You pay for results, not for compute.

| Event | Charged when | FREE | BRONZE | SILVER | GOLD |
|---|---|---|---|---|---|
| `capture` | One URL returned `ok: true` | $0.004 | $0.003 | $0.0025 | $0.002 |
| `residential-page` | Added to each successful capture when the RESIDENTIAL proxy group is selected | $0.02 | $0.02 | $0.02 | $0.02 |

Apify also charges its standard `apify-actor-start` event once per run. At the time of writing the platform docs list it at $0.00005 per start.

Not charged: HTTP 4xx and 5xx responses, timeouts, DNS and TLS errors, selector not found, invalid URLs, URLs skipped because the run hit its maximum cost.

1,000 screenshots on the BRONZE tier cost $3.00. Set "Maximum cost per run" in the Console if you want a hard cap; the Actor stops capturing when it is reached.

### Memory and performance

Measured on a batch of 10 URLs (BBC News, The Guardian, Le Monde, Mercado Livre, Wikipedia's Brazil article, apify.com, github.com via an http redirect, globo.com, example.com and one 404), full page PNG, desktop preset, 3 pages in parallel:

- 9 of 10 captured. The tenth was the 404 and was reported as such.
- Mean 11.7 s per URL from navigation start to stored file, 57 s for the whole batch (about 5.7 s per URL with 3 in parallel).
- Fastest: example.com, 2.3 s. Slowest: Wikipedia and globo.com, 26 s and 27 s, because of their length and lazy loading.

That run was on a home connection, not on Apify's datacenter, so treat the timing as an order of magnitude.

Memory: use 2 GB (the default). A cloud test with 1 GB, `maxConcurrency` 2 and the same 10 heavy URLs as full page PNG did not finish: after 15 minutes only 2 of 10 pages were captured and the run hit the timeout, because Chromium was starved and concurrency stayed at 1. 1 GB is only reasonable for light pages, one at a time. Use 2 GB for up to 4 parallel pages and 4 GB for up to 8. Long pages in PNG at mobile pixel ratios are the memory-heavy case; switch to JPEG or WebP, or lower `maxConcurrency`, if a run is killed for memory.

### Limits

- Height: 16,383 device pixels. With `iphone-15` (3x) that is about 5,460 CSS pixels of page.
- 1,000 URLs per run, 180 s per URL at most, 30 s extra delay at most.
- No cookies, no logins, no custom headers. This keeps the Actor simple to use from agents and it is not built for pages behind authentication.
- Sites that block datacenter traffic return 401, 403 or a challenge page. Those rows come back with `ok: false` and are not charged. The Actor does not try to defeat bot protection. For sites that allow it, the `RESIDENTIAL` proxy group may help.
- Consent banners that live inside cross-origin iframes may survive. Open a ticket with the URL.
- Animated content is captured at one instant. Videos show their poster or first frame.

### FAQ

**Why is my image shorter than the page?** The 16,383 px cap. Check `truncated` in the row. For very long pages capture in pieces with `clip`.

**Why a 401 or 403 on a site that works in my browser?** The site is blocking automated traffic or datacenter IPs. The row has `ok: false`, you are not charged, and the stored image shows what the server returned.

**Does it log in, read paywalled content or bypass anything?** No. It loads the public page as an anonymous visitor.

**Can I get WebP?** Yes. `format: "webp"`. Chromium does not produce WebP in headless mode here, so the Actor captures PNG and encodes it with sharp.

**The cookie banner is still there.** Send the URL and the banner's selector. Meanwhile add the selector to `hideSelectors`.

**Is it legal?** Taking a screenshot of a public page is technical, not a legal green light. Copyright, terms of service and privacy law (GDPR, LGPD) apply to what you do with the images, especially if they show personal data. You are responsible for your use.

**Which URLs are rejected?** `localhost`, `.local`, `.internal` and private IP ranges, and any scheme other than http and https.

**What happens when I hit the maximum cost per run?** Captures already done stay in the dataset. Remaining URLs are written as `ok: false` with an explanatory error and no charge.

### Changelog

**1.0.0** First release. PNG, JPEG, WebP and PDF; six device presets; full page with lazy-load scroll; element and rectangle crop; cookie banner removal; sticky element handling; dark mode; ad blocking; optional Apify Proxy; per-URL error rows; pay per successful capture.

### Support

Open an issue on the Actor page with the URL, your input and the run link. Reproducible cases are looked at first.

# Actor input Schema

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

Pages to capture, one per line. Up to 1,000 per run. A missing scheme is treated as https://.

## `device` (type: `string`):

Viewport, pixel ratio and user agent. Overridden by the viewport width/height fields below.

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

Overrides the preset width. 200 to 3840.

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

Overrides the preset height. 200 to 4000. Only matters when Full page is off.

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

Capture the whole scrollable page instead of only the first viewport. Capped at 16,383 device pixels of height (Chromium texture limit); the output item has truncated: true when the page was longer.

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

Output file type. PNG is lossless; JPEG and WebP are smaller and use the quality setting; PDF uses the print layout of the page (A4).

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

1 to 100. Ignored for PNG and PDF.

## `selector` (type: `string`):

If set, only the first matching element is captured. If the selector matches nothing, the URL fails with an error item and is not charged.

## `clip` (type: `object`):

Crop in CSS pixels from the top-left of the page, for example {"x": 0, "y": 0, "width": 1200, "height": 630}. Ignored when a selector is set.

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

Navigation event to wait for. 'load' also waits 1.5 s for late layout shifts; 'networkidle' waits until the network is quiet for 500 ms.

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

CSS selector that must be visible before capturing. If it never appears within the page timeout, the URL fails (not charged).

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

Additional wait right before the capture, 0 to 30,000.

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

Scrolls through the page in steps before a full-page capture so lazy images and infinite-reveal sections render. Adds 1 to 5 s on long pages.

## `removeCookieBanners` (type: `boolean`):

Clicks the reject button of common consent managers (OneTrust, Cookiebot, Didomi, Quantcast, Usercentrics, Klaro and others), then hides what is left and restores page scrolling.

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

Elements to hide with display:none before the capture, for example chat widgets or newsletter pop-ups.

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

Aborts requests to common ad and tracking domains. Makes pages faster and cleaner, but can occasionally break a layout.

## `fixStickyHeaders` (type: `boolean`):

Before a full-page capture, turns position:fixed into absolute and position:sticky into relative, so a sticky header is not repeated in the middle of the image.

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

Sets prefers-color-scheme: dark. Only sites that implement it will change.

## `pageTimeoutSecs` (type: `integer`):

Maximum time for navigation and for the whole capture of one URL.

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

Pages captured at the same time inside one browser. With 1 GB of memory use 1 or 2; with 2 GB use up to 4; with 4 GB up to 8.

## `returnBase64` (type: `boolean`):

Adds imageBase64 to the item when the file is 2 MB or smaller. Useful for AI agents that cannot fetch a URL. The file is always stored in the key-value store as well.

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

Off by default. Datacenter proxy is free of extra event charges. Selecting the RESIDENTIAL group adds the 'residential-page' event per successful capture (see Pricing in the README).

## Actor input object example

```json
{
  "urls": [
    "https://apify.com"
  ],
  "device": "desktop-1366",
  "fullPage": true,
  "format": "png",
  "quality": 80,
  "waitUntil": "load",
  "delayMs": 0,
  "scrollToLoad": true,
  "removeCookieBanners": true,
  "blockAds": false,
  "fixStickyHeaders": true,
  "darkMode": false,
  "pageTimeoutSecs": 45,
  "maxConcurrency": 3,
  "returnBase64": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Dataset items with url, ok, httpStatus, error, title, width, height, fileSizeBytes and screenshotUrl.

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

Key-value store holding the PNG, JPEG, WebP and PDF files.

# 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://apify.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pxdata/website-screenshot-api").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://apify.com"] }

# Run the Actor and wait for it to finish
run = client.actor("pxdata/website-screenshot-api").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://apify.com"
  ]
}' |
apify call pxdata/website-screenshot-api --silent --output-dataset

```

## MCP server setup

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

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/7sIbVbN6SDQ55hkqh/builds/amLDMceb3ahCnHQId/openapi.json
