Website Screenshot API: URL to PNG and PDF
Pricing
Pay per event
Website Screenshot API: URL to PNG and PDF
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.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Michael Lip
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
12 hours ago
Last modified
Categories
Security
Limited permissionsShare
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).
[{"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.
{"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
- Open the Actor and click Start. The input is prefilled with two sample pages.
- After about 10 seconds, open the Output tab and click a
fileUrlto see the image. - 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.
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
from apify_client import ApifyClientclient = 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
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, 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.
{"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. selectorandfullPage: falseare 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.