# Website Screenshot Scraper – Screenshot API, JPEG, URL to PDF (`fetchsmith/website-screenshot-scraper`) Actor

Website screenshot API to capture any public URL as JPEG, PNG or PDF — device presets, element capture, open graph image sizing, url to pdf and html to pdf rendering, plus raw HTML input. Full page screenshot, proxy, and viewport capture supported. Visual diff and screenshot comparison supported.

- **URL**: https://apify.com/fetchsmith/website-screenshot-scraper.md
- **Developed by:** [Fetch Smith](https://apify.com/fetchsmith) (community)
- **Categories:** Developer tools, Marketing
- **Security:** Limited permissions
- **Stats:** 2 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 screenshot captureds

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 Scraper

Capture a screenshot or PDF of any public URL using a real Chromium browser. Device presets, element capture, Open Graph card sizing and raw-HTML rendering are built in — the free official `apify/screenshot-url` Actor covers none of these.

### What it does

- Renders a public URL (or raw HTML you provide) in headless Chromium and captures a JPEG, PNG or PDF.
- Full-page or viewport-only capture, a single CSS-selector element, or a fixed 1200x630 Open Graph card.
- Device presets (desktop, iPhone 14, Pixel 7, iPad) or a custom viewport size.
- Optional `waitForSelector` to capture only once a specific element has rendered — more precise than a fixed delay or `networkidle` for single-page apps.
- **Visual diff mode**: set `compareUrl` and each URL is captured twice (yours and the comparison target), pixel-diffed, and returned with the differing-pixel count, change percentage and a highlighted diff image — staging vs production, or a page before vs after a deploy. One charged event per comparison, not two.
- Emulate dark mode (`colorScheme`), a locale/timezone (`locale`/`timezoneId`), touch support (`hasTouch`), or a GPS location (`geolocation`) before capturing, for pages that render differently based on any of these.
- Optional proxy (`proxyConfiguration`) — Apify Proxy or your own proxy URLs, with a fresh IP drawn per URL so a long list is spread across the pool instead of hitting a rate limit from one address.
- Optional custom HTTP headers (`headers`) and cookies (`cookies`) for pages that need your own API key or an existing logged-in session to render correctly — not a login or paywall bypass.
- Pay per successful capture: a URL that fails to load (timeout, DNS error, 4xx/5xx) returns an error row and is **never charged**.
- The run start is free; you only pay for screenshots actually returned.

### Input

| Field | Type | Description |
|---|---|---|
| `urls` | array of strings | Pages to screenshot (default `["https://example.com"]`) |
| `html` | string | Render this raw HTML instead of fetching a URL (ignores `urls`, produces one row; ignored in visual diff mode) |
| `format` | string | `jpeg` (default, recommended), `png` (lossless), or `pdf` |
| `quality` | integer | JPEG quality 10-100 (default 80) |
| `fullPage` | boolean | Capture full scrollable height, not just the first screen (default true) |
| `devicePreset` | string | `custom`, `desktop`, `iphone`, `pixel`, `ipad` |
| `viewportWidth` / `viewportHeight` | integer | Used when `devicePreset` is `custom` |
| `selector` | string | CSS selector to capture a single element instead of the whole page |
| `ogCard` | boolean | Force a 1200x630 viewport capture for social-share previews |
| `waitUntil` | string | `load` (default), `domcontentloaded`, or `networkidle` |
| `delayMs` | integer | Extra wait after load before capturing (0-15000ms) |
| `waitForSelector` | string | CSS selector to wait for (up to 10s) before capturing, e.g. `.chart-loaded` |
| `compareUrl` | string | Turns on visual diff mode: pixel-compare every URL in `urls` against this one (always PNG, one charge per comparison) |
| `diffThreshold` | number | Diff sensitivity 0-1 (default 0.1). Lower is stricter; 0.02 catches antialiasing shifts, 0.3+ only large colour blocks |
| `diffRegionMinPixels` | integer | Minimum connected-blob size counted as a changed *region* in `diffRegions` (default 4, 1-10000). Blobs below it are reported as `diffSpecks` instead |
| `colorScheme` | string | `light` (default), `dark`, or `no-preference` — emulates the OS dark-mode setting for pages that use `prefers-color-scheme` |
| `hasTouch` | boolean | Override touch-support emulation (default: on for iPhone/Pixel/iPad presets, off otherwise) |
| `locale` | string | BCP 47 locale to emulate, e.g. `fr-FR` (affects `navigator.language`, number/date formatting) |
| `timezoneId` | string | IANA timezone to emulate, e.g. `America/New_York` (affects locally-rendered times/dates) |
| `geolocation` | object | GPS coordinates to emulate, e.g. `{"latitude": 48.8566, "longitude": 2.3522, "accuracy": 10}` — only affects pages that call the browser Geolocation API themselves |
| `proxyConfiguration` | object | Route the browser through Apify Proxy or your own proxy URLs (off by default). A fresh IP per URL; in compare mode both captures of a pair share one IP |
| `headers` | object | Extra HTTP headers sent with every request, e.g. `{"Authorization": "Bearer ..."}` |
| `cookies` | array | Cookies to send, e.g. `[{"name": "session", "value": "..."}]` — add `domain`/`path` to scope beyond the captured URL |

### Output

One row per URL (or one row for raw HTML):

```json
{
  "url": "https://example.com",
  "format": "jpeg",
  "success": true,
  "error": null,
  "imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/screenshot-....jpg",
  "screenshotKey": "screenshot-1760000000000-0.jpg",
  "contentType": "image/jpeg",
  "byteSize": 84213,
  "capturedAt": "2026-10-16T12:00:00.000Z"
}
```

A failed URL returns `success: false`, `error` set to the failure reason, and `imageUrl: null` — it is not counted as a charged result.

#### Visual diff mode output

With `compareUrl` set, each row carries the same fields plus the comparison:

```json
{
  "url": "https://news.ycombinator.com",
  "format": "png",
  "success": true,
  "error": null,
  "imageUrl": "https://api.apify.com/v2/key-value-stores/.../records/screenshot-1792575281654-2.png",
  "screenshotKey": "screenshot-1792575281654-2.png",
  "contentType": "image/png",
  "byteSize": 235200,
  "capturedAt": "2026-10-21T09:34:41.995Z",
  "compareUrl": "https://example.com",
  "compareImageUrl": "https://api.apify.com/v2/key-value-stores/.../records/compare-1792575281762-2.png",
  "compareScreenshotKey": "compare-1792575281762-2.png",
  "diffImageUrl": "https://api.apify.com/v2/key-value-stores/.../records/diff-1792575281915-2.png",
  "diffScreenshotKey": "diff-1792575281915-2.png",
  "diffPixels": 131704,
  "comparedPixels": 2073600,
  "diffPercent": 6.3515,
  "changed": true,
  "diffRegions": 3248,
  "diffRegionsLargestPixels": 38432,
  "diffSpecks": 643,
  "diffRegionMinPixels": 4,
  "baselineWidth": 1920,
  "baselineHeight": 1215,
  "compareWidth": 1920,
  "compareHeight": 1080,
  "sizeMismatch": true
}
```

Real row from run `kFg6A0Blp01gjYpAD` (build 0.1.36, 2026-10-30): Hacker News compared against `example.com` at the default `diffThreshold: 0.1`. The same run compared `example.com` against itself and returned `diffPixels: 0`, `diffPercent: 0`, `changed: false`, `diffRegions: 0`, `diffSpecks: 0`, `sizeMismatch: false` — an unchanged page reads as exactly zero on every field, not as rendering noise. The 3,248 regions on the HN pair are what two *unrelated* pages look like: every line of text is its own blob. Region counts are most useful on the same page over time, where a real regression is a handful of regions and noise is `diffRegions: 0` with a non-zero `diffSpecks`.

**How many regions changed, not just how many pixels.** A percentage alone cannot tell *one block moved* from *speckle everywhere*: we built both cases as images with an identical pixel count and ran them through the same diff — a single contiguous 240x160 block and 38,400 isolated one-pixel changes both read **`diffPercent: 4.0`, `diffPixels: 38400`**, but the first returns `diffRegions: 1, diffRegionsLargestPixels: 38400, diffSpecks: 0` and the second `diffRegions: 0, diffRegionsLargestPixels: 0, diffSpecks: 38400`. So `diffRegions` counts connected blobs of changed pixels (4-connected) of at least `diffRegionMinPixels` (default 4), `diffRegionsLargestPixels` is the size of the biggest one, and `diffSpecks` counts the sub-floor blobs that were ignored — which is the number you want when deciding whether a non-zero diff is a real regression or just font/antialiasing noise. A high `diffPercent` with `diffRegions: 1` is one big structural change; a high `diffPercent` with `diffRegions: 0` and a large `diffSpecks` is rendering noise. All four fields are `null` (never `0`) if the count could not be produced, so "no regions found" and "not measured" stay distinguishable.

`changed` is simply `diffPixels > 0` at your chosen `diffThreshold`. `diffPercent` is measured over `comparedPixels`, which is the overlapping top-left region of the two captures: full-page screenshots of two pages rarely have identical heights, so when the dimensions differ the Actor diffs the overlap and sets `sizeMismatch: true` (with all four dimensions in the row) rather than failing the comparison. For a like-for-like comparison of two long pages, set `fullPage: false` or a fixed `viewportWidth`/`viewportHeight` so both captures are the same size.

### Pricing

`result` — $0.002 per successful screenshot/PDF, charged only when a capture succeeds. No start fee.
Repeated URLs in one `urls` list are captured and charged once, not once per copy (see the FAQ).

**Where this sits in the market. Features and pricing verified live 2026-10-19** against each
Actor's current Store listing, not marketing copy. The free official `apify/screenshot-url`
(9,851 users as of 2026-11-05) only does full-page/viewport PNG or JPEG — no element selector, device presets,
OG-card preset, raw-HTML input or PDF. Among paid rivals with real feature overlap,
`cheapapi/website-screenshot-api` (424 users as of 2026-11-05) and `moonweil/url-screenshot-api` (294 users as of 2026-11-05) both
already never charge for a failed capture and both support full-page, element and device-preset
capture — that is table stakes in this niche now, not an edge, and we don't claim otherwise.
Where the three of us actually differ: `cheapapi/website-screenshot-api` has no raw-HTML-input
mode and no dedicated 1200x630 OG-card preset, but tiers from $0.00089/result down to
$0.00061/result on its higher plans — cheaper than our flat $0.002/result, so if plain URL-to-
image is all you need, it's the better price. `moonweil/url-screenshot-api` matches our feature
set closely (raw HTML, OG preset, device presets, PDF) but charges a flat $0.004/result, twice
ours — if you want the HTML-input or OG-preset feature set at the lower price, we're the fit.
The one feature `moonweil` had and we didn't was visual diffing; as of build 0.1.17 (2026-10-21)
`compareUrl` closes that, so the diff mode is now available at half its per-result price.
`cheapapi/website-screenshot-api` has no diff mode at all — both rivals' input schemas were read
live from their own build OpenAPI documents on 2026-10-21, not from Store marketing copy.

**Competitor audit, verified live 2026-10-22**: a full re-sweep of the Store's `screenshot` listings (21
candidates, live pricing pulled from each Actor's own `pricingInfos`) found one real new
undercutter: `onescales/website-screenshot-pro` (138 users as of 2026-11-05) tests across multiple device sizes —
real feature overlap, not a bare screenshotter — and its PAY_PER_EVENT ladder drops to
**$0.001/result at GOLD and above**, half our flat $0.002, for any buyer doing enough volume to
reach that tier. Three cheaper-looking listings were checked and ruled out as narrower, not
undercutting the same product: `room_js/url-screenshot-generator` ($0.001/result + a one-time
$0.005 start fee, 91 users as of 2026-11-05) and `viralanalyzer/screenshot-url` (flat $0.001/result, 24 users as of 2026-11-05) are
both plain full-page/viewport PNG-or-JPEG capture with no element selector, device preset,
raw-HTML input or PDF — the same gap the free official Actor has, just cheaper; and
`leadsbrary/screenshot-html-file-from-url` ($0.001/result at GOLD+, 63 users as of 2026-11-05) adds PDF and an
HTML-file export but still has no element selector, device preset or diff mode. `cheapapi` and
`moonweil`'s own prices re-verified unchanged (tiers $0.00089→$0.00061 and flat $0.004
respectively) — no drift since 2026-10-19.

