# Website Screenshot Generator - $3.00 per 1,000 (`dami_studio/website-screenshot-generator`) Actor

Give it a list of URLs, get a rendered PNG or JPEG of each page: viewport or full page, 320-2560 px wide, 1x/2x/3x density, dark mode, mobile layout. A CSS selector takes out the cookie banner. One row per page: image link, final URL, title, status. Failed pages aren't charged.

- **URL**: https://apify.com/dami\_studio/website-screenshot-generator.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Developer tools, Automation, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 screenshot takens

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Website Screenshot Generator

Give it a list of URLs and you get back a rendered picture of each page, plus a row of data about it:
the URL it finally landed on, the page title, the HTTP status, the image dimensions, the file size
and how long the render took.

Each page is opened in a real Chromium, so what you get is the page a visitor sees, with the
JavaScript run and the lazy images pulled in. You set the viewport, whether it's the first screen or
the whole scrolled page, the pixel density, dark mode, a mobile layout, and what gets taken off the
page before the shutter fires.

### Price

$3.00 per 1,000 screenshots, plus a $0.004 start fee per run.

| Screenshots | Total |
|---|---|
| 100 | $0.304 |
| 1,000 | $3.004 |
| 10,000 | $30.004 |

One `screenshot-taken` event per picture of the page you actually asked for. Not charged: the sample
row, duplicate addresses (dropped before rendering), a page that timed out or refused, a page that
answered with an anti-bot check instead of the page, and any page skipped because the run hit the
size budget you set. Those all come back as rows with `"charged": false` so you can see what
happened.

Filter on `charged == true` and the count equals what you were billed for.

### Input

```json
{
  "urls": [
    "https://www.python.org",
    "https://developer.mozilla.org",
    "https://news.ycombinator.com"
  ],
  "viewportWidth": 1440,
  "viewportHeight": 900,
  "fullPage": false,
  "format": "jpeg",
  "quality": 80,
  "removeSelectors": ["#cookie-banner", ".cookie-consent"],
  "maxItems": 10
}
```

| Field | What it does |
|---|---|
| `urls` | Pages to photograph, one per line. A bare domain like `example.com` works. Up to 300 per run; duplicates dropped before anything renders. |
| `maxItems` | Hard stop on how many pages this run captures, whatever the list length. |
| `fullPage` | Off gives you the viewport. On scrolls the page and stitches it into one tall image, up to the height ceiling. |
| `viewportWidth` / `viewportHeight` | The window the page lays out in. 1440x900 is a laptop, 1920x1080 a desktop, 390x844 a phone. Width 320–2560, height 200–4000, and the schema rejects anything outside that. |
| `deviceScaleFactor` | 1 normal, 2 retina, 3 print. 2x is four times the file size, 3x is nine times. |
| `darkMode` | Tells the page the visitor prefers a dark scheme. Sites with a dark theme render in it; sites without look identical. |
| `mobile` | Touch device profile and a mobile user agent, so you get the responsive layout. Pair with a narrow width. |
| `format` / `quality` | JPEG at 80 is the default and right for almost everything. PNG for lossless or a transparent background. |
| `waitUntil` / `delayMs` / `navigationTimeoutMs` | How ready the page must be, how long to pause after, when to give up. Raise `delayMs` for a heavy single-page app. |
| `scrollToBottom` | Scrolls to the foot first so lazy images below the fold are actually there. Turned on automatically with `fullPage`. |
| `selector` | A CSS selector to photograph one element instead of the page — a pricing table, a chart, a hero. |
| `hideSelectors` / `removeSelectors` | Hide keeps the space, remove takes the element out of the layout. Remove is the one for cookie banners and pop-ups; hide is the one for a sticky header you want gone without the layout shifting. |
| `customCss` | Raw CSS injected just before capture, up to 4,000 characters. |
| `transparentBackground` | PNG only. |
| `locale` / `timezone` | `en-US`, `Europe/Berlin`. For sites that localise or print clocks. |
| `maxFullPageHeight` | Where a whole-page capture gets cut. Default 8,000 px, max 20,000. |
| `maxTotalImageMb` | The run stops writing images past this and says so in an uncharged row. Default 250 MB. |
| `concurrency` | Pages rendering at once. 3 is a good balance. Drop to 1 if a site objects to several requests at a time. |
| `useProxy` | Off by default, and off is usually right. Turn it on if a site answers the container with an anti-bot check instead of the page. |
| `proxyUrls` | Your own proxy servers, if you have them. Takes precedence over everything else. Metered per-gigabyte proxy groups are never used, whatever the input asks for. |

