# Bulk Screenshot & PDF Generator (`jungle_synthesizer/website-screenshot-pdf-bulk`) Actor

Capture full-page screenshots or PDFs from a bulk list of URLs. PNG, JPG, WebP, or PDF output. Desktop/mobile/tablet viewports, dark mode, wait-for-selector. Each URL gets a dataset row plus a binary in the run's key-value store. Built for CI snapshot tests, SEO audits, brand monitoring, and QA.

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

## Pricing

from $4.00 / 1,000 record scrapeds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Bulk Screenshot & PDF Generator

Capture full-page screenshots or PDFs from a list of URLs in a single run. Feed it 3 URLs or 3,000 — each one comes back as a PNG, JPG, WebP, or PDF, paired with a metadata row that tells you exactly what happened.

***

### Bulk Screenshot & PDF Generator Features

- Accepts a **list** of URLs, not one at a time — the whole point of a bulk tool
- Captures **PNG, JPG, WebP, or PDF** in the same run, picked once for the whole batch
- Desktop, mobile, tablet, or a custom viewport size — your call
- Full-page or viewport-only capture
- Dark mode emulation, for screenshots that don't look like they're from 2015
- Optional CSS selector wait, so you can capture a page after its content actually loads
- Every URL gets exactly one dataset row — success or failure. Nothing gets silently dropped, even navigation timeouts get an error row
- Resumable — pick a large batch back up without re-capturing what already finished

***

### Who Uses a Bulk Screenshot Tool?

- **QA engineers** — snapshot every page in a release before and after a deploy, then diff them
- **SEO teams** — pull visual + PDF snapshots of a site's key pages on a schedule
- **Brand monitoring** — capture how partner or competitor pages render across viewports
- **E-commerce ops** — archive product pages as PDFs for pricing disputes or catalog audits
- **Legal and compliance** — capture dated, full-page evidence of a web page's state, in PDF form
- **Anyone writing a CI check** — visually confirm nothing broke, across a whole URL list, in one run

***

### How It Works

1. Give it a list of URLs and pick a format — PNG, JPG, WebP, or PDF.
2. Set a viewport (desktop, mobile, tablet, or custom) and, if you want, dark mode.
3. It navigates each URL, optionally waits for a selector or a fixed delay, then captures.
4. You get one dataset row per URL with the capture's metadata, plus the actual file sitting in the run's key-value store.

***

### Input