**Two more rivals surfaced 2026-10-24** by Apify's rental-pricing-deprecation auto-conversion
(both were FLAT_PRICE_PER_MONTH rentals until 2026-10, invisible to every per-result price
comparison before that): `dz_omar/ultimate-screenshot` (268 users as of 2026-11-05) has real overlap — 100+
device presets and, unlike us, video (MP4) and GIF export — but its PAY_PER_EVENT ladder charges
$0.007/page-capture at its lowest tier down to $0.005 at GOLD and above, 2.5x–3.5x our flat
$0.002; it's a feature add for buyers who need video/GIF, not a price threat.
`scrapeai/screenshot-downloader` (6 users as of 2026-11-05, flat $0.00249/result) is a bare full-or-partial PNG/
JPEG capture with no element selector, device preset, raw-HTML input, PDF or diff mode — the
same narrow gap as the three already-ruled-out listings above, just newly visible. Both
re-verified live 2026-10-25: user counts and tiered pricing unchanged for each.

**Full re-sweep, verified live 2026-10-29** (16 live `screenshot` Store listings, live `pricingInfos` pulled
per Actor): four more now-visible rivals, all checked and ruled out, none a price or feature
threat. `dz_omar/screenshot` (338 users as of 2026-11-05, a second listing from the same seller as
`ultimate-screenshot` above) charges a one-time $0.05→$0.01 startup fee *plus* $0.01→$0.005 per
capture *plus* a per-30-second memory-time fee — more expensive than `ultimate-screenshot` for a
narrower feature set (cookie-header auth only, no selector/device-preset/OG-card/diff; we already
cover cookie auth via our own `cookies`/`headers` inputs). `i-scraper/website-screenshot` (325
users as of 2026-11-05) is a flat $0.006/result bare PNG/JPG capture — 3x our price with no selector, device
preset, raw-HTML input, PDF or diff. `akash9078/website-screenshot-generator` (94 users as of 2026-11-05) nets out
near $0.02/screenshot once its actor-start and per-row fees are added to its $0.01–$0.02
per-capture tier — pricier than us, same bare-capture feature set. `egra_van/website-screenshot-pro`
(1 user as of 2026-11-05, was 3 at the 2026-10-29 sweep) is the one tiered ladder cheaper than ours at its top rung ($0.0015/screenshot at
GOLD+, +$0.001/PDF) but has no element selector, raw-HTML input, OG-card preset or diff mode —
the same gap as the already-ruled-out `room_js`/`viralanalyzer`/`leadsbrary` listings, just a
newer entrant. `cheapapi`, `moonweil` and `onescales` re-verified live, pricing unchanged since
their last-dated mentions above.

