# Website Screenshot & Visual Diff (`lintlab/screenshot-diff`) Actor

Capture public website screenshots and compare them with page or image baselines. Get pixel-level diff images and change percentages for visual regression testing, monitoring, CI, and AI agents.

- **URL**: https://apify.com/lintlab/screenshot-diff.md
- **Developed by:** [Lintlab](https://apify.com/lintlab) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 & Visual Diff

Capture public web pages and compare them with live-page or PNG/JPEG baselines. Get stored screenshots, pixel-level diff images, and a clear change percentage for visual regression QA.

### Highlights

- Full-page, viewport, or CSS-selector captures in PNG or JPEG
- Desktop, laptop, tablet, and mobile device presets
- Optional page or image baselines with configurable diff thresholds
- Structured dataset rows plus direct screenshot and diff-image URLs
- $0.004 per successful capture; $0.002 per computed diff

### Quick start

```json
{"urls":["https://example.com"],"mode":"fullPage","device":"desktop"}
```

### Use with AI agents / MCP

Call `lintlab/screenshot-diff` through the Apify API or Apify MCP server. Read the default dataset, then pass `screenshotUrl` or `diff.diffImageUrl` to the next vision-capable agent step.

### Overview

`screenshot-diff` is a lintlab Apify Actor that captures screenshots of public web pages. It can compare each capture with another public page or a public PNG/JPEG baseline and store a pixel-level diff image.

The Actor uses Playwright Chromium, honors `robots.txt` for the `lintlab-screenshot` user agent (an unreachable or 5xx `robots.txt` counts as "disallow", per RFC 9309), re-checks every redirect hop, and rejects non-HTTP URLs and private, loopback, or link-local network destinations.

### Input

```json
{
  "urls": [
    "https://example.com",
    "https://example.org"
  ],
  "mode": "fullPage",
  "device": "desktop",
  "waitUntil": "networkidle",
  "delayMs": 500,
  "hideSelectors": [".cookie-banner"],
  "format": "png",
  "baselineImageUrls": [
    "https://assets.example.net/baselines/example.png",
    "https://assets.example.net/baselines/example-org.png"
  ],
  "threshold": 0.1,
  "changedThresholdPercent": 0.5,
  "timeoutSecs": 45
}
```

`urls` is required and accepts at most 200 entries. Baselines are optional. Use either `baselineUrls` (pages captured with the same settings) or `baselineImageUrls` (public PNG/JPEG files), with one baseline per input URL in the same order.

Capture modes:

- `fullPage` captures the full document.
- `viewport` captures the configured viewport.
- `selector` captures the first element matching `selector`.

Device presets are desktop (1366×768), laptop (1440×900), tablet (768×1024), and mobile (390×844 at 3× device scale with a mobile user agent). `networkidle` is the default readiness setting and falls back to `load` if the page does not become idle before the timeout.

### Output

The default dataset contains one item for each requested URL. Screenshots and diff images are records in the run's default key-value store.

```json
{
  "url": "https://example.com",
  "finalUrl": "https://example.com/",
  "status": 200,
  "device": "desktop",
  "mode": "fullPage",
  "width": 1366,
  "height": 768,
  "bytes": 18452,
  "screenshotUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/screenshot-001-100680ad546c.png",
  "capturedAt": "2026-09-25T12:34:56.000Z",
  "diff": {
    "baseline": "https://assets.example.net/baselines/example.png",
    "diffPixels": 218,
    "diffPercent": 0.020763,
    "diffImageUrl": "https://api.apify.com/v2/key-value-stores/STORE_ID/records/diff-001-100680ad546c.png",
    "changed": false
  }
}
```

Failed or policy-blocked URLs still receive a dataset item with an `error` field and null capture fields. A path blocked by the site's robots policy reports `"error": "disallowed by robots.txt"`.

### Pricing

| Event | Price |
| --- | ---: |
| Screenshot captured and stored | $0.004 |
| Visual diff computed and stored | $0.002 |
| Start fee from us | $0.00 |

A failed capture is not charged. A diff is charged only after it is computed and its image is stored. Apify platform usage or storage charges may still apply under the platform's terms. The Actor respects a run's maximum charge limit and stops scheduling work when that limit is reached.

### Use cases

- Visual regression checks in CI
- Monitoring your own public pages for visual changes
- Giving AI agents eyes on public web interfaces

### Limits and behavior

- Public HTTP(S) pages only. DNS answers resolving to private, loopback, link-local, reserved, or other non-public IP ranges are blocked.
- `robots.txt` is fetched once per origin per run and honored for target pages, baseline pages, baseline images, and navigation redirects.
- No login, cookies input, CAPTCHA handling, proxy, or stealth/anti-detection behavior is provided.
- The Actor does not bypass access controls. Use it only on pages you are allowed to access and capture.
- PNG and JPEG baselines are supported. Differently sized images are aligned at the top-left and padded with white to the larger canvas before comparison.
- Dynamic pages can vary between runs. Use `waitForSelector`, `delayMs`, and `hideSelectors` to reduce expected noise.
- Public key-value-store record URLs use the store ID. Access remains subject to the run storage's Apify access and retention settings.

### Local development

Node.js 20 or later is required.

```sh
npm ci
npm test
npx playwright install chromium
npm run test:e2e
```

The end-to-end script writes only to `./storage`, captures `https://example.com`, and prints elapsed time plus a 2 GB memory × seconds compute estimate.

Built by **lintlab** — small, reliable data tools. AI-assisted (Claude/Codex), reviewed before release.
Support: lintlab.dev@gmail.com · https://github.com/lintlab/lintlab/issues

# Actor input Schema

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

Public HTTP(S) page URLs. Each URL produces one dataset item. Maximum 200 per run.

## `mode` (type: `string`):

Capture the full page, the configured viewport, or one CSS selector.

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

Required when capture mode is selector. The first matching element is captured.

## `device` (type: `string`):

Desktop is 1366×768, laptop 1440×900, tablet 768×1024, and mobile 390×844 at 3× scale with a mobile user agent.

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

Page readiness state. Network idle falls back to load if it does not settle before the page timeout.

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

Extra delay in milliseconds after page readiness and optional selector wait.

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

Optional CSS selector that must become visible before capture.

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

CSS selectors to hide before capture, for example cookie banners or dynamic widgets.

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

PNG is lossless. JPEG is smaller and uses the JPEG quality setting.

## `jpegQuality` (type: `integer`):

JPEG quality from 1 to 100. Used only when image format is JPEG.

## `baselineUrls` (type: `array`):

Optional public page URLs captured with the same settings. Supply exactly one per target URL, in the same order. Do not combine with baseline image URLs.

## `baselineImageUrls` (type: `array`):

Optional public PNG or JPEG URLs. Supply exactly one per target URL, in the same order. Do not combine with baseline page URLs.

## `threshold` (type: `number`):

Pixel color-difference sensitivity from 0 (strict) to 1 (lenient).

## `changedThresholdPercent` (type: `number`):

Mark a result changed when its differing-pixel percentage is greater than this value.

## `timeoutSecs` (type: `integer`):

Maximum navigation and readiness wait for each target or baseline page.

## `maxConcurrency` (type: `integer`):

Maximum pages processed at once. Keep the default unless the Actor has more memory.

## Actor input object example

```json
{
  "urls": [
    "https://example.com"
  ],
  "mode": "fullPage",
  "device": "desktop",
  "waitUntil": "networkidle",
  "delayMs": 500,
  "hideSelectors": [],
  "format": "png",
  "jpegQuality": 85,
  "threshold": 0.1,
  "changedThresholdPercent": 0.5,
  "timeoutSecs": 45,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

No description

## `images` (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("lintlab/screenshot-diff").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("lintlab/screenshot-diff").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 lintlab/screenshot-diff --silent --output-dataset

```

## MCP server setup

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

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/dsgu4h2yBvWRHvc8D/builds/r7wCrgECldlE3nZW6/openapi.json
