# Website Screenshot API: URL to PNG and PDF (`zovo-one/website-screenshot-pdf-api`) Actor

Capture full-page screenshots (PNG/JPEG) and PDFs of any public web page, built for AI agents and automations. Set viewport and device scale, capture one element, hide cookie banners, block ads and set PDF margins. Files land in the key-value store with direct URLs.

- **URL**: https://apify.com/zovo-one/website-screenshot-pdf-api.md
- **Developed by:** [Michael Lip](https://apify.com/zovo-one) (community)
- **Categories:** Developer tools, AI
- **Security:** 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

## Website Screenshot & PDF API for AI agents

Send a list of URLs and get back a PNG, JPEG or PDF of each page, with a direct file link, for $0.001 per saved file.

You don't write Playwright code, run a browser or set up storage. Each URL gives one dataset row with the HTTP status, image size, byte size and a `fileUrl` you can open or pass to another tool. Cookie banners are hidden by CSS selector and common ad and tracker hosts are blocked. Failed URLs are free.

### What you get

One row per input URL. Duplicate URLs are dropped, and URLs left out at the charge cap get no row (they are counted in `SUMMARY`). The file itself sits in the run's key-value store.

| Field | Type | Meaning |
|---|---|---|
| `url` | string | The URL as you sent it (trimmed) |
| `status` | string | `success` when a file was saved, `failed` when the URL was refused or no file could be made |
| `httpStatus` | integer or null | HTTP status of the main document after redirects, such as 200, 404 or 403. `null` if no response came back |
| `file` | string or null | Key-value store key, for example `screenshot-0001-apify-com.png` |
| `fileUrl` | string or null | Signed link to the file. A plain GET returns the bytes, no token needed |
| `width` | number or null | Image width in pixels (PNG, JPEG) or paper width in PDF points |
| `height` | number or null | Image height in pixels (PNG, JPEG) or paper height in PDF points |
| `bytes` | integer or null | File size in bytes |
| `clipped` | boolean | `true` when the page was taller than 16384 or wider than 7680 device pixels and the image was cut |
| `error` | string or null | Why the row failed (SSRF guard, DNS, timeout, browser error), `null` on success |

The run also writes a `SUMMARY` record to the key-value store with counts of succeeded, failed and skipped URLs, total bytes and the events charged.

### Example output

These two rows come from a real run of the prefilled input on 2026-11-05 (run `lPNsxG6uGnLcSJa55`, finished in about 7 seconds).

```json
[
  {
    "url": "https://news.ycombinator.com",
    "status": "success",
    "httpStatus": 200,
    "file": "screenshot-0002-news-ycombinator-com.png",
    "fileUrl": "https://api.apify.com/v2/key-value-stores/REDGccWd0hMLbVgSy/records/screenshot-0002-news-ycombinator-com.png?signature=1jrmKQ809iHohQCvx5rFN",
    "width": 1280,
    "height": 1185,
    "bytes": 214618,
    "clipped": false,
    "error": null
  },
  {
    "url": "https://apify.com",
    "status": "success",
    "httpStatus": 200,
    "file": "screenshot-0001-apify-com.png",
    "fileUrl": "https://api.apify.com/v2/key-value-stores/REDGccWd0hMLbVgSy/records/screenshot-0001-apify-com.png?signature=hCz8LgEGzXNBpAiQWIYn",
    "width": 1280,
    "height": 10052,
    "bytes": 820825,
    "clipped": false,
    "error": null
  }
]
```

A refused URL looks like this one, from an earlier test run that tried the cloud metadata address.

```json
{
  "url": "http://169.254.169.254/latest/meta-data",
  "status": "failed",
  "httpStatus": null,
  "file": null,
  "fileUrl": null,
  "width": null,
  "height": null,
  "bytes": null,
  "clipped": false,
  "error": "IP address 169.254.169.254 is private or reserved"
}
```

### Pricing

You pay per saved file. Nothing else is listed in this Actor's pricing.

| Event | Price | Charged when |
|---|---|---|
| `screenshot` | $0.001 | a PNG or JPEG file is saved to the key-value store |
| `pdf` | $0.001 | a PDF file is saved to the key-value store |

Some worked examples.

| Job | Files | Cost |
|---|---|---|
| The prefilled sample run | 2 PNG | 2 x $0.001 |
| One full run of 50 URLs | 50 PNG | $0.05 |
| 1,000 product pages a month | 1,000 PNG | $1.00 |
| 500 pages as both PNG and PDF (20 runs of 50 URLs: 10 PNG, 10 PDF) | 1,000 files | $1.00 |

Refused URLs, timeouts and browser errors aren't charged. A saved error page is charged, though. A 404 or a bot-challenge page is captured like any page, so check `httpStatus` if you only want 2xx pages.

To cap spend, set the maximum cost per run in Console or pass `maxTotalChargeUsd` through the API. When the cap is reached, the Actor stops rendering, leaves the rest out of the dataset and reports them as `skippedForChargeLimit` in `SUMMARY`.

### Quick start in Console

1. Open the Actor and click Start. The input is prefilled with two sample pages.
2. After about 10 seconds, open the Output tab and click a `fileUrl` to see the image.
3. Replace the sample URLs with your own and pick PNG, JPEG or PDF.

### Use from the API

`run-sync-get-dataset-items` starts a run, waits for it and returns the rows in one request.

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

A sync call waits at most 300 seconds for the run. After that the HTTP request ends, but the run keeps going. With the defaults (30 s per page, 2 pages in parallel), about 20 URLs fit in 300 seconds in the worst case. For bigger batches, start the run with `POST /v2/acts/zovo-one~website-screenshot-pdf-api/runs`, wait for it to finish, then read `/v2/datasets/<defaultDatasetId>/items`.

### Use from Python

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("zovo-one/website-screenshot-pdf-api").call(run_input={
    "urls": ["https://example.com", "https://apify.com/pricing"],
    "output": "png",
    "viewportWidth": 390,
    "deviceScaleFactor": 2,
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(row["status"], row["httpStatus"], row["fileUrl"])
```

### Use from Node.js

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('zovo-one/website-screenshot-pdf-api').call({
    urls: ['https://example.com'],
    output: 'jpeg',
    quality: 80,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((row) => row.fileUrl));
```

### Use from AI agents (MCP)

Agents that speak the Model Context Protocol can call this Actor as a tool through the Apify MCP server. Each client adds servers in its own way. VS Code and Claude Desktop, for example, don't take the JSON below as written. For your client, open the [MCP configurator](https://mcp.apify.com), pick this Actor and copy the setup it gives you. A client that reads an `mcpServers` block with a `url` key, such as Cursor, can use this config.

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=zovo-one/website-screenshot-pdf-api"
    }
  }
}
```

The first connection opens a browser so you can sign in to Apify. If your client can't do that, add an `Authorization: Bearer <APIFY_TOKEN>` header instead. Without the `tools` parameter, an agent can still find the Actor with `search-actors`, run it with `call-actor` and read the rows with `get-dataset-items`.

A prompt that works well with it:

> Take a full-page screenshot of https://example.com/pricing at phone width (390 px) and give me the file link and the HTTP status.

The agent gets back `fileUrl`, so it can open the image or hand the link to a vision model. You can also pass `maxTotalChargeUsd` in the call options to cap what one agent call may spend.

### Input reference

Only `urls` is required. Everything else has a default.

| Field | Type | Default | Notes |
|---|---|---|---|
| `urls` | array of strings | `["https://apify.com", "https://news.ycombinator.com"]` | 1 to 50 public http(s) URLs. If you leave `urls` out of an API call, these two sample URLs are rendered and charged, so always send your own list |
| `output` | `png`, `jpeg` or `pdf` | `png` | One format per run |
| `quality` | integer 1–100 | `90` | JPEG only |
| `fullPage` | boolean | `true` | Whole page or viewport only. Ignored for PDF and when `selector` is set |
| `viewportWidth` | integer 320 to 3840 | `1280` | 390 is a phone, 1920 a desktop |
| `viewportHeight` | integer 240 to 2160 | `800` | Matters only when `fullPage` is off |
| `deviceScaleFactor` | integer 1 to 3 | `1` | 2 gives retina images at double the pixel size |
| `waitUntil` | `load`, `domcontentloaded`, `networkidle`, `commit` | `networkidle` | Use `domcontentloaded` for pages that never go idle |
| `delay` | integer 0 to 30000 ms | `0` | Extra wait before capture, counted in the page timeout |
| `selector` | CSS selector | none | Capture only the first matching element (PNG and JPEG) |
| `hideSelectors` | array of CSS selectors | `["#onetrust-banner-sdk", ".cookie-banner"]` | Hidden before capture, up to 20. Send `[]` to hide nothing |
| `blockAds` | boolean | `true` | Blocks common ad and tracker hosts, never the page you asked for |
| `pdfFormat` | `A4`, `Letter`, `Legal`, `A3`, `Tabloid` | `A4` | PDF only |
| `pdfLandscape` | boolean | `false` | PDF only |
| `pdfPrintBackground` | boolean | `true` | PDF only |
| `pdfMarginTop`, `pdfMarginRight`, `pdfMarginBottom`, `pdfMarginLeft` | string | `1cm` | `0` or a length in mm, cm, in, px or pt |
| `maxUrls` | integer 1 to 50 | `50` | Refuses longer lists before the browser starts |
| `pageTimeoutSecs` | integer 5 to 120 | `30` | Per URL, covering load, delay and capture |
| `maxConcurrency` | integer 1 to 4 | `2` | Lowered automatically when memory is tight |

Bad input fails fast with a message that names the field.

### Limits

- 50 URLs per run at most, in one output format. Split longer lists into several runs.
- Each page gets 30 seconds by default (5 to 120). A page that runs out of time becomes a free failed row.
- Width times device scale factor may be at most 7680. Full-page and element images are cut at 7680 device pixels wide and 16384 high, and the row says `clipped: true`.
- `selector` and `fullPage: false` are ignored for PDF. Playwright always prints the whole page.
- It doesn't log in, solve captchas or get past bot protection. You get an image of whatever the site served.

Only public pages are rendered. The Actor refuses `localhost`, `*.local`, `*.internal`, private and reserved IPv4 and IPv6 ranges and cloud metadata addresses. It checks every URL before the browser starts, after DNS lookup, and again for every request and redirect the page makes. Service workers are blocked.

The guard has known gaps. A DNS-rebinding host with a 0 s TTL that answers two lookups differently isn't caught. A redirect to a private address is detected only after the request was sent, and WebRTC traffic is cut by browser settings that weren't tested in a real browser. Don't use this Actor as your only network boundary.

Default memory is 2048 MB (1024 to 4096). Near the run's own timeout, the Actor stops starting new pages and writes the rest as free failed rows. Plan about `URLs x pageTimeoutSecs / maxConcurrency` seconds for the worst case.

### FAQ

#### Why did a row say success when the page was an error?

The Actor captures whatever the server returns, including 404 pages and bot challenges, and the file is charged. Check `httpStatus` in the row.

#### Do I need a token to download the file?

No. `fileUrl` carries a signature, so a plain GET returns the file. Files live in the run's default key-value store and follow your Apify data retention.

#### How do I get a mobile screenshot?

Set `viewportWidth` to 390 and `deviceScaleFactor` to 2 or 3. The image is then 780 or 1170 pixels wide.

#### Can I capture only one part of the page?

Yes. Put a CSS selector such as `#pricing` in `selector`. The first matching element is captured, for PNG and JPEG.