**Full re-sweep, verified live 2026-11-05** (16 live `screenshot` Store listings this time, same
count as the 2026-10-29 sweep but different composition — `gochujang/image-ocr-extractor` and
`maximedupre/twitter-screenshot-generator` now rank for the term but are a different product
each (OCR-to-text, Twitter-card-specific) and are not screenshot-capture rivals, so neither is
named here). Three newly-visible rivals, all checked and ruled out, none a price or feature
threat. `fetchbase/web-screenshot-pro` (20 users) has real overlap — device presets, dark mode,
retina, authenticated pages, PDF — and no start fee, but charges a flat $0.02/screenshot, 10x our
price. `spectre_scrape/website-screenshot-api` (9 users) is the one with the closest feature
match — it has element targeting, our actual differentiator against the bare-capture rivals above
— but charges $0.009/result plus a one-time $0.005 actor-start fee, 4.5x+ our price with no
volume discount. `parseforge/screenshot-url` (84 users) renders HTML snapshots and mobile
variants but has no element selector, OG-card preset or diff mode, and its PAY_PER_EVENT ladder
only reaches $0.0112/result at DIAMOND — still 5.6x ours at the best tier anyone can reach. All
other previously-named rivals re-verified live this sweep (counts updated above); no pricing
drift beyond the user-count churn already folded into this paragraph.