Empty input gives you one labelled sample row, free.

### Output

```json
{
  "ok": true,
  "charged": true,
  "recordType": "screenshot",
  "url": "https://example.com/",
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/r7tznlACMo9u7atq8/records/screenshot-001-example.com.jpg?signature=...",
  "screenshotKey": "screenshot-001-example.com.jpg",
  "finalUrl": "https://example.com/",
  "title": "Example Domain",
  "statusCode": 200,
  "imageWidth": 1440,
  "imageHeight": 900,
  "format": "jpeg",
  "fullPage": false,
  "darkMode": false,
  "deviceScaleFactor": 1,
  "viewportWidth": 1440,
  "viewportHeight": 900,
  "pageHeight": 900,
  "truncated": false,
  "fileSizeBytes": 17536,
  "loadTimeMs": 1220,
  "scrapedAt": "2026-08-21T16:20:52.314Z"
}
```

### Where the images live

Each picture goes into the run's key-value store. `screenshotUrl` is a direct HTTPS link you can put
in an `<img>` tag, drop in a spreadsheet or fetch from a script; `screenshotKey` is the file name.
File names are numbered by position in your list and tagged with the domain, so ten pages give you
`screenshot-001-example.com.jpg` through `screenshot-010-...` and they sort the way you sent them.

The images last as long as your account keeps the run's data. On the free plan that's a limited
window, so copy anything you want to keep into your own storage.

### Keeping file sizes sane

Pictures get big faster than people expect. A whole-page capture of a long article at 2x is many
times the size of that page's first screen at 1x, because you're multiplying the height by the
density.

The defaults already lean small. JPEG at quality 80 is the one that matters most; it's visually
clean and a fraction of the file at quality 100. Leave `deviceScaleFactor` at 1 unless somebody is
going to zoom in. `maxFullPageHeight` cuts a whole-page capture at 8,000 px, which is what stops an
infinite-scroll feed rendering until something gives way. `maxTotalImageMb` caps the run on top of
all that.

### Limits

- Pages behind a login are out of scope. The run carries no accounts and no cookies, so it sees the
  logged-out page. A paywall or sign-in screen is what you'll get a picture of.
- Some sites refuse automated browsers outright. Across a battery of 50 well-known public pages, 48
  rendered and 2 answered with an anti-bot check — and those 2 still refused through the rotating
  pool, so the pool isn't the fix for them. Your own proxy servers in `proxyUrls` are. Either way you
  get the image of the block page and an uncharged `BLOCKED` row instead of a bill for a picture of
  the wrong thing.
- Every page is stopped once it has painted rather than being allowed to load indefinitely. That's
  what keeps a batch of marketing sites from taking minutes each, but a page that fills in content
  after that point shows the state at the moment of the stop.
- A full-page capture is cut at the height ceiling. An infinite-scroll feed always gets cut; the row
  says `truncated: true` and gives you the real `pageHeight`.
- Video and audio are blocked, so an autoplaying hero video shows its poster frame. Analytics and
  session-recording scripts are blocked too — that doesn't change how a page looks, but a site that
  refuses to render until its tag manager loads is the rare exception.
- PDFs and other non-HTML addresses aren't rendered. The browser treats them as a download and you
  get an uncharged `NAV_ABORTED` row.
- Dark mode only asks the site for its dark theme. A site with no dark theme looks the same.
- Fonts, ads and personalised content vary by where a request comes from, so two runs of the same
  page a day apart won't be pixel-identical. Compare content, not hashes.
- Image dimensions cap at 16,000 px per side, and a total surface ceiling quietly lowers the
  full-page height when you combine a wide viewport with 3x density. The row reports what you
  actually got.
- 300 pages per run is the hard ceiling.

### Diagnostic codes