#### A cookie banner still covers the page. What now?

Find the banner's id or class in your browser's inspector and add it to `hideSelectors`. The two defaults cover OneTrust and sites that use a `.cookie-banner` class.

#### Can I get a PNG and a PDF of the same page?

Run the Actor twice, once with `output` set to `png` and once with `pdf`. Each saved file is one $0.001 event.

# Actor input Schema

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

Public http(s) pages to capture, one per line, for example https://example.com/pricing. Each URL gives one dataset row and, on success, one file. Up to 50 per run. Localhost, private IP addresses and internal hostnames are refused. Duplicates (same page with a trailing slash or a different host case) are rendered once. The two sample URLs render in about 10 seconds.

## `output` (type: `string`):

File type for every URL in this run. PNG is lossless, JPEG is smaller, PDF prints the page on paper-sized pages. Each saved file is one $0.001 event. To get both an image and a PDF, run twice.

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

On: capture the whole scrollable page. Off: capture only the visible viewport. Ignored for PDF (always the whole page) and when a CSS selector is set. Images are cut at 16384 device pixels of height and 7680 of width, and the row then says clipped: true.

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

Browser width in CSS pixels. 390 is a typical phone, 1280 a laptop, 1920 a desktop. Width times device scale factor may be at most 7680.

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

