ShotPro — Bulk Website Screenshot & PDF Report API
Pricing
Pay per usage
ShotPro — Bulk Website Screenshot & PDF Report API
Turn a list of URLs into full-page screenshots, PDFs and one merged PDF report. Handles cookie banners and lazy loading, keeps working when networkidle never settles, and tells you why a capture failed instead of returning a blank image. Blank and failed captures are never charged.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Ashutosh Dadu
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
11 days ago
Last modified
Categories
Share
In: a list of URLs. Out: full-page screenshots, per-URL results, and one merged PDF report you can hand to a client or attach to a ticket.
Price: free while ShotPro is in launch. Pay-per-event pricing is planned; when it arrives, blank and failed captures will never be charged.
Three things people use it for
- Agency client reports — 40 URLs in, one PDF out, desktop and mobile side by side.
- QA evidence — capture a release across viewports and attach the report to the ticket.
- SEO / audit documentation — dated visual proof of what a set of pages looked like.
Try it
Paste a few URLs into urls, tick mergedReport, press Start. Results land in the dataset; images and REPORT.pdf in the key-value store.
Why this one instead of a plain screenshot Actor
Most screenshot tools fail in the same few ways. These are the ones ShotPro was built to fix, each verified by a real run:
| Common failure | What ShotPro does |
|---|---|
Waits for networkidle, which never arrives on sites with analytics or websockets, so the whole run fails on a page that rendered fine | Navigates on domcontentloaded, then settles separately with a bounded wait. If the network never quiets you still get the capture, flagged degraded with the reason. Measured: apify.com went from hard failure to a complete 1440×9618 capture. |
| A navigation timeout throws away a page that had already painted | Captures it anyway and records degraded: true plus why. |
| Returns a blank white image and calls it success | Blank detection scaled to pixel area, plus a DOM render check. A blank capture is reported with its likely cause — and never charged. |
| Cookie banner covers the hero | Dismissed via known consent-platform handles (OneTrust, Cookiebot, HubSpot, Usercentrics, cookieconsent) and button text. |
| Lazy images missing from a full-page shot | Auto-scrolls, waits, returns to top, then captures. |
| Error text arrives full of terminal escape codes | All text is sanitised at source, so the dataset, CSV export and PDF are readable. |
What it produces
- One image per URL × viewport in the key-value store (PNG / JPEG / WebP), or a per-page PDF.
REPORT.pdf(optional) — a cover page listing every URL with its outcome and failure reason, then every successful capture, one per page.SUMMARY.json— machine-readable run summary: counts, success rate, median duration, and the file manifest.- One dataset row per capture:
url · viewport · width · height · deviceScaleFactor · ok · blank · degraded · degradedReason · httpStatus · consentDismissed · bytes · durationMs · error · key · url_public
Options
urls · viewports (many per run) · fullPage · format png/jpeg/webp · quality · deviceScaleFactor (2 = retina) · colorScheme light/dark · selector (element capture) · pdf · mergedReport + reportTitle · dismissConsent · blockResources · waitUntil · waitForSelector · extraWaitMs · timeoutSecs · maxConcurrency
Sample input
{"urls": ["https://example.com", "https://news.ycombinator.com", "https://apify.com"],"viewports": [{ "name": "desktop", "width": 1440, "height": 900 },{ "name": "mobile", "width": 390, "height": 844 }],"fullPage": true,"format": "png","mergedReport": true,"reportTitle": "Client Site Audit — September"}
Sample output (real run, 5 URLs × 2 viewports)
{"requestedUrls": 5, "viewports": 2, "capturesAttempted": 10,"captured": 8, "degraded": 2, "blank": 0, "failed": 2,"successRate": 0.8, "medianDurationMs": 2686,"format": "png", "mergedReport": "REPORT.pdf", "mergedReportError": null}
The two failures were a deliberately invalid domain; both rows carry ERR_NAME_NOT_RESOLVED as the reason. Neither was charged.
Call it from code
Apify CLI
$apify call USERNAME/shotpro-screenshot-pdf --input-file=input.json
curl
curl -X POST "https://api.apify.com/v2/acts/USERNAME~shotpro-screenshot-pdf/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"urls":["https://example.com"],"mergedReport":true}'
Python
from apify_client import ApifyClientclient = ApifyClient("<TOKEN>")run = client.actor("USERNAME/shotpro-screenshot-pdf").call(run_input={"urls": ["https://example.com", "https://news.ycombinator.com"],"viewports": [{"name": "desktop", "width": 1440, "height": 900}],"mergedReport": True,})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["url"], item["ok"], item.get("error"))
JavaScript
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<TOKEN>' });const run = await client.actor('USERNAME/shotpro-screenshot-pdf').call({urls: ['https://example.com'], mergedReport: true,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
n8n — one HTTP Request node, no community package, no Playwright to install or maintain:
- Method/URL:
POSTtohttps://api.apify.com/v2/acts/ashutoshdadu~shotpro-screenshot-pdf/run-sync-get-dataset-items?token=YOUR_TOKENrun-sync-get-dataset-itemsruns the Actor and returns the results in the same call - no polling loop, no Wait node, no webhook. - Body: the JSON above.
- Set the node timeout to
300000ms. n8n's 10-second default kills any real batch, and this is the single most common reason the pattern appears "not to work". - Branch on
ok, not on the HTTP status. A run returns200even when individual URLs failed- deliberately, so one dead link never destroys a batch of 200. Each row carries
ok,blank,degraded+degradedReason, and a readableerror.
- deliberately, so one dead link never destroys a batch of 200. Each row carries
Batch behaviour
Every URL is captured at every viewport. maxConcurrency controls parallelism (default 4). One URL failing never affects the others — each capture is isolated and reported on its own row.
Degraded and failure semantics
ok: true— a clean, non-blank capture. Charged.ok: true, degraded: true— captured, but something was imperfect (network never idled, navigation timed out after paint).degradedReasonsays which. Charged, because the image is usable.blank: true— the file exists but is effectively empty. Not charged.errorgives the likely cause.ok: false— the capture failed. Not charged.errorcarries the real reason.
Limitations — stated plainly
- Cannot capture pages behind a login, a paywall, or a bot wall. Those come back as blank or failed, with a reason.
- Consent-banner dismissal is best-effort. Unusual or custom banners may survive.
- No JavaScript execution or interaction beyond scrolling and one consent click.
- Very long pages (>40,000 px of scroll) stop auto-scrolling at that limit.
- No proxy rotation, and no attempt to defeat anti-bot systems.
- Report generation is capped by run memory; on very large batches, capture the batch in parts.
Privacy and data retention
ShotPro visits only the URLs you supply and stores only what it captured. Images, REPORT.pdf and SUMMARY.json live in your own key-value store under your Apify account, subject to your account's retention settings. Nothing is sent anywhere else, and the developer has no access to your runs or their contents.
Troubleshooting
| Symptom | Try |
|---|---|
| Blank capture | Raise extraWaitMs, or set waitForSelector to something you know renders. |
| Cookie banner still visible | Set waitForSelector to the banner, or capture with selector scoped to your content. |
| Timeouts | Raise timeoutSecs, or set waitUntil to load / domcontentloaded. |
| Slow or expensive runs | blockResources: ["font","media"] and raise maxConcurrency. |
| Report missing | Check mergedReportError in SUMMARY.json — it names the cause. |
Support
Open an issue on the Actor's Issues tab. Include the run ID; every run carries SUMMARY.json with everything needed to diagnose it.
Changelog
- 0.1.5 — Merged PDF report,
SUMMARY.jsonmanifest, blank/failed captures no longer charged, all text sanitised at source (fixes a report-generation crash on ANSI escape codes in Playwright errors), cover page sized to content. - 0.1.2 — Bounded network settling, capture-after-timeout with
degradedreporting, area-scaled blank detection, consent-platform selectors. - 0.1.0 — Initial release.