# Website Screenshot & URL to PDF Converter (`luxe_tasks/screenshot-pdf`) Actor

Screenshots (PNG/JPEG, viewport/full page) and PDFs of public web pages. No charge for failed captures; $0.01 start fee per default 2 GB run, $0.005 per GB RAM.

- **URL**: https://apify.com/luxe_tasks/screenshot-pdf.md
- **Developed by:** [Luxe Tasks](https://apify.com/luxe_tasks) (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 $2.50 / 1,000 viewport screenshots

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 & URL to PDF Converter

Give it a list of public web page URLs and get back a **PNG or JPEG screenshot** (viewport or full page) or a **PDF** (A4, Letter and other sizes) of each page, with one result record per URL that links to the file.

**Cost:** from $2.50 per 1,000 viewport screenshots, plus a start fee of $0.01 per run at the default 2 GB of memory ($0.005 per GB). Failed, blocked, blank or skipped URLs have no per-result charge, but the run's start fee still applies. Full details are under Pricing below.

**Try it:** the input is prefilled with `https://example.com/`. Press Start, then open the dataset's Captures view and click the file link.

### How to take a full-page screenshot or save a web page as PDF

1. Paste your URLs into **URLs** (one per line, up to 1,000 per run).
2. Pick **Output format**: `png`, `jpeg` or `pdf`.
3. For a whole scrolling page, switch on **Full page** (PNG/JPEG only). For PDFs, choose the paper size and orientation.
4. Press **Start**. Each file goes to the run's key-value store, and each dataset record has a `link` to it.
5. To run it from code, use the JavaScript, Python or CLI examples in the API tab, or schedule the run in Apify.

### Example input and output

```json
{ "urls": ["https://example.com/"], "outputFormat": "png", "fullPage": false }
```

```json
{ "index": 3, "url": "https://example.com/", "status": "success", "ok": true, "format": "png", "bytes": 48213,
  "key": "capture-0003-example.com-1a2b3c4d5e.png", "link": "https://api.apify.com/v2/key-value-stores/<id>/records/capture-0003-…",
  "httpStatus": 200, "finalUrl": "https://example.com/", "redirects": 0, "truncated": false, "attempts": 1,
  "charged": true, "chargedEvents": 1, "chargeEvent": "screenshot", "pdfPages": null,
  "timingMs": { "total": 2140, "navigation": 610, "capture": 180 }, "blockedRequests": 0,
  "robots": { "checked": true, "allowed": true, "overridden": false, "attestation": null, "detail": "robots.txt HTTP 200" },
  "error": null }
```

### What it does

- Opens **only the URLs you list**. It never follows links, crawls a site or opens popups.
- Captures one file per URL: PNG, JPEG or PDF (A4, Letter, Legal, A3, A5, Tabloid; landscape; print backgrounds).
- **Full-page mode** scrolls the page step by step to trigger lazy-loaded content, waits for images to load and decode, scrolls back to the top, then captures. Pages taller than `maxFullPageHeight` (default 16,000 px) are cut at that height and marked `truncated: true`. A full-page capture is charged as one `full-page-screenshot` event (not as a `screenshot`).
- Optionally waits for a CSS selector or a fixed delay, hides elements you name and injects your own CSS.
- By default hides common **cookie-banner and ad-slot elements with CSS** (cosmetic only, see FAQ). Turn off with `hideCookieBannersAndAds: false`.
- Honours **robots.txt** for every URL, every redirect hop, and every image, iframe, script or other resource the page loads, and identifies itself with a fixed User-Agent:
  `<browser UA> ScreenshotActor/0.1.0 (+https://apify.com/luxe_tasks/screenshot-pdf; honours robots.txt)`.
- **At most one retry per URL**, in only these cases: a navigation timeout or a dropped/refused connection; an HTTP 429 that sends `Retry-After` of 60 s or less (the wait is honoured); and, for a domain you own and attested with `overrideDomainStop`, one retry after a 403, CAPTCHA page or 429 (see Responsible use). It never retries other HTTP errors, robots refusals, login walls, `selector_not_found`, or a URL that hit the hard per-URL limit (3 × `timeoutSecs` + 5 seconds, e.g. 95 s at the default `timeoutSecs` of 30).

### Input

| Field | Default | Notes |
|---|---|---|
| `urls` | required | A list, or one string with URLs separated by whitespace or commas. `example.com` becomes `https://example.com/`. Whitespace is trimmed and duplicates are removed. Max 1,000. |
| `outputFormat` | `png` | `png`, `jpeg`, `pdf` |
| `fullPage` | `false` | Full scrollable page (PNG/JPEG); charged as `full-page-screenshot` |
| `viewportWidth` / `viewportHeight` | 1280 / 800 | 320–3840 / 240–2000. A height above 2000 px is **rejected** (the run stops before any capture; the start fee still applies), not clamped: for taller output use `fullPage`. |
| `waitUntil` | `load` | `load`, `domcontentloaded`, `networkidle` |
| `waitForSelector` | – | Must become visible, otherwise `selector_not_found` |
| `delayMs` | 0 | Extra wait before capture (max 30,000) |
| `scrollToLoadLazy` | `true` | Scroll to trigger lazy content, wait for images |
| `maxFullPageHeight` | 16000 | Height cap in px (1,000–30,000) |
| `jpegQuality` | 80 | 1–100 |
| `pdfFormat`, `pdfLandscape`, `pdfPrintBackground` | A4, false, true | PDF options |
| `pdfMarginMm` | 0 | PDF margin on all four sides, 0–50 mm. With 0, the page's own CSS `@page { margin }` is used. |
| `pdfScale` | 1 | PDF content scale, 0.1–2 (smaller fits more on each page) |
| `hideCookieBannersAndAds` | `true` | Cosmetic CSS hiding of known banner/ad-slot selectors |
| `hideSelectors`, `customCss` | – | Your own selectors/CSS (`@import` and `url()` are refused) |
| `timeoutSecs` | 30 | Per-attempt navigation timeout, and the time allowed to take the screenshot/PDF (5–120). Very heavy full pages can need more than 30 s. |
| `maxConcurrency` | 3 | 1–5 pages in parallel, in one shared browser. Each site is also limited to about one page per second. Concurrency may be reduced automatically to fit the run's memory (at the default 2 GB, up to 3 viewport or 2 full-page pages in parallel; at 1 GB, one page at a time); more memory allows more parallel pages (the start fee is charged per GB of memory above 1 GB). |
| `ignoreRobotsTxt` + `robotsOwnershipAttestation` | off | **Only for a domain you own or control**: one domain, at most 10 URLs, with a written attestation. The attestation is stored in every result record. |

**Not accepted:** passwords, cookies, HTTP headers, auth tokens, sessions, proxies or a custom User-Agent. Inputs containing such fields are rejected before anything runs. URLs with `user:password@` are rejected too.

### Output

- **Files** go to the run's **default key-value store**, with keys like `capture-0003-example.com-1a2b3c4d5e.png`.
- **One dataset record per URL** goes to the run's **default dataset**:

```json
{ "index": 3, "url": "https://example.com/", "status": "success", "ok": true, "format": "png", "bytes": 48213,
  "key": "capture-0003-example.com-1a2b3c4d5e.png", "link": "https://api.apify.com/v2/key-value-stores/<id>/records/capture-0003-…",
  "httpStatus": 200, "finalUrl": "https://example.com/", "redirects": 0, "truncated": false, "attempts": 1,
  "charged": true, "chargedEvents": 1, "chargeEvent": "screenshot", "pdfPages": null,
  "timingMs": { "total": 2140, "navigation": 610, "capture": 180 }, "blockedRequests": 0,
  "robots": { "checked": true, "allowed": true, "overridden": false, "attestation": null, "detail": "robots.txt HTTP 200" },
  "error": null }
```

**Status values:** `success`, `invalid_input`, `ssrf_blocked`, `robots_disallowed`, `robots_unreachable`, `http_error`, `timeout` (the page did not load, or the screenshot/PDF could not be taken, within `timeoutSecs`; not charged, the start fee still applies), `too_large`, `blocked_or_captcha`, `rate_limited`, `skipped_blocked`, `login_required`, `navigation_error`, `selector_not_found`, `save_failed`, `charge_failed` (the charge could not be attempted; not charged, output deleted), `budget_reached`, `blank_capture` (the page rendered as a blank/uniform image or an empty file; not charged), `partial_capture` (the output file was incomplete, e.g. a PDF without its end marker; not charged), `delivered_record_failed` (rare: the file was saved and **charged**, but the full record could not be written; this short record has `charged: true` and the file `key`), `internal_error` (an unexpected error before anything was charged; **not charged**; the short record has the error message), `state_save_failed` (the progress marker could not be saved before charging, after 3 tries; **not charged**, output deleted), `charge_pending_interrupted` (the run restarted while this URL's charge was in progress and the file is gone; counted as **charged** with `chargeUnconfirmed: true` so it is never charged twice, and the record gives the `key`), `resumed_skip` (internal only: a restarted run skips a URL it already finished; no new record and no charge, so you will normally only see it in `RUN_SUMMARY` counts).

**Other record fields:** `warnings` (list of plain-language problems with a delivered file, e.g. `2 of 9 images did not load`, resources refused by robots.txt or by the network-safety checks, blank sections), `images` (`{ loaded, total }`), `imagesLoaded` (e.g. `"7/9"`), `robotsBlockedRequests` (resources the page tried to load that robots.txt refused; they were never requested), `blankSections` (for `blank_capture` on a mostly blank page), `chargeUnconfirmed` (see Limitations), `chargeStatus` (`"charge_uncertain"` on a `success` record when the charge request failed or returned an unknown result after it was sent: the file is kept, `charged` is `null`, and the URL is never charged again; see Billing order), and for PDFs `truncatedIsEstimate: true` (see Limitations). `url` and `finalUrl` never include query strings (`?[redacted]`) or `#fragments`.

A `RUN_SUMMARY` record in the key-value store gives the counts for each status (plus `charge_uncertain`, the number of `success` records whose charge result is unknown), the number of events charged, `chargeUncertain` (events per name whose charge result is unknown), and `skippedDomains`: every registered domain the actor stopped visiting during the run, with the hostname that refused (`refusingHost`), the reason (`403`, `captcha`, or `429 (...)`), the URL that triggered it, how many later URLs were skipped and on which hostnames. Each skipped URL's record has `status: "skipped_blocked"` and a `skippedDomain` object with the registered domain, the refusing host and the reason.

### Pricing

Note: Apify's pricing table lists *Actor Start* at $0.005. That price is per GB of memory, so a default 2 GB run is charged a $0.01 start fee. Apify's 'Free up to 2,000 viewport screenshots' line does not include the start fee.

Pricing: $2.50 per 1,000 viewport screenshots; $8 per 1,000 full-page screenshots; $3 per 1,000 PDF pages; plus a $0.01 start fee per run at the default 2 GB of memory (charged per GB: $0.005 at 1 GB, +$0.005 per extra GB). Failed captures are not charged.

- Event `screenshot`: one per delivered viewport PNG/JPEG. Event `full-page-screenshot`: one per delivered full-page PNG/JPEG (`fullPage: true`), charged **instead of** `screenshot`, never both. Event `pdf-page`: one per page of a delivered PDF (`fullPage` does not apply to PDFs).
- Viewport captures are limited to 3840 × 2000 px (no zoom, scale or clip options), so a viewport screenshot cannot stand in for a full page.
- **No per-result charge** for failures, timeouts, blocked, rate-limited, skipped or robots-refused URLs, invalid input, or anything not delivered; the per-run start fee still applies.
- **Billing order:** each file is saved to the key-value store and the save is confirmed *before* the event is charged; the dataset record (with the file link) is released only *after* a successful charge. If the save fails, nothing is charged (`save_failed`). If your budget cannot cover the item, or the charge could not be attempted at all, the saved file is deleted again and no success record is released (`budget_reached` / `charge_failed`, with `fileCleanup` showing the result of the deletion). If the charge request was sent but failed or returned an unknown result (Apify may have recorded it), the file is **kept** and delivered as `success` with `chargeStatus: "charge_uncertain"`, `charged: null` and `chargeUnconfirmed: true`; that URL is never charged again, in the same run or after a restart.
- Your *max total charge* for the run is respected. When the budget is used up, the actor stops capturing, marks the remaining URLs `budget_reached` and does not charge for them.
- **What you pay even when every URL fails:** the start fee, charged through Apify's built-in `apify-actor-start` event once per GB of run memory. That start fee is **$0.01 per run at the default 2 GB** ($0.005 at 1 GB, up to $0.04 at 8 GB). This Actor does not pass platform usage (compute, storage, transfer) on to you, so a run where every URL fails costs only the start fee.

### Acceptable use

> **Acceptable use.** This Actor captures screenshots or PDFs of **publicly accessible web pages that you choose**. By running it you confirm that:
>
> 1. You will submit only **public URLs** that you have the right or permission to capture, and you will use the output lawfully (for example archiving, QA, monitoring, research or evidence of your own pages or of public pages you are entitled to copy).
> 2. You will **not** submit pages behind a login, paywall, or other access control, and the Actor does not accept passwords, cookies or session tokens.
> 3. You will **not** use it to get around CAPTCHAs, rate limits, IP blocks, or any other technical measure a site uses to restrict access.
> 4. You will comply with the target site's terms of use and robots.txt, and with all applicable law, including copyright and data-protection law (GDPR, CCPA and similar). You are the controller of any personal data in your captures.
> 5. You will **not** capture, store or distribute illegal content, including child sexual abuse material, or use the Actor to harass, stalk, or profile individuals.
> 6. You will not use captures to impersonate a site or its owner, build phishing or look-alike pages, or republish others' pages or trademarks except where you are entitled to (for example your own pages, licensed use, or lawful quotation or reporting).
> 7. You will keep request volumes reasonable. The Actor enforces per-domain rate limits and identifies itself in its User-Agent.
>    The Actor may refuse or skip URLs that violate these rules. Report problems through the Actor's **Issues** tab.

### Privacy

> "Captures are written only to your run's default Apify storage. The developer does not copy, view or retain them. The Actor does not analyse captures or extract faces or other personal data. PDFs keep the page's text as rendered by the browser. If your captures contain personal data, you are the controller and the developer acts only as your processor under Apify's Standard Actor Contract. Apify's retention settings apply."

Outputs stay in **your** Apify account until you delete them or Apify's retention period ends. Logs record only the URL, the status and timings, never page content. Query strings are redacted (`?[redacted]`) in logs and results (dataset `url` and `finalUrl`).

### Responsible-use and robots.txt notes

- robots.txt is checked against both the `*` group and our `ScreenshotActor` token, and again on every redirect hop.
  - **Resources too:** every image, stylesheet, script, font, iframe and other request the page makes is checked against the robots.txt of **its own** host, on every redirect hop, before it is sent. A refused resource is not requested; the page is still captured without it, and the refusal is listed in `warnings` and `robotsBlockedRequests`. If a resource host's robots.txt is unreachable (5xx/network error), its resources are not loaded either.
  - A robots.txt that returns 4xx (except 429) means no restrictions; a 429 is treated as a rate limit and stops the domain; 5xx or an unreachable robots.txt means nothing is fetched.
  - **robots.txt 429:** if it sends `Retry-After` of 60 seconds or less, the actor waits that long and requests robots.txt again **once**. With no `Retry-After`, a longer one, or a second 429, the URL is reported as `rate_limited` and every later URL on that domain is skipped (`skipped_blocked`); no page on that domain is requested and nothing is charged.
  - A 5xx or unreachable robots.txt is treated as "disallow all" (RFC 9309), with status `robots_unreachable`.
- **Network safety:** only `http`/`https` are fetched.
  - Addresses are checked after DNS resolution, on every request and redirect hop, including images, iframes and scripts.
  - Private, loopback, link-local, carrier-grade-NAT, multicast and reserved addresses are refused, as are cloud metadata endpoints (`169.254.169.254`, `metadata.google.internal`). Those requests are blocked and counted in `blockedRequests`.
  - WebSockets are refused.
- No stealth plugins, CAPTCHA solving, proxy rotation or User-Agent rotation.
- **When a site says no, the actor stops visiting that domain for the rest of the run:**
  - **HTTP 403 or a CAPTCHA/bot-wall page:** reported as `blocked_or_captcha`; no retry; every later URL on that domain is skipped (`skipped_blocked`) without being requested.
  - **HTTP 429:** if the site sends `Retry-After` of 60 seconds or less, the actor waits that long and retries **once**. With no `Retry-After`, a longer one, or a second 429, it is reported as `rate_limited` and the domain is skipped for the rest of the run.
  - **HTTP 404 or 5xx:** only that URL fails (`http_error`); the domain is not stopped.
  - A "domain" here is the **registered domain** (eTLD+1, from the Public Suffix List including its private section): a refusal from `shop.example.com` stops every `*.example.com` URL in the run. Customer sites on shared hosts (`alice.github.io`, `a-store.myshopify.com`, `x.blogspot.com`, `y.herokuapp.com`) count as separate domains. An IP-address URL stops that exact IP (any port).
  - **PSL updates:** the list is the snapshot bundled in the exact-pinned `tldts` package (7.4.19, published 2026-10-20 per the npm registry), so it works offline. To update: `npm install --save-exact tldts@<new>`, run `npm test` (the `domain-scope` tests cover co.uk, github.io and myshopify.com), and note the version here.
- **Runtime:** Node.js 24 everywhere (G-8): `package.json` engines `>=24 <25`, `.nvmrc` 24, Docker base image `apify/actor-node-playwright-chrome:24-1.64.1` (Apify base images support Node 22, 24 and 26: https://docs.apify.com/sdk/js/docs/guides/docker-images), with Playwright pinned to 1.64.1 to match the image. The unit tests also pass on Node 20.19.2 (the reviewers' box), but Node 20 is not a supported runtime for this Actor.
  - Requests to one domain run one at a time, in input order, so no further request is sent once a domain is stopped.
  - **Owners only:** `overrideDomainStop` (with the ownership attestation naming your registered domain; all URLs on that one registered domain, subdomains included; up to 10 URLs) keeps the domain open, but each URL still gets at most one retry: after a 403 or CAPTCHA page the retry is immediate; after a 429 the actor waits for `Retry-After` (capped at 60 s), or **5 seconds** if the site sent none. A second refusal ends that URL (`blocked_or_captcha` / `rate_limited`) but does not stop the domain. The attestation is logged in every record.

### Limitations

- Pages behind logins or paywalls are out of scope by design, and redirects into a login page end as `login_required`.
- Very heavy pages (large articles with many images) can take longer than 30 s to capture in full-page mode; they end as `timeout` (no per-result charge; the per-run start fee still applies; earlier versions labelled this `navigation_error`). Raise `timeoutSecs` for such pages.
- Very tall pages are truncated at `maxFullPageHeight`. PDFs are capped at 50 pages.
- **Memory:** the default is 2 GB. Heavy pages (large JavaScript sites, very tall full-page captures) may need 4 GB; for those, choose 4 GB when you start the run (each extra GB adds $0.005 to the start fee). For light pages, 1 GB is cheaper ($0.005 start fee) but renders one page at a time. The Actor automatically lowers concurrency to fit the run's memory (at the default 2 GB, up to 3 viewport or 2 full-page pages in parallel; at 1 GB, one page at a time), so more memory also allows more pages in parallel.
  - **Out of memory:** `navigation_error` with "Page crashed" or "browser has been closed", or a `timeout` with "hard per-URL limit 95s exceeded" (3 × `timeoutSecs` + 5 s; 95 s at the default 30) on a heavy page, usually means the run ran out of memory (this page or another one in progress). After "browser has been closed" the Actor relaunched the browser and continued with the remaining URLs. The affected URL was not charged (the start fee still applies). Run it again with more memory.
  - **PDF `truncated` is an estimate:** it is `true` when the PDF reached the 50-page cap; the browser does not tell us whether more pages would have followed, so a document of exactly 50 pages is also marked. Use `pdfPages` for the real page count of the delivered file (that is also what you are charged for).
  - **PDF page setup (measured with Chrome):** the paper is always `pdfFormat`/`pdfLandscape`. A page's CSS `@page { size }` does not change the paper, but Chrome still applies its orientation keyword (e.g. `landscape`) and lays the content out in the `@page` box, centred on the sheet. A CSS `@page { margin }` is used when `pdfMarginMm` is 0; a non-zero `pdfMarginMm` replaces it.
- **`#fragments` are kept:** `https://site/app#/pricing` opens the `#/pricing` view of hash-routed apps and jumps to anchors. The fragment never leaves the browser (it is not sent to the site) and is removed from logs and results. URLs that differ only by fragment are captured separately.
- Each fetched resource is capped at 10 MB, so larger documents end as `too_large`. Output files are capped at 20 MB.
- **Blank pages are not billed:** before capturing, the actor checks a viewport image; if it is a single uniform colour (fewer than 64 pixels differ), the URL is `blank_capture`, even on a tall page with content further down. For full-page images, the delivered file is also split into screen-height sections: if more than half of them are blank, the URL is `blank_capture` (not charged; `blankSections` gives the counts); if only some are blank, the file is delivered and charged with a warning. The 50% threshold is our choice, not a standard. Output files that are empty or structurally incomplete are `partial_capture`. Pages that load but are partly missing images are still delivered: check `imagesLoaded` and `warnings`.
- **CAPTCHA detection is narrow:** a page counts as a CAPTCHA/bot wall only on specific challenge markers (Cloudflare, DataDome, PerimeterX, Imperva, AWS WAF, Akamai "Access Denied" pages), or a CAPTCHA widget (reCAPTCHA, hCaptcha, Turnstile, Arkose, GeeTest) on a page whose main content (header, navigation and footer ignored) is almost empty and has no form of its own. A login page with a widget is reported as `login_required`. An article that mentions CAPTCHAs (even one titled "Are you a robot?"), or a contact form with a reCAPTCHA widget, is captured normally.
- **Restarts don't double-charge or duplicate:** before each charge the Actor writes a `pending` marker for that URL to the run's key-value store (`STATE`); only then does it charge. Progress is also saved when Apify migrates or aborts the run, and whenever a domain is stopped. A restarted run never charges a URL again: a URL left `pending` is treated as already charged and delivered with `charged: true` and `chargeUnconfirmed: true` (if the run stopped before the charge went through, you get that screenshot free). If its file is gone, the record says `charge_pending_interrupted` with the key. A restarted run also checks the run's dataset, so a URL that already has a record is not written again, and it does not contact a domain that an earlier attempt stopped. If the progress record cannot be saved (after 3 tries), that URL is not charged (`state_save_failed`), and if the final save fails the run ends with an error. A URL marked `charge_uncertain` is likewise treated as charged after a restart. At worst one screenshot is free; a URL is never charged twice.
- Sites that block automated browsers will show up as `blocked_or_captcha` or `rate_limited`, and **the rest of that domain's URLs in the run are skipped** (`skipped_blocked`, not charged). The actor does not try to get around blocks, and it does not retry from another IP or with another browser identity.
- A 429 with `Retry-After` up to 60 s adds that wait to the run (one retry only).
- Invalid or self-signed TLS certificates fail. There is no "ignore certificate errors" option.
- Cookie-banner hiding covers common banner selectors only. Sites with full-page consent walls will be captured as they are.
- About one page per second per site, so large batches on a single domain take longer.

### FAQ

**Do I pay for failed or skipped URLs?** No per-result charge. Only delivered files get a per-result charge; the per-run start fee still applies. Each record has `charged`/`chargedEvents`.

**Does it click "Accept" on cookie banners?** No. It never clicks anything, never sets consent cookies and never blocks ad or tracking requests. With `hideCookieBannersAndAds` on, it only hides common banner and ad-slot elements in the rendered image with CSS. Paywalls, login walls and consent walls are not removed.

**Can I pass cookies or log in?** No. The actor is for public pages only.

**Why was my URL `skipped_blocked`?** Earlier in the same run, that domain answered with a 403, a CAPTCHA, or a 429 the actor could not honour. The record's `skippedDomain.reason` and `RUN_SUMMARY.skippedDomains` say which. If you own the domain, see `overrideDomainStop`.

**Why was my URL `robots_disallowed`?** The site's robots.txt disallows that path for automated agents. If you own or control the domain, use `ignoreRobotsTxt` with an ownership attestation, for up to 10 URLs on that one domain.

**Why `ssrf_blocked`?** The URL, or a redirect or resource it loads, points at a private or internal network address. The actor only captures the public internet.

**Where are my files?** They are in the run's default key-value store. Each dataset record has a `link`.

**How much does it cost to screenshot 1,000 web pages?** In one run at the default 2 GB of memory, 1,000 delivered viewport screenshots cost $2.50 plus the $0.01 start fee, $2.51 in total. Full-page screenshots cost $8 per 1,000 plus the same $0.01 start fee per run. Each separate run adds its own start fee.

**How much does it cost to convert a URL to PDF?** Each PDF page costs $0.003, plus the $0.01 start fee per run at the default 2 GB, so a single 10-page PDF in its own run costs $0.03 plus the $0.01 start fee ($0.04).

**Can I convert raw HTML (not a URL) to PDF?** No. The Actor opens only public `http(s)` URLs you list. It does not accept HTML pasted as input.

**What's the difference between a viewport and a full-page screenshot?** A viewport screenshot is the visible window (default 1280 × 800 px, height at most 2000 px). A full-page screenshot scrolls the whole page (up to `maxFullPageHeight`, default 16,000 px) and is charged as a `full-page-screenshot` event instead.

**Can I run it on a schedule or from my own code?** Yes. Use Apify schedules, or the JavaScript, Python, CLI or MCP examples in the API tab.

### More tools from Luxe Tasks

Took a PNG or JPEG screenshot and need the text in the image? OCR Text Extractor reads PNG and JPEG files.

| Tool | What it does |
|---|---|
| [Tech Stack Detector](https://apify.com/luxe_tasks/tech-stack-detector) | Finds the CMS, JavaScript frameworks, analytics, CDN and web server a public website uses, with evidence. |
| [OCR Text Extractor](https://apify.com/luxe_tasks/ocr-text-extractor) | Extracts English text from scanned PDFs, PNG and JPEG images with Tesseract OCR that runs inside the Actor. |
| [HTTP API Request Runner](https://apify.com/luxe_tasks/http-runner) | Calls APIs you are authorized to use, with your own key kept in an encrypted secret input field. |

Each tool is priced per result plus a per-run start fee; see that tool's Pricing section.

Developer: Luxe_tasks.

# Changelog

This Actor's version history is a separate document: https://apify.com/luxe_tasks/screenshot-pdf/changelog.md

# Actor input Schema

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

Public http(s) pages to capture: a list of URLs (or {"url": ...} objects). Only these pages are opened: links are never followed. A missing scheme gets https://; whitespace is trimmed; exact duplicates are removed. A #fragment is kept and sent to the browser, so page#a and page#b are two captures (useful for hash-routed pages like /#/pricing). Private, loopback, link-local and cloud-metadata addresses are refused.

## `outputFormat` (type: `string`):

PNG or JPEG screenshot, or PDF.

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

Capture the whole scrollable page (up to the max height) instead of only the viewport. Ignored for PDF.

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

Viewport width (px).

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

Viewport height (px), at most 2000. For anything taller use Full page (charged as a full-page screenshot).

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

Page event to wait for before capturing.

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

Optional CSS selector that must become visible before capture.

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

Extra delay (ms).

## `scrollToLoadLazy` (type: `boolean`):

Scroll through the page (up to the max height) to trigger lazy-loaded images, wait for images to load, then scroll back to the top.

## `maxFullPageHeight` (type: `integer`):

Taller pages are cut at this height and marked truncated.

## `jpegQuality` (type: `integer`):

JPEG quality.

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

PDF page size.

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

PDF landscape.

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

PDF print background.

## `pdfMarginMm` (type: `integer`):

Margin on all four sides of each PDF page, in millimetres. 0 = no margin; the page's own CSS @page margin is then used if it sets one. CSS @page size is ignored: the page size comes from "PDF page size" and "PDF landscape".

## `pdfScale` (type: `number`):

Zoom of the page content in the PDF (0.1 to 2). Below 1 fits more content on each page.

## `hideCookieBannersAndAds` (type: `boolean`):

Cosmetic only: hides common cookie-banner and ad-slot elements with CSS in the captured image. Never clicks accept, never blocks requests, never removes paywalls or login walls.

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

Extra CSS selectors to hide (display:none) before capture.

## `customCss` (type: `string`):

CSS injected before capture. @import and url() are not allowed.

## `timeoutSecs` (type: `integer`):

Navigation timeout per attempt. Transient timeouts/network errors are retried once.

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

Pages rendered in parallel in one shared browser. Each site is still limited to about one page per second. The effective concurrency is reduced automatically to fit the run's memory (up to 3 viewport or 2 full-page pages in parallel at the default 2 GB; one page at a time at 1 GB).

## `ignoreRobotsTxt` (type: `boolean`):

Off by default. Allowed only for runs of up to 10 URLs on ONE domain you own or control, and only together with the attestation below. Logged in every result.

## `overrideDomainStop` (type: `boolean`):

Off by default. Normally a 403 or CAPTCHA stops the whole domain for the rest of the run, and a 429 is retried once only if Retry-After is 60 s or less. For ONE domain you own or control (runs of up to 10 URLs, with the attestation below), the domain is not stopped, but each URL still gets at most one retry. Logged in every result.

## `robotsOwnershipAttestation` (type: `string`):

Required when ignoring robots.txt or overriding the domain stop, e.g. "I own or control example.org and authorise these captures." Stored in each result record.

## Actor input object example

```json
{
  "urls": [
    "https://example.com/"
  ],
  "outputFormat": "png",
  "fullPage": false,
  "viewportWidth": 1280,
  "viewportHeight": 800,
  "waitUntil": "load",
  "delayMs": 0,
  "scrollToLoadLazy": true,
  "maxFullPageHeight": 16000,
  "jpegQuality": 80,
  "pdfFormat": "A4",
  "pdfLandscape": false,
  "pdfPrintBackground": true,
  "pdfMarginMm": 0,
  "pdfScale": 1,
  "hideCookieBannersAndAds": true,
  "timeoutSecs": 30,
  "maxConcurrency": 3,
  "ignoreRobotsTxt": false,
  "overrideDomainStop": false
}
```

# Actor output Schema

## `captures` (type: `string`):

Default dataset, one record per input URL, shown in the 'Captures' view: index, url (query string redacted), status (success or the failure reason), format (png, jpeg or pdf), bytes, key and link (public URL of the file; null when nothing was delivered), httpStatus, finalUrl, redirects, charged, chargedEvents, chargeStatus, pdfPages, truncated, imagesLoaded, warnings, robotsBlockedRequests, attempts, timingMs and error. Failed captures are not charged.

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

Default key-value store. Each delivered capture is a record named capture-<index>-<host>-<hash>.\<png|jpg|pdf> (image/png, image/jpeg or application/pdf). The store also holds INPUT, RUN_SUMMARY (counts, charged events, concurrency) and STATE (progress record used on restart).

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

RUN_SUMMARY record: totals by status, charged events, budget reached, skipped domains and the effective concurrency for the run's memory.

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

// Run the Actor and wait for it to finish
const run = await client.actor("luxe_tasks/screenshot-pdf").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "urls": ["https://example.com/"] }

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

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

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

```

## CLI example

```bash
echo '{
  "urls": [
    "https://example.com/"
  ]
}' |
apify call luxe_tasks/screenshot-pdf --silent --output-dataset

```

## MCP server setup

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

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/NxZNBjFleOeTQrFQg/builds/6X68cljfhZRyfOfEB/openapi.json