Uncharged rows carry `"_diagnostic": true` and an `errorCode`: `BLOCKED` (anti-bot check served
instead of the page — the image is attached anyway so you can see it), `NAV_TIMEOUT`, `DNS_FAILED`,
`CONNECTION_REFUSED`, `TLS_FAILED`, `SELECTOR_NOT_FOUND`, `IMAGE_TOO_LARGE`, `BUDGET_REACHED`,
`TIME_BUDGET`. Each names the `url` it belongs to and says what to change.

### Questions

**How do I get rid of the cookie banner?** Put its CSS selector in `removeSelectors` —
`#onetrust-banner-sdk`, `.cookie-consent`, whatever the site uses.

**Why is my full-page screenshot cut off?** It was taller than `maxFullPageHeight`, 8,000 px by
default. The row says `truncated: true`. Raise it to 20,000 if you need the rest.

**Will one broken page fail the run?** No. It becomes an uncharged diagnostic row and the run moves
on. A failed run would still bill the start fee, which would mean paying to be told something went
wrong.

**Can I schedule it?** Yes. Nothing is held between runs. Keep the `screenshotUrl` off each run and
you end up with a dated history of what the page looked like.

# Actor input Schema

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

One or more pages to photograph, one per line. A bare domain like example.com works too. Up to 300 per run, and duplicates are dropped before anything is charged.

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

Off: you get exactly the viewport, like the first screen a visitor sees. On: the page is scrolled and stitched into one tall image, down to the height ceiling below.

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

Browser window width the page is laid out at. 1440 is a typical laptop, 1920 a desktop monitor, 390 a phone. Between 320 and 2560.

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

Browser window height. This is the image height unless "Capture the whole page" is on. Between 200 and 4000.

## `deviceScaleFactor` (type: `integer`):

1 for a normal screen, 2 for a retina-quality image at twice the pixels on each axis, 3 for print. Remember that 2x is four times the file size and 3x is nine times.

## `darkMode` (type: `boolean`):

Tells the page the visitor prefers a dark colour scheme. Sites that ship a dark theme will render it; sites that do not are unchanged.

## `mobile` (type: `boolean`):

Renders as a touch phone with a mobile user agent, so you get the responsive layout. Pair it with a narrow viewport width such as 390.

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

JPEG is far smaller and right for almost everything. PNG is lossless and supports a transparent background.

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

Only applies to JPEG. 80 is visually clean at about a third of the size of 100. Lower it to fit more pages inside the run's size budget.

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

How ready the page must be before the shutter fires. "domcontentloaded" is the fast default. "load" waits for images and stylesheets. "networkidle" waits for traffic to stop, which on an ad-funded page can mean waiting for the timeout. "commit" fires almost immediately.

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

Pause after the page is ready, to let animations finish and fonts swap in. 800 ms suits most sites; raise it for a heavy single-page app.

## `navigationTimeoutMs` (type: `integer`):

Give up on a page that has not become ready in this long. The page still gets an uncharged row explaining the timeout.

## `scrollToBottom` (type: `boolean`):

Scrolls to the foot of the page first so lazy-loaded images below the fold are actually there in the picture. Turned on automatically when you capture the whole page.

## `maxFullPageHeight` (type: `integer`):

The tallest a whole-page capture may get before it is cut off. Protects you from an endless feed turning into a 60,000 pixel image. Rows tell you when a picture was cut.

## `maxTotalImageMb` (type: `integer`):

The run stops taking pictures once it has written this many megabytes and writes one uncharged row saying so. Nothing is charged for the pages it skipped.

## `maxItems` (type: `integer`):

Hard stop on how many pages this run will photograph, whatever the list length. Keep it low while you are testing - you pay per screenshot.

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

A CSS selector. Fill it in to photograph just that element - a pricing table, a hero banner, a chart - instead of the page. Left empty, the whole viewport or page is captured.

## `hideSelectors` (type: `array`):

CSS selectors to make invisible before capturing, while keeping the space they occupy. Good for a sticky header you want gone without the layout shifting.

## `removeSelectors` (type: `array`):

CSS selectors to take out of the layout entirely. This is the one for cookie banners, chat bubbles and newsletter pop-ups.

## `customCss` (type: `string`):

Raw CSS injected just before the picture is taken. Anything you can write in a stylesheet works, up to 4,000 characters.