Browser height in CSS pixels. Only matters when Full page is off; full-page captures grow to the content height.

## `deviceScaleFactor` (type: `integer`):

Pixel density. 1 is standard, 2 gives retina-sharp images at double the pixel size, 3 is the maximum. Width times this value may be at most 7680.

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

JPEG compression quality from 1 to 100. Higher is sharper and larger. Used only when Output format is JPEG image.

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

When the page counts as loaded. Network idle (no requests for 500 ms) works best for JavaScript-heavy pages but can time out on pages that poll or keep a websocket open. Use DOM content loaded for those.

## `delay` (type: `integer`):

Extra wait in milliseconds after the page loads and banners are hidden, so animations, lazy images and fonts can settle. 0 to 30000. It counts toward the page timeout. If it leaves less than 5 s and you kept the default timeout, the timeout is raised for you; an explicit timeout that is too short is refused.

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

Optional CSS selector, for example #pricing or main article. Only the first matching element is captured and Full page is ignored. PNG and JPEG only (ignored for PDF). The element must become visible within the page timeout, or the row fails.

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

CSS selectors of elements to hide before capture, such as cookie banners and chat widgets. Example: #onetrust-banner-sdk, .cookie-banner, \[aria-label="Cookie banner"]. Up to 20. Braces, semicolons and angle brackets are refused. Send an empty list to hide nothing.

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