### FAQ

**Why JPEG by default instead of PNG?** JPEG at quality 80 is visually indistinguishable from PNG for a normal web page screenshot and never larger, so it is the safer default. How much smaller depends on the page: measured on a long text-heavy Wikipedia article (black text on flat white, PNG's best case) the saving was only 15% — 2.41 MB to 2.04 MB. Measured on a photo-heavy page (a Wikipedia gallery of large photographs) the saving was 63% — 1.95 MB to 725 KB. Expect more savings the more photos/gradients a page has, least on flat text/UI pages. Use `format: "png"` only if you need pixel-exact/lossless output (e.g. comparing against a reference image).

**What actually makes a screenshot smaller?** Cropping, not the format. On that same article, `fullPage: false` took the JPEG from 2.04 MB to 274 KB — a 7.4x cut, versus 15% for the format switch. If you only need to show what the top of the page looked like, set `fullPage: false`. Every output row carries `byteSize` so you can measure this on your own pages.

**Can I screenshot just one element, like a pricing table?** Yes — set `selector` to any CSS selector (e.g. `#pricing-table`). This takes priority over `fullPage`.

**Does it work for pages that need JavaScript to render?** Yes, it uses a real Chromium browser. Set `waitUntil: "networkidle"` for JS-heavy single-page apps that load content after the initial page load. If the page keeps a background connection open (polling, websockets, analytics beacons) `networkidle` can time out even after your content is ready — set `waitForSelector` to a selector that only exists once the real content has rendered (e.g. a chart container or the last row of a table) instead; it's a more precise signal than a fixed delay or network state.

**What if a URL fails to load?** You get a row with `success: false` and an `error` message describing why (timeout, DNS failure, HTTP error, etc.) — the run continues to the next URL and you are not charged for the failure. You do not have to scan the dataset to notice: the run's **status message** in the Console now summarises the outcome (e.g. `9 capture(s) delivered, but: 1 of the 10 target(s) could not be captured…`), and also tells you when a run stopped early on its charge limit or run timeout, skipped duplicate URLs, or ignored an input because of another input you also set (see next question). A run with nothing to report sets no status message at all.

**What happens if I set `html` together with `urls`, `cookies`, or `compareUrl`?** Whichever one loses is silently ignored by the browser work, but you are told: the status message names it (e.g. `Ignored this run (cannot be honored together with the mode you ran): urls, cookies`) and `RUN_SUMMARY.ignoredInputs` carries the same list for a pipeline to check. `compareUrl` wins over `html` (a diff needs two fetched URLs, not one rendered string); when `html` wins instead, it ignores `urls` (one row from the HTML, not one per URL) and `cookies` (there is no real URL for the browser to scope a cookie's domain/path to — `headers` still applies, since those don't need one).

**What if the same URL appears twice in my `urls` list?** It is captured once and charged once. As of build 0.1.32 the Actor de-duplicates the list before any browser work starts and logs `Skipped N duplicate URL(s) in \`urls\` -- each unique URL is captured and charged exactly once.`Before that build, a list pasted from a spreadsheet export, a sitemap dump or two concatenated lists would capture the same page twice and bill two`result`events for a byte-identical screenshot. Matching is on the exact URL string after trimming surrounding whitespace, so`https://x.com/a`and` https://x.com/a `collapse. It deliberately does **not** fold case or trailing slashes —`/Path`vs`/path`and`/x`vs`/x/\` are treated as different URLs and both captured, because many servers genuinely serve different content at those, and silently dropping one you meant to capture would be the worse failure.

**How do I check whether a deploy changed a page?** Put the production URL in `urls` and the staging/preview URL in `compareUrl`. You get one row per pair with `diffPercent`, `diffPixels` and a diff image where every changed pixel is highlighted. For a *time*-based check (same URL, before vs after), screenshot it now, then on a later run compare the live URL against the stored baseline image's URL. Note the Actor compares two live URLs, not a URL against an uploaded image file.

**Is this diff one real change or noise everywhere?** Read `diffRegions` next to `diffPercent`. `diffRegions` is the count of connected blobs of changed pixels at least `diffRegionMinPixels` (default 4) in size, `diffRegionsLargestPixels` is the biggest blob, and `diffSpecks` is how many sub-floor blobs were dropped. Two diffs with the same percentage can be structurally opposite — a single moved card (`diffRegions: 1`, one large `diffRegionsLargestPixels`) versus antialiasing speckle spread over the whole page (`diffRegions: 0` with a large `diffSpecks`) — and the percentage cannot separate them. Raise `diffRegionMinPixels` to ignore bigger cosmetic churn, or set it to `1` to count every blob including single pixels. Added 2026-10-30 after a reader asked for exactly this on our `diffThreshold` write-up.

**Why do I get a non-zero diff on a page I did not change?** Fonts, antialiasing, lazy-loaded images, carousels, ad slots, timestamps and animations all shift between two loads of the same page. Raise `diffThreshold` (0.2-0.3) to ignore rendering noise, set `waitForSelector` so both captures happen at the same render state, and prefer `fullPage: false` for pages with infinite scroll. A sub-0.1% diff on a dynamic page is usually noise, not a regression.

**Am I charged twice in compare mode?** No. A comparison captures two screenshots and writes three images (baseline, comparison, diff) but charges a single `result` event — $0.002 per comparison row. A comparison that fails returns an error row and is not charged at all.

**How much does `diffThreshold` actually change the reading?** We measured it, not just described the direction: on the same Hacker News vs `example.com` pair, `diffThreshold: 0.02` read **99.75%** different, `0.1` (the default) read **6.22%**, and `0.3` read **4.47%**. Most of the slider's effect sits in the narrow 0.02-0.1 range — below that, near-black and near-white pixel pairs that are merely *different*, not *visibly* different, all count as changed, so two unrelated pages read as "almost entirely different" regardless of how different they actually look. Above 0.1 the reading is comparatively flat. **Mapped the interior too (2026-10-28, same pair): 0.04 reads 23.19%, 0.06 reads 22.97%, 0.08 reads 6.40% — the collapse is two separate steps with a plateau in between, not one smooth slope, so 0.04-0.06 is a trap: it looks meaningfully stricter than the 0.08-0.1 range but reads almost the same as 0.02-level noise.** A same-page control (HN vs itself) confirmed `diffPixels: 0` at both 0.02 and 0.1, ruling out capture jitter as part of any of these readings. If you're comparing two genuinely different pages, stay at 0.08 or above; use 0.02-0.06 only for same-page-over-time checks where you want to catch a single shifted element.

**Can I render my own HTML instead of fetching a URL?** Yes — set `html` to a raw HTML string (e.g. an email template or an Open Graph card you built) and leave `urls` unset; it's rendered directly with no network request.

**Can I capture a page's dark-mode or a different locale/timezone appearance?** Yes — set `colorScheme: "dark"` for pages that style themselves via `prefers-color-scheme`, or `locale`/`timezoneId` for pages that branch on either (date/number formatting, geo-targeted copy, a timestamp rendered in the visitor's local time). All three are plain browser emulation settings, verified by checking the actual rendered pixels/`navigator` values, not just passed through. An invalid `timezoneId` returns a clean error row rather than a silently-wrong capture.

**Can I screenshot through a proxy?** Yes — set `proxyConfiguration` to use Apify Proxy (pick groups/country in the input editor) or paste your own `proxyUrls`. It is **off by default**, because most public pages do not need it and a direct capture is faster. Turn it on when you are capturing a long URL list from one site and hitting per-IP rate limits, or when the page serves different content per country. The Actor draws a fresh IP per URL rather than per run, so the list is spread across the pool. One deliberate exception: in visual diff mode both captures of a pair go through the **same** IP — otherwise a geo-routed page would diff as "changed" when nothing had changed.

**Can I capture a page that needs an API key or my own logged-in session?** Yes — set `headers` for an API key or bearer token (e.g. `{"Authorization": "Bearer ..."}`), or `cookies` to send a session cookie you already have (e.g. `[{"name": "session", "value": "..."}]`). This is for pages you already have a legitimate right to access with your own credentials, like an internal dashboard, a staging environment behind basic headers, or your own account on a site — it does not log in for you or defeat any access control on content you don't already have permission to view. A malformed `headers`/`cookies` entry (missing `name`/`value`) fails the whole run with a clear message rather than a silent per-URL error.

**Can I emulate a different GPS location?** Yes — set `geolocation` to `{"latitude": ..., "longitude": ..., "accuracy": ...}`. This only changes what a page gets back if it calls the browser's own Geolocation API (`navigator.geolocation`) — most pages never do, so it has no visible effect on them. It's for geo-aware dashboards, maps, store locators and "near me" pages that branch on the visitor's coordinates. Verified end to end: a test page that calls `getCurrentPosition()` and renders its result rendered the exact coordinates supplied, not an IP-based guess. An out-of-range latitude/longitude (outside ±90/±180) fails the whole run with a clear message instead of a silently-wrong capture.

### Notes

Only public pages you have a right to view/fetch should be submitted — this Actor does not log you in or bypass a paywall/login you don't already have access to. `headers`/`cookies` let you pass credentials you already hold (an API key, your own session cookie); they do not grant access to anything you couldn't already reach yourself. Issues or feature requests: support@fetchsmith.com. Also available as a hosted API at https://fetchsmith.com

### Related guides

- https://fetchsmith.com/blog/website-screenshot-api-no-key-compared — three free, keyless screenshot endpoints measured: one 403s from a datacenter IP, one serves a 24h-stale cached image unless you ask the right way, one allows 5 requests/day
- https://fetchsmith.com/blog/screenshot-png-vs-jpeg-byte-size-measured — PNG vs JPEG vs viewport cropping on the same page, with the real byte sizes and billing lines
- https://fetchsmith.com/blog/http-only-vs-headless-browser-scraping-cost — when you need a browser at all, timed against HTTP-only extraction
- https://fetchsmith.com/blog/visual-diff-threshold-cliff-not-curve — `diffThreshold` swept 0.02/0.1/0.3 on the same pair: the reading drops from 99.75% to 6.22% in one notch, then barely moves
- https://fetchsmith.com/tools

# Actor input Schema

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

Public web pages to screenshot, e.g. https://example.com. Each becomes one charged result row (or one error row on failure, never both).

## `html` (type: `string`):

Instead of fetching a URL, render this raw HTML string directly (e.g. an email template or OG-card markup you built yourself). If set, 'URLs' is ignored and this produces exactly one result row.

## `format` (type: `string`):

JPEG is smaller than PNG for the same page with no visible quality loss at quality ~80 -- measured 15% smaller on a text-heavy page, 63% smaller on a photo-heavy one. Use PNG only if you need pixel-exact/lossless output, or PDF for a printable document.

## `quality` (type: `integer`):

Ignored for PNG and PDF.

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

Capture the entire scrollable page height, not just the visible viewport. Ignored for PDF, which is always full-document.

## `devicePreset` (type: `string`):

Emulate a common device's viewport, pixel ratio and user agent. 'Custom' uses the Viewport width/height fields below instead.

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

Browser viewport width in pixels. Only used when Device preset is 'custom'.

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

Browser viewport height in pixels. Only used when Device preset is 'custom'.

## `selector` (type: `string`):

CSS selector of a single element to capture instead of the whole page, e.g. '#pricing-table' or '.hero'. Takes priority over Full page.

## `ogCard` (type: `boolean`):

Force a 1200x630 viewport screenshot sized for social-share previews, ignoring other viewport/fullPage settings.

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

When to consider the page ready to capture.

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

Wait this long after the page load condition is met, e.g. to let an animation or lazy-loaded image settle.

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

Before capturing, wait up to 10s for this CSS selector to become visible, e.g. '.chart-loaded' or '#app-root img'. More reliable than 'Network idle' for SPAs that keep a background connection open after the content you actually want has rendered. If the selector never appears, the run returns an error row for that URL instead of a blank/partial capture.

## `compareUrl` (type: `string`):

Leave empty for normal screenshots. Set it to turn each URL above into a visual-diff/regression check: the Actor captures your URL and this one, pixel-diffs them, and returns the change percentage plus a highlighted diff image. Typical use: staging vs production, or a saved page vs its live version after a deploy. Compare mode always captures PNG (a lossy JPEG would invent differences that are not there), ignores the raw HTML input, and charges ONE event per comparison row even though it takes two captures.

## `diffThreshold` (type: `number`):

Per-pixel colour-distance tolerance from 0 to 1. Lower is stricter: 0.02 flags faint antialiasing and font-rendering shifts, the 0.1 default ignores them but catches any real visual change, 0.3+ only flags large blocks of colour. Only used when a compare URL is set.

## `diffRegionMinPixels` (type: `integer`):

How big a connected blob of changed pixels must be to count as one changed REGION in `diffRegions`. The default of 4 ignores isolated 1-3 pixel antialiasing speckle, which is what makes a raw diff percentage hard to act on: 'one 4% block moved' and '400 stray pixels all over the page' can produce the same percentage but very different region counts. Raise it to ignore larger cosmetic churn, or set it to 1 to count every blob. Blobs below the floor are reported separately as `diffSpecks`. Only used when a compare URL is set.

## `colorScheme` (type: `string`):

Emulate the browser's light/dark mode preference before capturing, so a page that reacts to `prefers-color-scheme` renders its dark variant instead of light.

## `hasTouch` (type: `boolean`):

Whether the emulated device reports touch support. Leave unset to use the device preset's default (on for iPhone/Pixel/iPad, off for desktop/custom) or override it either way.

## `locale` (type: `string`):

BCP 47 locale to emulate, e.g. 'fr-FR' or 'ja-JP'. Affects `navigator.language`, number/date formatting, and any page content that branches on locale.

## `timezoneId` (type: `string`):

IANA timezone to emulate, e.g. 'America/New_York' or 'Asia/Tokyo'. Affects any page content that renders times/dates in the visitor's local timezone.

## `proxyConfiguration` (type: `object`):

Route the browser through a proxy. Off by default (captures come straight from the Apify datacenter, which is fine for most public pages). Turn it on when a target rate-limits repeated hits from one IP across a big URL list, or serves different content per country. A fresh proxy IP is drawn per URL, so a 100-URL list is spread across the pool instead of hammering one address; in compare mode BOTH captures of a pair deliberately share one IP, so a geo/IP difference can never show up as a false visual diff. You can also paste your own proxy URLs instead of using Apify Proxy.

## `headers` (type: `object`):

Custom headers sent with every request, e.g. {"Authorization": "Bearer ...", "X-Api-Key": "..."}. For reaching your own API-key-gated page or internal tool -- not for bypassing access controls on a site you don't have permission to access.

## `cookies` (type: `array`):

Send your own session cookies so the browser loads a page the way you'd see it logged in, e.g. \[{"name": "session", "value": "..."}]. Each entry needs "name" and "value"; add "domain"/"path" to scope a cookie to a specific host, otherwise it's scoped to the URL being captured. For pages you already have a legitimate login/session for -- this does not log in or bypass a paywall on your behalf.

## `geolocation` (type: `object`):

Emulate the browser GPS location, e.g. {"latitude": 48.8566, "longitude": 2.3522, "accuracy": 10}. Only takes effect on pages that call the Geolocation API themselves (navigator.geolocation) -- most pages never do, so this is for geo-aware sites/dashboards, not a general IP-geolocation override.

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "format": "jpeg",
  "quality": 80,
  "fullPage": true,
  "devicePreset": "desktop",
  "viewportWidth": 1920,
  "viewportHeight": 1080,
  "ogCard": false,
  "waitUntil": "load",
  "delayMs": 0,
  "diffThreshold": 0.1,
  "diffRegionMinPixels": 4,
  "colorScheme": "light",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# 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("fetchsmith/website-screenshot-scraper").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("fetchsmith/website-screenshot-scraper").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 fetchsmith/website-screenshot-scraper --silent --output-dataset

```

## MCP server setup

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

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/nzq9YxI7Ek1pRZgsg/builds/Ag5CVjFniwNXThHed/openapi.json