## `transparentBackground` (type: `boolean`):

PNG only. Leaves the page background out of the image, which is what you want when capturing a single element to drop into a design.

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

The language the browser asks for, as a code such as en-US, de-DE or fr-FR. Sites that localise will render in that language.

## `timezone` (type: `string`):

An IANA time zone such as Europe/Berlin or America/New\_York, for pages that print dates or clocks.

## `concurrency` (type: `integer`):

How many pages render simultaneously. 3 is a good balance. Lower it to 1 if a site you are capturing objects to several requests at once.

## `useProxy` (type: `boolean`):

Off by default, and off is usually right: the run goes out from its own container address, which costs you nothing and works on ordinary public pages. Turn it on if a site you are capturing serves the container an anti-bot check instead of the page — every render then leaves through a different address from a large rotating pool.

## `proxyUrls` (type: `array`):

Leave this empty unless you already pay for proxy servers and want the traffic to leave through them, in the form http://user:pass@host:port. Anything listed here takes precedence over the rotating pool. This Actor never uses per-gigabyte metered proxy groups, whatever is requested.

## Actor input object example

```json
{
  "urls": [
    "https://example.com",
    "https://news.ycombinator.com"
  ],
  "viewportWidth": 1440,
  "viewportHeight": 900,
  "deviceScaleFactor": 1,
  "format": "jpeg",
  "quality": 80,
  "waitUntil": "domcontentloaded",
  "delayMs": 800,
  "navigationTimeoutMs": 25000,
  "maxFullPageHeight": 8000,
  "maxTotalImageMb": 250,
  "maxItems": 10,
  "selector": "main .pricing-table",
  "locale": "en-US",
  "timezone": "Europe/Berlin",
  "concurrency": 3,
  "useProxy": false
}
```

# Actor output Schema

## `results` (type: `string`):

Every row in the default dataset: url, screenshotUrl, finalUrl, title, statusCode, imageWidth, imageHeight, format, fullPage, fileSizeBytes, loadTimeMs. An empty, blocked or unmatched run returns a single uncharged row explaining what happened instead.

# 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",
        "https://news.ycombinator.com"
    ],
    "fullPage": false,
    "viewportWidth": 1440,
    "viewportHeight": 900,
    "deviceScaleFactor": 1,
    "darkMode": false,
    "mobile": false,
    "format": "jpeg",
    "quality": 80,
    "waitUntil": "domcontentloaded",
    "delayMs": 800,
    "navigationTimeoutMs": 25000,
    "scrollToBottom": false,
    "maxFullPageHeight": 8000,
    "maxTotalImageMb": 250,
    "maxItems": 10,
    "transparentBackground": false,
    "concurrency": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/website-screenshot-generator").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",
        "https://news.ycombinator.com",
    ],
    "fullPage": False,
    "viewportWidth": 1440,
    "viewportHeight": 900,
    "deviceScaleFactor": 1,
    "darkMode": False,
    "mobile": False,
    "format": "jpeg",
    "quality": 80,
    "waitUntil": "domcontentloaded",
    "delayMs": 800,
    "navigationTimeoutMs": 25000,
    "scrollToBottom": False,
    "maxFullPageHeight": 8000,
    "maxTotalImageMb": 250,
    "maxItems": 10,
    "transparentBackground": False,
    "concurrency": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/website-screenshot-generator").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",
    "https://news.ycombinator.com"
  ],
  "fullPage": false,
  "viewportWidth": 1440,
  "viewportHeight": 900,
  "deviceScaleFactor": 1,
  "darkMode": false,
  "mobile": false,
  "format": "jpeg",
  "quality": 80,
  "waitUntil": "domcontentloaded",
  "delayMs": 800,
  "navigationTimeoutMs": 25000,
  "scrollToBottom": false,
  "maxFullPageHeight": 8000,
  "maxTotalImageMb": 250,
  "maxItems": 10,
  "transparentBackground": false,
  "concurrency": 3
}' |
apify call dami_studio/website-screenshot-generator --silent --output-dataset

```

## MCP server setup

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

```

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/4eFLOgIenBptTtm2g/builds/xadXbTRNXMiqokUQp/openapi.json