```json
{
  "urls": [
    "https://example.com/",
    "https://example.com/pricing",
    "https://example.com/about"
  ],
  "format": "png",
  "viewport": "desktop",
  "fullPage": true,
  "darkMode": false,
  "waitMs": 1000
}
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `urls` | array | — | List of URLs to capture. Each one produces one dataset row and one binary in the key-value store. |
| `format` | string | `"png"` | Output format: `png`, `jpg`, `webp`, or `pdf`. Applies to the whole run. |
| `viewport` | string | `"desktop"` | `desktop` (1920x1080), `mobile` (390x844), `tablet` (820x1180), or `custom`. |
| `customWidth` | integer | `1280` | Used when `viewport` is `custom`. |
| `customHeight` | integer | `800` | Used when `viewport` is `custom`. |
| `fullPage` | boolean | `true` | Capture the full scrollable page instead of just the viewport. |
| `darkMode` | boolean | `false` | Emulate `prefers-color-scheme: dark` before capturing. |
| `waitForSelector` | string | — | CSS selector to wait for before capturing, with a 10s timeout fallback. |
| `waitMs` | integer | `1000` | Extra wait after navigation completes, in milliseconds. |
| `quality` | integer | `85` | JPG/WebP quality (1-100). Ignored for PNG and PDF. |
| `maxItems` | integer | — | Safety cap on how many URLs to capture in this run. Leave blank to capture every URL in the list. |

#### PDF example

```json
{
  "urls": ["https://example.com/invoice/2026-08-01"],
  "format": "pdf"
}
```

#### Mobile, dark mode example

```json
{
  "urls": ["https://example.com/"],
  "format": "jpg",
  "viewport": "mobile",
  "darkMode": true
}
```

***

### Bulk Screenshot & PDF Generator Output Fields

```json
{
  "url": "https://example.com/",
  "format": "png",
  "kvsKey": "b559c7edd3fb67374c1a25e739cdd7edd1d79949-png",
  "sizeBytes": 22134,
  "viewport": { "width": 1920, "height": 1080, "preset": "desktop" },
  "capturedAt": "2026-08-04T02:53:13.724Z",
  "status": "success",
  "errorMsg": null,
  "finalUrl": "https://example.com/",
  "title": "Example Domain",
  "loadTimeMs": 1329
}
```

| Field | Type | Description |
|-------|------|--------------|
| `url` | string | Target URL that was captured |
| `format` | string | Output format: `png`, `jpg`, `webp`, or `pdf` |
| `kvsKey` | string | Key in the run's default key-value store holding the binary. `null` if the capture failed. |
| `sizeBytes` | number | Binary size in bytes |
| `viewport` | object | Viewport used: `width`, `height`, `preset` |
| `capturedAt` | string | ISO-8601 timestamp when the capture completed |
| `status` | string | `success` or `error` |
| `errorMsg` | string | Error message if the capture failed, otherwise `null` |
| `finalUrl` | string | Final URL after redirects |
| `title` | string | Page title at capture time |
| `loadTimeMs` | number | Milliseconds from navigation start to capture |

Fetch the binary itself from the run's key-value store using `kvsKey` — the dataset row is metadata, the KVS entry is the actual PNG/JPG/WebP/PDF file.

***

### Resuming a large crawl

Every run emits a `resumeCursor` in its Output. If a large crawl stops before it finishes — because it hit `maxItems`, your spend cap (`maxTotalChargeUsd`), or was aborted — start a new run with **the same input** plus that `resumeCursor` to continue from where it left off. The crawl resumes from the queued work the previous run didn't reach.

- You are **not re-charged** for records the earlier run already delivered.
- Resume within your account's run-retention window — on the free tier, roughly your 10 most recent runs. Once the source run is pruned, its `resumeCursor` is no longer valid.
- `resumeCursor` is opaque — supply it unmodified.

***

### FAQ

#### How do I convert a URL to PDF in bulk?

Set `format` to `"pdf"` and pass your URL list in `urls`. Each URL gets its own PDF in the key-value store and a matching dataset row with the `kvsKey` to fetch it.

#### Is there a bulk screenshot API on Apify?

Yes — this actor. Pass a list of URLs and pick a format; the Apify API returns one dataset row per URL with a `kvsKey` pointing at the captured file, so you can script it into any pipeline that speaks HTTP.

#### What happens if a URL fails to load?

It still gets a dataset row, with `status: "error"` and an `errorMsg` describing what went wrong. Nothing is silently dropped — you always get one row per input URL.

#### Can I capture mobile screenshots?

Set `viewport` to `"mobile"` for a 390x844 viewport, or `"custom"` with your own `customWidth`/`customHeight`.

#### Do I need to set anything up before running it?

No. Point it at a URL list and run it — the capture, formatting, and storage are all handled for you.

***

### Need More Features?

Need a different output format, a per-URL viewport, or something else entirely? [File an issue](https://console.apify.com/actors/issues) or get in touch.

### Why Use Bulk Screenshot & PDF Generator?

- **Actually bulk** — one run, one URL list, one format decision. No looping single-URL calls yourself.
- **PDF included** — most screenshot actors stop at PNG. This one prints to PDF in the same code path, so you don't need a second tool for invoices, contracts, or archival captures.
- **Nothing gets lost** — every URL in your list produces exactly one dataset row, success or failure, so a partial run never leaves you guessing which URLs didn't make it.

# Actor input Schema

## `sp_intended_usage` (type: `string`):

What will this data feed? E.g. lead lists, KYB checks, price tracking.

## `sp_improvement_suggestions` (type: `string`):

Provide any feedback or suggestions for improvements.

## `sp_contact` (type: `string`):

We'll personally help with your use case. No spam.

## `resumeCursor` (type: `string`):

Leave empty for a fresh crawl. To CONTINUE a previous run where it stopped — without paying again for records you already received — paste the `resumeCursor` value from that run's Output (the run's OUTPUT key). Resume promptly: the previous run's data expires with your account's retention window (free tier: your ~10 most recent runs).

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

List of URLs to capture. Each URL produces one dataset row plus one binary in the key-value store.

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

No description

## `viewport` (type: `string`):

No description

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

Used when Viewport Preset is Custom.

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

Used when Viewport Preset is Custom.

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

Capture the full scrollable page, not just the viewport.

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

Emulate prefers-color-scheme: dark.

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

CSS selector to wait for before capturing.

## `waitMs` (type: `integer`):

Additional wait after navigation completes, in milliseconds.

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

JPG/WebP quality. Ignored for PNG/PDF.

## `maxItems` (type: `integer`):

Safety cap on how many URLs to capture in this run. Leave blank to capture every URL in the list.

## Actor input object example

```json
{
  "sp_intended_usage": "Describe your intended use...",
  "sp_improvement_suggestions": "Share your suggestions here...",
  "sp_contact": "Share your email here...",
  "urls": [
    "https://example.com/"
  ],
  "format": "png",
  "viewport": "desktop",
  "customWidth": 1280,
  "customHeight": 800,
  "fullPage": true,
  "darkMode": false,
  "waitMs": 1000,
  "quality": 85
}
```

# 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 = {
    "sp_intended_usage": "Describe your intended use...",
    "sp_improvement_suggestions": "Share your suggestions here...",
    "sp_contact": "Share your email here...",
    "urls": [
        "https://example.com/"
    ],
    "format": "png",
    "viewport": "desktop",
    "fullPage": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("jungle_synthesizer/website-screenshot-pdf-bulk").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 = {
    "sp_intended_usage": "Describe your intended use...",
    "sp_improvement_suggestions": "Share your suggestions here...",
    "sp_contact": "Share your email here...",
    "urls": ["https://example.com/"],
    "format": "png",
    "viewport": "desktop",
    "fullPage": True,
}

# Run the Actor and wait for it to finish
run = client.actor("jungle_synthesizer/website-screenshot-pdf-bulk").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sp_intended_usage": "Describe your intended use...",
  "sp_improvement_suggestions": "Share your suggestions here...",
  "sp_contact": "Share your email here...",
  "urls": [
    "https://example.com/"
  ],
  "format": "png",
  "viewport": "desktop",
  "fullPage": true
}' |
apify call jungle_synthesizer/website-screenshot-pdf-bulk --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=jungle_synthesizer/website-screenshot-pdf-bulk",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/1F4DY5XvKs0obOWBv/builds/iIhrNo5dsT9A6qQIU/openapi.json
