# URL to PDF: Save Web Pages as PDF Documents (`ataism/url-to-pdf`) Actor

Save any list of URLs as PDF in real Chromium: A4, Letter or Legal, landscape, margins, print CSS, page numbers, public links. Blocks are free. $7 per 1K PDFs of up to 20 pages.

- **URL**: https://apify.com/ataism/url-to-pdf.md
- **Developed by:** [Kaizoku Data](https://apify.com/ataism) (community)
- **Categories:** Developer tools, Automation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $7.00 / 1,000 pdf saveds

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

## URL to PDF: Save Web Pages as PDF Documents

Save a **list of web pages as PDF files**, printed by a real Chromium browser. Choose **A4, Letter or Legal**, portrait or **landscape**, your **margins** and **scale**, the page's **print or screen styles**, and optional **page numbers with the URL and date**. Each PDF is stored in the run's key-value store with a **public link**. Each row lists the final URL, HTTP status, title, page count, file size and load time. No proxy, no key.

**Price: $7 per 1,000 PDFs ($0.007 each, up to 20 PDF pages included).** Longer documents cost **$0.0001 per page after the 20th** ($0.10 per 1,000 pages). Apify's default Actor-start event applies. Failed pages, blocks, CAPTCHA or bot-challenge pages, timeouts and refused URLs are free.

- **Paper and layout:** A4, Letter or Legal; portrait or landscape; margins from 0 to 50 mm; scale from 10% to 200%; backgrounds on or off.
- **Print or screen styles:** `print` uses the site's own print stylesheet, as when you press Print in a browser. `screen` keeps the page as it looks on screen, cut into pages.
- **Page numbers on request:** a fixed header (date and URL) and footer ("Page X of Y"). No custom HTML is accepted, so nothing can be injected.
- **Honest output:** the page count, paper size, PDF version and file size are read back from the saved file. A page longer than **Max pages** is cut there and marked `truncated`.
- **Quick start:** press *Start*. The prefilled input saves `https://crawlee.dev/` (a site run by Apify) as an A4 PDF of 4 pages. It takes under a minute (about 15–20 seconds) and costs $0.007.

> **Safe and polite by design.** robots.txt is read and obeyed for every page and every redirect hop. The browser's User-Agent says it is an automated Chrome and names this Actor. Every request a page makes goes through a guard that refuses private, internal and cloud-metadata addresses. Blocks and challenges become free rows: they are never retried or worked around.

### What it does

For each URL:

1. **Guard.** The URL must be http(s). Social-media, video and Google hosts are refused before any DNS lookup (`platform`), and so are private, loopback and link-local addresses (`ssrf`).
2. **robots.txt.** The robots.txt of the page's origin is fetched once per run with the product token `url-to-pdf-actor` and obeyed. A `Crawl-delay` is honoured in full. Pages on one host are loaded at most once per second. The page's own images, scripts, styles and fonts load as in any browser and pass the address guard. Preloading is switched off, so a page cannot make the browser fetch other pages it was not sent to. Speculation rules and `<link rel=prefetch|prerender>` are removed before the browser acts on them, whether they are in the HTML, added by scripts or sent as headers. Any remaining prefetch request is refused.
3. **Fresh browser context.** Each URL gets its own isolated context (no cookies, cache, storage or service workers shared with other URLs). One Chromium is reused for the whole run.
4. **Load.** The page is loaded, with the screen styles switched on first if you asked for them. Every redirect hop, and every later navigation of the page itself, is checked against the same rules (platform list, addresses, robots.txt) **before it is requested**. A refusal (401, 403, 429, 503, 999), a bot challenge or CAPTCHA wall, or an HTTP error gives a free row without a PDF.
5. **Wait.** The Actor waits for your chosen load state, then your delay. With **Hide cookie banners** on, it adds the consent stylesheet. It then scrolls down the page once (at most 3 s), so images that load on scroll start loading. Images marked `loading="lazy"` are loaded by the browser when it prints.
6. **Print.** Chromium prints the page with your paper, margins, scale and background settings, pages 1 to **Max pages**. The PDF is checked before it is saved: it must be readable and at most 50 MB, and its page count and paper size are read from the file. It is saved to the key-value store as `application/pdf`.
7. **Row.** The row is written with the public link. A row is charged only after both the PDF and the row are saved.

### Input

| Field | What it does |
|---|---|
| **URLs** (`urls`) | Pages to save, one per line, up to 200 per run. `example.org` becomes `https://example.org/`. Duplicates are saved once. |
| **Paper format** (`format`) | `A4` (default, 210 × 297 mm), `Letter` (8.5 × 11 in) or `Legal` (8.5 × 14 in). |
| **Landscape** (`landscape`) | Default off. On: the paper is turned sideways. |
| **Margins (mm)** (`margins`) | 0–50, default 10, on all four sides. With the header and footer on, top and bottom are at least 15 mm. |
| **Print backgrounds** (`printBackground`) | Default on: background colours and images are printed, so the PDF looks like the page. Off: a white background, as in a browser's default print. |
| **Scale (%)** (`scale`) | 10–200, default 100. Below 100 fits more on each page. |
| **CSS media type** (`mediaType`) | `print` (default): the site's print styles. `screen`: the on-screen look. |
| **Header and footer** (`headerFooter`) | Default off. On: date and time (UTC) and the page's URL at the top of every page; "Page X of Y" at the bottom. The text is fixed. |
| **Max pages per PDF** (`maxPages`) | 1–300, default 100. A longer page is cut and marked `truncated`. Pages after the 20th cost $0.0001 each. |
| **Wait until** (`waitUntil`) | `load` (default), `domcontentloaded` or `networkidle` (the load event, then up to 10 s more until the network is quiet for 0.5 s). |
| **Extra delay (ms)** (`delayMs`) | 0–10,000, default 0. |
| **Hide cookie banners** (`hideCookieBanners`) | Default on. Hides the containers of about 20 common consent tools (OneTrust, Cookiebot, Didomi, Quantcast, TrustArc, Usercentrics, Osano, Termly, Iubenda, Complianz, CookieYes, Sourcepoint, Axeptio, Borlabs, consentmanager and others) with one stylesheet. |

#### Example inputs

**1. A list of pages as A4 PDFs with the default settings (archiving, sharing, offline reading):**

```json
{
  "urls": ["https://crawlee.dev/", "https://www.python.org/", "https://go.dev/"]
}
```

**2. US Letter with page numbers, the date and the URL on every page (reports, evidence of what a page said):**

```json
{
  "urls": ["https://books.toscrape.com/", "https://quotes.toscrape.com/"],
  "format": "Letter",
  "headerFooter": true,
  "margins": 12
}
```

**3. A wide page as it looks on screen, in landscape, at most 10 pages:**

```json
{
  "urls": ["https://books.toscrape.com/catalogue/category/books/travel_2/index.html"],
  "mediaType": "screen",
  "landscape": true,
  "scale": 80,
  "maxPages": 10
}
```

### Output

You get one item per URL (JSON, CSV, Excel or XML). The **Overview** view links each PDF, and the **Free rows and problems** view lists what was not saved and why. Example from a real run:

```json
{
  "url": "https://crawlee.dev/",
  "requestedUrl": "https://crawlee.dev/",
  "finalUrl": "https://crawlee.dev/",
  "httpStatus": 200,
  "title": "Crawlee · Build reliable crawlers. Fast.",
  "status": "ok",
  "billable": true,
  "chargedEvent": "pdf",
  "pdfKey": "pdf-crawlee.dev-151c821820-5ad554f4.pdf",
  "pdfUrl": "https://api.apify.com/v2/key-value-stores/<store id>/records/pdf-crawlee.dev-151c821820-5ad554f4.pdf",
  "pages": 4,
  "extraPages": 0,
  "truncated": false,
  "bytes": 698648,
  "sha256": "3475837f…",
  "pdfVersion": "1.4",
  "format": "A4",
  "landscape": false,
  "pageWidthPt": 595.92,
  "pageHeightPt": 841.92,
  "mediaType": "print",
  "loadMs": 5797,
  "loadState": "load",
  "renderMs": 11996,
  "pageBytes": 871206,
  "pageRequests": 18,
  "blockedRequests": 6,
  "blockedHosts": ["www.googletagmanager.com (platform)", "analytics.tiktok.com (platform)", "www.redditstatic.com (platform)", "analytics.twitter.com (platform)", "snap.licdn.com (platform)"],
  "note": null,
  "errorCode": null,
  "message": null,
  "capturedAt": "2026-10-26T21:23:51.713Z",
  "urlKey": "https://crawlee.dev/"
}
```

(Times are from a run limited to a quarter of a CPU core, the platform's share at 1 GB.)

| Field | Meaning |
|---|---|
| `status`, `billable`, `chargedEvent` | `ok` = PDF saved and charged (`pdf`); `error` = free row, see `errorCode` and `message`. |
| `pdfUrl`, `pdfKey` | Public link to the PDF and its key in the run's default key-value store. |
| `pages`, `extraPages`, `truncated` | Pages in the saved PDF; pages after the 20th (charged as `extra-page`); `true` when the page was longer than **Max pages** (or than the run's maximum charge pays for) and the PDF stops there. |
| `bytes`, `sha256`, `pdfVersion`, `pageWidthPt`, `pageHeightPt` | Read from the saved file. Sizes are in points (1/72 inch): A4 595.92 × 841.92, Letter 612 × 792, Legal 612 × 1008, swapped in landscape. |
| `finalUrl`, `httpStatus`, `title` | The page after redirects, its status and its title (at most 300 characters). |
| `loadMs`, `loadState`, `renderMs` | Time to the load state that was reached, which state, and the total browser time for this URL. |
| `pageBytes`, `pageRequests` | What the page downloaded through the Actor's guard. |
| `blockedRequests`, `blockedHosts` | Requests of the page that were refused, with up to 10 hosts and reasons (`ssrf`, `platform`, `port`). The page is still printed. |
| `note` | For example "The load event did not fire within 30 s; printed after DOMContentLoaded", or "The page is longer than 100 PDF pages; the PDF stops at page 100". |

#### Error codes (all free)

| Code | Meaning |
|---|---|
| `invalid-input` | Not an http(s) URL, embedded credentials, or longer than 2,048 characters (`file:`, `chrome:`, `javascript:`, `data:` and the like are refused). |
| `platform` | A social-media, video or Google host (YouTube, TikTok, Instagram, Facebook, X, Vimeo, SoundCloud, Spotify, Twitch, Reddit, LinkedIn, Google domains), also when reached by a redirect. Never contacted. |
| `ssrf` | The host is, or resolves to, a private, loopback, link-local, carrier-grade NAT or reserved address, also after a redirect. |
| `robots-blocked` | robots.txt disallows the page, or a redirect target, for this Actor. Not requested. |
| `robots-unavailable` | robots.txt answered 5xx or 429, or could not be read (tried twice). The page is not loaded without its rules. |
| `slow-host` | robots.txt asks for a `Crawl-delay` over 30 s, or the run's 600 s of Crawl-delay waiting is used up. The delay is never shortened. |
| `refused` | The page answered 401, 403, 429, 503 or 999. Not retried, and no other method is tried. |
| `challenge` | A bot challenge or CAPTCHA wall (a Cloudflare challenge, hCaptcha or reCAPTCHA wall, PerimeterX, DataDome, or a bare "Access denied" page). Never solved or worked around. |
| `host-skipped` | The host refused or challenged 2 pages in this run, so it is not asked again. |
| `http` | Another HTTP error (for example 404 or 500). No PDF is made of an error page. |
| `dns`, `network`, `tls` | The name does not resolve, the connection failed, or the HTTPS certificate was not accepted. |
| `timeout` | No HTML within 30 s, printing took over 60 s, or the page used up its time budget. |
| `download` | The URL starts a file download instead of showing a page. Downloads are refused. (To fetch existing PDF files, use a file downloader.) |
| `too-large` | The page downloaded more than 50 MB or made more than 1,500 requests, or the PDF is larger than 50 MB. |
| `redirects`, `invalid-url` | More than 5 redirects (HTTP or page-initiated), or a redirect to an unusable URL or port. |
| `page-crashed`, `browser-crashed` | The tab or the browser crashed, usually because the page needs more memory. The next URL gets a fresh browser. |
| `navigation`, `store-failed`, `internal` | Another loading error, the PDF could not be saved, or an unexpected error in this Actor. |

#### RUN_SUMMARY (key-value store record)

This record holds:

- charged and free rows, and the charged events (`pdf`, `extra-page`);
- PDFs, PDF pages, PDF bytes, and PDFs cut at **Max pages**;
- browser time (`browserSecs`, `browserSecsUnpaid`) and time per step (`phaseMs`);
- bytes downloaded by pages, requests, and refused requests;
- browser launches and crashes;
- errors by code;
- `stoppedBy`: `finished`, `max-charge`, `unpaid-work-cap`, `time-limit`, `migrating`, `aborted`, `browser-crashed`, `browser-launch` or `error`.

### Pricing

Pay per event:

- **`pdf` = $0.007**: one PDF saved, up to 20 PDF pages included.
- **`extra-page` = $0.0001**: each page after the 20th of a saved PDF. Most web pages print to fewer than 20 pages; long documentation pages, specifications and articles can print to 50–100 or more, and they take more browser work.

These are free:

- URLs whose page was blocked or showed a challenge;
- URLs that timed out or failed;
- URLs that were refused (robots.txt, platform, private addresses) or invalid.

There is no start fee of our own; Apify's default Actor-start event applies.

| PDFs saved | Cost |
|---|---|
| 1 (the prefilled example, 4 pages) | $0.007 |
| 100 ordinary pages (20 PDF pages or fewer each) | $0.70 |
| 1 long document of 100 pages (the default **Max pages**) | $0.007 + 80 × $0.0001 = $0.015 |
| 200 ordinary pages (the per-run maximum) | $1.40 |
| 10,000 ordinary pages (50 runs) | $70 |

Set a **maximum charge per run** in the run options. The Actor stops cleanly before the next URL it could not pay for: that URL is not loaded and no PDF is saved. A long document is printed only up to the pages the remaining budget pays for. It is cut there, marked `truncated`, and the note says why. To protect you from paying for compute on bad lists, the run also stops when:

- free rows reach 50 and outnumber the charged ones;
- browser time spent on free rows passes 300 s plus 2 s per saved PDF;
- 300 MB were downloaded for free rows.

**Memory.** The default and minimum is 1 GB. More memory makes each page faster, but the price per PDF is the same.

### Use it via API, n8n or Make

Get an API token in Apify Console under *Settings*, *API & Integrations*, and keep it in an environment variable (`APIFY_TOKEN` below). Never paste it into shared code.

**cURL** (starts a run, waits and returns the rows; the synchronous endpoint waits up to 5 minutes, so use it for lists of up to about 20 URLs):

```bash
curl -X POST "https://api.apify.com/v2/acts/ataism~url-to-pdf/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://crawlee.dev/"], "format": "Letter"}'
```

**Python** (`pip install "apify-client>=3"`):

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("ataism/url-to-pdf").call(run_input={
    "urls": ["https://crawlee.dev/", "https://www.python.org/"],
    "format": "A4",
    "headerFooter": True,
})
for row in client.dataset(run.default_dataset_id).iterate_items():
    print(row["url"], row["status"], row["pages"], row["pdfUrl"], row["errorCode"])
```

(With apify-client versions before 3.0, `call()` returns a dict: use `run["defaultDatasetId"]`.)

**JavaScript** (`npm install apify-client`):

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('ataism/url-to-pdf').call({
    urls: ['https://crawlee.dev/'],
    landscape: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const row of items) console.log(row.url, row.pages, row.bytes, row.pdfUrl);
```

**n8n:**

1. Add the **Apify** node, operation **Run an Actor and get dataset**, Actor `ataism/url-to-pdf`, input `{"urls": ["https://your-site.example/"]}`.
2. Use `pdfUrl` in the next node, for example an **HTTP Request** node that downloads the file to attach it to an email or upload it to Google Drive.

**Make:**

1. Add the **Apify** module **Run an Actor**, choose `ataism/url-to-pdf`, put your URLs in the input JSON and set *Run synchronously* to *Yes*.
2. Add the **Apify** module **Get Dataset Items** and map the run's default dataset ID (one bundle per URL).
3. Send `url`, `pdfUrl`, `pages` and `status` to Google Sheets, Airtable or your document store.

### Privacy, rights and terms

- **You are responsible for having the rights to save the pages you list as PDF, and to use the PDFs.** A PDF copies the page's text, images, logos and any personal data shown on it. Save only pages you are allowed to save: your own sites, sites whose terms allow it, or uses your law permits (for example archiving your own pages or keeping a record of public terms you agreed to). Nothing here is legal advice.
- **No logins, no paywalls, no consent clicking.** The Actor sends no cookies or credentials of yours, and it never clicks a button. The cookie-banner option only hides banners with CSS, so a page that shows content only after consent is printed as it looks without consent.
- **Public links.** PDFs are saved in your run's key-value store. Their links can be opened by anyone who has them (like any Apify record URL). Delete the store, or keep the links private, if that matters to you.
- **What the output contains:** the page's URL, status, title, timings and the PDF. With the header and footer on, the PDF also shows the date, time and URL. The Actor adds nothing else about the page, but the PDF shows whatever the page shows.
- **Polite access.** robots.txt is obeyed for every page and every redirect. The browser's User-Agent ends with `url-to-pdf-actor/0.1 (+https://apify.com/ataism/url-to-pdf)` and still says `HeadlessChrome`. Blocks and challenges are reported, never worked around. No proxies are used. Social-media, video and Google sites are refused.

### Security

The browser loads pages from the open web, so the Actor treats each page as untrusted:

- **Every request of the page is guarded.** The browser has no direct network access. Each request goes through a small guard proxy inside the run: the page, every redirect, iframes, scripts, images, fonts, fetch/XHR, WebSockets and preconnects. The guard resolves the host name itself and refuses private, loopback, link-local and reserved addresses (IPv4 and IPv6, including IPv4-mapped, NAT64 and 6to4). It then connects to exactly the address it checked, so a name that changes its answer (DNS rebinding) gains nothing. Chrome's own DNS is switched off. WebRTC is limited to the proxy, and QUIC (HTTP/3) is off. Only ports 80, 443, 8080, 8443 and the page's own port are allowed. HTTPS is not intercepted: certificates are checked by Chrome as usual.
- **Not just the first request.** Redirects and page-initiated navigations are paused and checked before they are sent. If the page navigates away while it is being printed and that navigation is refused or fails, the row is free. Chrome's error page is never printed.
- **Nothing from the input runs or is inserted.** The only scripts the Actor runs in a page are its own fixed functions: reading the title, the challenge check, the scroll, the banner stylesheet and the preloading guard (it runs in a separate script world the page cannot reach). The header and footer are fixed templates; Chrome fills in the date, URL and page numbers as text.
- **Nothing in the PDF can act.** Chrome's PDFs contain no scripts, forms, attachments or open actions. Links stay clickable when they point to web or mail addresses (`http`, `https`, `mailto`, `tel`). Links to `javascript:`, `file:`, `data:` and other targets are disabled in the saved file, and the row's `note` says how many. The page count and paper size are read from the PDF's own page tree, so text on the page (such as its title) cannot change them or the price.
- **No downloads, no service workers, no popups, no preloading, no dialogs.** Downloads are refused and service workers are blocked. Popups and new tabs are never opened, so nothing they would load is requested. The page's prefetch and prerender requests are removed or refused; every page the browser loads goes through the check above. Dialogs are dismissed. Video and audio are not loaded (poster images still show).
- **No secrets in the browser.** Chromium starts with a short list of environment variables. The run's API token is not among them.
- **Bounded.** Each page has a hard time budget (at most about 110 s plus your delay), 50 MB and 1,500 requests, and the PDF itself at most 50 MB. A crashed tab or browser gives a free row, and the next URL gets a fresh one.

### Limits and honest caveats

- **Max pages:** at most 300 pages per PDF (default 100). Longer pages are cut and marked `truncated`.
- **Print styles decide the look.** With `print` (the default), many sites hide menus, ads and some images when printed, and some show link addresses after links. Use `screen` to keep the on-screen look. The paper size is always the one you chose; a page's own `@page` size is ignored. Print styles also decide the page count: a page whose print style hides its content gives a blank PDF, and one with very large print text gives many pages (charged, up to **Max pages**). Check a sample first, and lower **Max pages** if you are unsure.
- **Pages that scroll inside a container** (some web apps keep the page at the window height and scroll an inner element) print only the part that fits in the window.
- **Lazy content:** images marked `loading="lazy"` are loaded for printing. Images that a script loads on scroll start loading during the Actor's one scroll down the page (at most 3 s). Content that loads only on clicks or infinite scroll is not loaded.
- **Bot protection:** many large sites refuse automated browsers. Those rows are free (`refused` or `challenge`); the Actor will not try to get around them. Challenge detection is best-effort: a challenge served in an unusual way can still be printed and charged.
- **Cookie banners:** only the listed tools are hidden. A banner from another tool stays visible.
- **Google and social content inside pages:** pages may embed resources from Google, YouTube, Facebook, X and the like. Those are not loaded, so embedded videos, maps, posts and ad slots show as empty boxes. Web fonts from `fonts.googleapis.com` and `fonts.gstatic.com` are the one exception: they are loaded so text prints in the right font.
- **Preloading inside shadow DOM:** speculation rules that a page places inside a shadow root (a web component's own DOM) are outside the preloading guard and could still make the browser prefetch a page. We know of no site that does this. Such a prefetch is never printed or charged, because every navigation is checked against robots.txt before it is sent.
- **Header and footer:** fixed layout and text (date and time in UTC, US format; URL; "Page X of Y"). In a cut PDF, "of Y" counts the whole page.
- **Load time:** if the load event does not arrive within 30 s, the page is printed as it is at that moment (charged, with a `note`). If no HTML arrives within 30 s, the row is a free `timeout`.
- **PDF files only from web pages:** a URL that serves a file (a PDF, ZIP or the like) is a free `download` row.
- Per run: 200 URLs and 55 minutes (a run that reaches the time limit stops cleanly with `stoppedBy: time-limit`; split long lists); one page at a time, in one browser.

### FAQ

**Why is my PDF missing the menu or the background?** That is the site's print stylesheet (CSS media `print`). Set **CSS media type** to `screen`, and keep **Print backgrounds** on.

**Why is my PDF showing an "Access denied" page?** If the Actor recognised a block, the row is a free `refused` or `challenge` row. If the site served a normal-looking page that is empty until a login or consent, that is what the page looks like to a visitor without them.

**Can I save pages behind a login?** No. The Actor sends no cookies or credentials and never fills in forms.

**Can I convert my own HTML or merge PDFs?** Not with this Actor. It prints public web pages by URL, one PDF per URL.

### Open-source components

- `apify` SDK 3.7.2 (Apache-2.0).
- `playwright-core` 1.64.1 (Apache-2.0), with the Chromium build that ships in Apify's `actor-node-playwright-chrome` image.

Everything else (the guard proxy, robots.txt, the URL guard, challenge detection and the PDF reader) is written in this repository without further dependencies.

# Actor input Schema

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

Pages to save as PDF, one per line, up to 200 per run. "example.org" becomes https://example.org/. Only http and https pages; private, internal, social-media, video and Google hosts are refused. Save only pages you have the right to save.

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

A4 (210 x 297 mm), Letter (8.5 x 11 in) or Legal (8.5 x 14 in).

## `landscape` (type: `boolean`):

Turn the paper sideways (wide pages, tables).

## `margins` (type: `integer`):

White space on all four sides of every page, in millimetres (0-50). With the header and footer on, top and bottom are at least 15 mm so they have room.

## `printBackground` (type: `boolean`):

Include background colours and images, so the PDF looks like the page. Off: the browser's ink-saving print (white background); files are often smaller.

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

Zoom of the page content on paper (10-200). Below 100 fits more on each page; above 100 makes text larger.

## `mediaType` (type: `string`):

print: the page's own print styles, as when you press Print in a browser (often hides menus and ads). screen: the page as it looks on screen, cut into pages.

## `headerFooter` (type: `boolean`):

Add a small header with the date and the page's URL, and a footer with "Page X of Y". The text is fixed; custom HTML is not accepted.

## `maxPages` (type: `integer`):

The PDF stops after this many pages (1-300). A longer page is cut and marked truncated. Each PDF includes 20 pages; every page after the 20th costs $0.0001.

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

When the page counts as loaded. load: the load event (images, styles). domcontentloaded: the HTML is parsed (fastest, images may be missing). networkidle: the load event, then up to 10 s more until no request has run for 0.5 s. If the load event does not come within 30 s, the page is printed as it is and the row says so in note.

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

Wait this long after loading, before printing (0-10000), for animations or late content.

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

Best-effort: adds one stylesheet that hides the banners of common consent tools (OneTrust, Cookiebot, Didomi, Usercentrics and others). Nothing is clicked, accepted or rejected; banners of other tools stay visible. Paywalls and logins are never touched.

## `mode` (type: `string`):

Internal: "selfTest" runs the maintainer health check and saves no items.

## Actor input object example

```json
{
  "urls": [
    "https://crawlee.dev/"
  ],
  "format": "A4",
  "landscape": false,
  "margins": 10,
  "printBackground": true,
  "scale": 100,
  "mediaType": "print",
  "headerFooter": false,
  "maxPages": 100,
  "waitUntil": "load",
  "delayMs": 0,
  "hideCookieBanners": true,
  "mode": "run"
}
```

# Actor output Schema

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

One item per URL: public link to the PDF, pages, size, final URL, title, load time, and for free rows the error code and message (JSON, CSV, Excel or XML).

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

The PDFs in the run's key-value store (keys pdf-<host>-<hash>-<options>.pdf, content type application/pdf).

## `runSummary` (type: `string`):

Charged and free rows, charged events (pdf, extra-page), PDF pages, browser time, bytes, refused requests, errors by code, browser launches and why the run stopped.

# 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/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ataism/url-to-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/"] }

# Run the Actor and wait for it to finish
run = client.actor("ataism/url-to-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/"
  ]
}' |
apify call ataism/url-to-pdf --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ataism/url-to-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/kYXvXokm5K17ZCXFF/builds/8vnsc9fbqfTbim5zP/openapi.json
