Universal Screenshot API
Pricing
from $6.00 / 1,000 screenshots
Universal Screenshot API
Batch website screenshot and PDF capture with full-page-by-default rendering, dark mode, ad/tracker/cookie-banner blocking, SSRF-safe URL validation, and an explicit error taxonomy.
Pricing
from $6.00 / 1,000 screenshots
Rating
0.0
(0)
Developer
Barak Eliov
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Batch website screenshot and PDF capture. Full-page-by-default rendering, dark mode, ad/tracker/cookie-banner blocking, SSRF-safe URL validation, and an explicit error taxonomy with no charge on failure.
This is v1 (P0 feature set only, per the approved spec in SPEC.md). See "What's not in v1 yet" below.
What this Actor does
Give it a list of URLs and it captures a PNG/JPEG/WebP screenshot and/or a PDF for each one, using a real
headless Chromium browser (Crawlee's PlaywrightCrawler). Every input URL always produces at least one
dataset row per requested output type - successes and failures alike - so you never get a silently missing
result.
Input example
{"urls": ["https://apify.com", "https://example.com"],"outputs": ["screenshot"],"fullPage": true,"format": "png","colorScheme": "no-preference"}
Full P0 input fields (all have defaults; see .actor/input_schema.json for the authoritative list):
urls, outputs, format, quality, fullPage, device, viewportWidth, viewportHeight,
deviceScaleFactor, waitUntil, waitForSelector, delayMs, timeoutSecs, scrollToBottom,
colorScheme, selectorsToHide, blockAds, blockTrackers, hideCookieBanners, stealthMode,
proxyConfiguration. Plus cookies (P1, pre-approved - see below).
Output example
One row per URL per requested output type (real output from a local run against https://example.com):
{"url": "https://example.com","finalUrl": "https://example.com/","status": "success","outputType": "screenshot","device": "desktop","format": "png","width": 1280,"height": 800,"pdfPageCount": null,"fileSizeBytes": 11423,"assetUrl": "https://api.apify.com/v2/key-value-stores/.../records/screenshot-...png","mimeType": "image/png","captureTimeMs": 1083,"cacheHit": false,"waitForSelectorTimedOut": false,"fullPageHeightCapped": false,"consoleErrorCount": null,"extractedText": null,"extractedLinks": null,"extractedMetadata": null,"errorCategory": null,"errorMessage": null,"retryCount": 0,"scrapedAt": "2026-09-30T17:40:12.858Z"}
A failed URL gets "status": "error", errorCategory set to one of blocked / not_found / timeout /
render_error / invalid_url / upstream_changed, and every capture-specific field left null. Failed
rows are never charged.
Errors (never silent, never charged)
| Category | When |
|---|---|
invalid_url | Malformed URL, non-http(s) URL, or the URL resolves to a private/link-local/loopback/cloud-metadata IP address (SSRF protection, always on) |
not_found | DNS failure, connection refused, 404/410 |
blocked | 401/403/429 or a bot-challenge page |
timeout | Navigation/render exceeded timeoutSecs |
render_error | Browser crash or an unclassified render failure |
upstream_changed | Reserved for graceful-degradation cases (page structure changed) |
A waitForSelector that never appears is not an error: the page is still captured and
waitForSelectorTimedOut: true is reported (and the successful output is charged normally).
SSRF protection (always on)
Before any browser context is opened for a URL, its hostname is DNS-resolved and every resolved IP is
checked against RFC1918/link-local/loopback/CGNAT/cloud-metadata ranges (IPv4 and IPv6). A hit is rejected
as invalid_url with zero navigation attempted. This is mandatory and not a togglable input.
Known limitation: this is a pre-navigation check, not a pinned connection. A target that changes DNS or redirects to a private address after the check passed would not be caught by this v1 (a real gap shared with most screenshot tools; closing it fully needs IP-pinned egress, out of scope for v1). An earlier design also re-validated the connected IP after navigation as extra defense-in-depth, but this was removed after local testing showed it produces false positives on any network path that goes through a local/corporate HTTP proxy or VPN (common setups) - it broke normal captures on non-proxied networks too aggressively to justify keeping.
Ad/tracker blocking and cookie banners
blockAds/blockTrackers intercept requests to a small, curated list of well-known ad and tracker domains
(see src/blocklist.ts). hideCookieBanners hides a small list of well-known cookie-consent banner
selectors via CSS. Both lists are intentionally short for v1 and easy to extend - they are not a full
filter-list replacement (e.g. EasyList).
Cookies (capture your own authenticated pages)
The cookies input (P1, pre-approved) lets you supply session cookies to capture pages behind your own
login, e.g.:
"cookies": [{ "name": "session", "value": "abc123", "domain": "example.com", "path": "/" }]
Supply only credentials you own or are authorized to use. This does not scrape third-party logged-in areas; it lets you authenticate your own session before capture.
Devices
device selects a realistic viewport + pixel-density + user-agent preset: desktop, desktop_hd,
laptop, tablet, mobile, iphone_15, pixel_8, ipad, or custom (use with viewportWidth/
viewportHeight). deviceScaleFactor left at its default (1) uses the preset's own pixel density;
any other value overrides it.
Limits
- Max 100 URLs per run (internal safety cap; there is no
maxItemsinput field in v1). - Max 120s
timeoutSecs, max 60sdelayMsper URL.timeoutSecsbounds the entire per-URL pipeline (navigation,waitForSelector, auto-scroll, and the final screenshot/PDF capture) - exceeding it at any stage reportserrorCategory: "timeout"for that URL and is not charged. - Full-page height is internally capped at 20,000px, a cost/reliability safety limit (not a Chromium
limit - Chromium itself can render far taller pages). Pages taller than the cap are captured up to
20,000px and
fullPageHeightCapped: trueis set in the output row; shorter pages are unaffected and reportfullPageHeightCapped: false. - WebP output is produced by capturing PNG and converting with
sharp(Chromium's native screenshot API only supports png/jpeg). stealthModemasks common automation fingerprints; it does not defeat CAPTCHAs or paywalls.- No login/CAPTCHA solving beyond the
cookiesfeature above (your own sessions only).
Use with AI agents / MCP
Call the Actor with urls as an array (never a comma-separated string). Check status per row; only
status: "success" rows have a populated assetUrl. outputType tells you whether a row is a
screenshot or a pdf. Batch multiple URLs in one call instead of one run per URL.
FAQ
Am I charged for failed URLs or a waitForSelector timeout? No charge for failed rows. A
waitForSelectorTimedOut: true row that still successfully produced an image/PDF is charged - a
screenshot was in fact delivered.
Does this bypass CAPTCHAs or paywalls? No. stealthMode only reduces the chance of being blocked by
basic bot detection.
Can I capture a page that needs a login? Only your own pages, via the cookies input. This Actor does
not scrape third-party logged-in areas.
What's not in v1 yet (P1/P2, explicitly deferred)
devices multi-device fan-out, selectorsToBlur, elementSelector (single-element capture), customCss,
omitBackground, acceptCookieConsent, blockImages, userAgent/locale/timezone/geolocation
overrides, extractData (text/links/metadata/console extraction - the output fields exist and are always
null for forward compatibility), pdfFormat/pdfLandscape/pdfPrintBackground (PDF is currently always
A4/portrait/backgrounds-on), and a user-configurable maxConcurrency (internally fixed at 5). cookies is
the one P1 field implemented in this build (explicitly pre-approved - see SPEC.md).
Local development
npm installnpx playwright install chromium # one-time, downloads a browser to the shared Playwright cachenpm run typechecknpm testnpm run buildapify run --input-file smoke-input.json --purge