ShotPro — Bulk Website Screenshot & PDF Report API avatar

ShotPro — Bulk Website Screenshot & PDF Report API

Pricing

Pay per usage

Go to Apify Store
ShotPro — Bulk Website Screenshot & PDF Report API

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

Ashutosh Dadu

Maintained by Community

Actor 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

  1. Agency client reports — 40 URLs in, one PDF out, desktop and mobile side by side.
  2. QA evidence — capture a release across viewports and attach the report to the ticket.
  3. 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 failureWhat ShotPro does
Waits for networkidle, which never arrives on sites with analytics or websockets, so the whole run fails on a page that rendered fineNavigates 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 paintedCaptures it anyway and records degraded: true plus why.
Returns a blank white image and calls it successBlank 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 heroDismissed via known consent-platform handles (OneTrust, Cookiebot, HubSpot, Usercentrics, cookieconsent) and button text.
Lazy images missing from a full-page shotAuto-scrolls, waits, returns to top, then captures.
Error text arrives full of terminal escape codesAll 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 ApifyClient
client = 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: POST to https://api.apify.com/v2/acts/ashutoshdadu~shotpro-screenshot-pdf/run-sync-get-dataset-items?token=YOUR_TOKEN run-sync-get-dataset-items runs 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 300000 ms. 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 returns 200 even when individual URLs failed
    • deliberately, so one dead link never destroys a batch of 200. Each row carries ok, blank, degraded + degradedReason, and a readable error.

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). degradedReason says which. Charged, because the image is usable.
  • blank: true — the file exists but is effectively empty. Not charged. error gives the likely cause.
  • ok: false — the capture failed. Not charged. error carries 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

SymptomTry
Blank captureRaise extraWaitMs, or set waitForSelector to something you know renders.
Cookie banner still visibleSet waitForSelector to the banner, or capture with selector scoped to your content.
TimeoutsRaise timeoutSecs, or set waitUntil to load / domcontentloaded.
Slow or expensive runsblockResources: ["font","media"] and raise maxConcurrency.
Report missingCheck 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.json manifest, 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 degraded reporting, area-scaled blank detection, consent-platform selectors.
  • 0.1.0 — Initial release.