Abort requests to common ad, analytics and tracker hosts (DoubleClick, Google Ads, Facebook, Taboola, Criteo, Hotjar and others) for cleaner, faster captures. The page you asked for and requests to its own host are never blocked.

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

Paper size for PDF output. Ignored for PNG and JPEG.

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

Print the PDF in landscape orientation. Ignored for PNG and JPEG.

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

Keep background colours and images in the PDF. Leave it on for dark or colourful sites; without it they print as dark text on white.

## `pdfMarginTop` (type: `string`):

Top margin of each PDF page. 0 or a length in mm, cm, in, px or pt, for example 1cm, 12mm, 0.5in or 12pt. Ignored for PNG and JPEG.

## `pdfMarginRight` (type: `string`):

Right margin of each PDF page. 0 or a length in mm, cm, in, px or pt, for example 1cm, 12mm, 0.5in or 12pt. Ignored for PNG and JPEG.

## `pdfMarginBottom` (type: `string`):

Bottom margin of each PDF page. 0 or a length in mm, cm, in, px or pt, for example 1cm, 12mm, 0.5in or 12pt. Ignored for PNG and JPEG.

## `pdfMarginLeft` (type: `string`):

Left margin of each PDF page. 0 or a length in mm, cm, in, px or pt, for example 1cm, 12mm, 0.5in or 12pt. Ignored for PNG and JPEG.

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

Safety cap on how many URLs one run accepts, 1 to 50. If the list is longer, the run stops before the browser starts and nothing is charged. 50 is also the hard maximum.

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

Time limit for one URL, 5 to 120 seconds, covering navigation, delay and capture. A page that times out becomes a failed row (not charged) and the run moves on.

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

Pages rendered in parallel, 1 to 4. Lowered automatically when the run has little memory (about 512 MB per page, 1 GB per page above 2560 device pixels of width).

## Actor input object example

```json
{
  "urls": [
    "https://apify.com",
    "https://news.ycombinator.com"
  ],
  "output": "png",
  "fullPage": true,
  "viewportWidth": 1280,
  "viewportHeight": 800,
  "deviceScaleFactor": 1,
  "quality": 90,
  "waitUntil": "networkidle",
  "delay": 0,
  "hideSelectors": [
    "#onetrust-banner-sdk",
    ".cookie-banner"
  ],
  "blockAds": true,
  "pdfFormat": "A4",
  "pdfLandscape": false,
  "pdfPrintBackground": true,
  "pdfMarginTop": "1cm",
  "pdfMarginRight": "1cm",
  "pdfMarginBottom": "1cm",
  "pdfMarginLeft": "1cm",
  "maxUrls": 50,
  "pageTimeoutSecs": 30,
  "maxConcurrency": 2
}
```

# Actor output Schema

## `renders` (type: `string`):

No description

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

No description

## `allFields` (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://apify.com",
        "https://news.ycombinator.com"
    ],
    "hideSelectors": [
        "#onetrust-banner-sdk",
        ".cookie-banner"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zovo-one/website-screenshot-pdf-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",
        "https://news.ycombinator.com",
    ],
    "hideSelectors": [
        "#onetrust-banner-sdk",
        ".cookie-banner",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("zovo-one/website-screenshot-pdf-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",
    "https://news.ycombinator.com"
  ],
  "hideSelectors": [
    "#onetrust-banner-sdk",
    ".cookie-banner"
  ]
}' |
apify call zovo-one/website-screenshot-pdf-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zovo-one/website-screenshot-pdf-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/RG9I2lpLunhFo487Q/builds/4DwaKLwNII7tRujce/openapi.json
