lintlab Screenshot Diff

Screenshot Diff by lintlab: capture full-page, viewport or element screenshots of public URLs and optionally compare them against a baseline page or image, returning a diff image and the percentage of pixels changed. A pay-per-event Apify Actor, used as an MCP tool through Apify's hosted MCP server; needs an Apify token.

Hosted MCP Server

npx add-mcp 'https://mcp.apify.com?tools=lintlab/screenshot-diff'

Installs into Claude Code, Codex, Cursor and more

Documentation

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

Visual regression testing for public web pages: capture screenshots and compare them with another page or a PNG/JPEG baseline. Get pixel-level diff images and change percentages for QA, monitoring, CI, and AI agents.

Pricing

from $3.20 / 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. Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 section below.

Each client library has reference documentation the quickstart does not restate: JavaScript/TypeScript (npm install apify-client) and Python (pip install apify-client).

README

Website Screenshot & Visual Regression Diff

Compare public website screenshots for visual regression testing. Give the Actor a page URL and, optionally, a second page or a PNG/JPEG baseline: it returns the captured screenshots, a pixel-level diff image and the percentage of the page that changed. Use it to check releases, or schedule it to watch a page for visual changes.

Highlights

  • Full-page, viewport, or CSS-selector captures in PNG or JPEG
  • Fixed-height and offset captures (captureHeight, topOffset) for above-the-fold shots or one band of a long page
  • Lazy-load scrolling (scrollToLoad) and sticky/fixed-header handling (hideFixedElements) for clean full-page shots
  • 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

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

Try a ready-made example: Full-page website screenshot. Open it, click Start, then copy it and swap in your own URLs.

What a diff looks like

A real run (zTkTt3VRHJyClQ4gT, 2026-09-28) on two small demo pages: the changed page raises the price, drops one feature bullet and restyles the button. Changed pixels are red; unchanged content is faded.

BaselineChanged pageDiff (1.24% of pixels changed)
Baseline screenshotChanged screenshotDiff image

Input: the changed page in urls, the original page in baselineUrls, with "mode":"viewport", "device":"laptop" and "captureHeight":560. The row reports "diffPixels": 9963, "diffPercent": 1.235491, "changed": true, and it cost $0.006 in events ($0.004 capture + $0.002 diff).

Use in CI

Call the Actor from any CI job through the Apify API: pass your preview URL in urls and the production URL in baselineUrls, then fail the job when diff.diffPercent is above your threshold. No browser on the runner and no baseline images in git.

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 main-frame redirect hop before capture or charge, and routes Chromium through a local validating forward proxy. The proxy pins each outbound socket to a validated public IP and rejects non-HTTP URLs and private, loopback, or link-local network destinations.

Input

{
  "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.

Fixed-height and offset captures (full page and viewport modes):

  • captureHeight crops the capture to a fixed height in CSS pixels. {"mode":"fullPage","captureHeight":768} gives a consistent above-the-fold shot, even on pages whose height keeps changing.
  • topOffset starts the capture that many CSS pixels from the top of the page, e.g. to skip a header or capture a band further down ({"topOffset":1200,"captureHeight":800}).
  • The capture is clamped to the rendered page height. A topOffset past the end of the page returns an error row, and no capture is charged.
  • scrollToLoad scrolls through the page (bounded: up to 50 viewport steps, 20,000 px, or timeoutSecs), returns to the top, and waits briefly for images, so lazy-loaded images and sections render in full-page shots.
  • hideFixedElements (full-page mode) turns fixed elements into absolute and sticky elements into static, so a sticky header or cookie bar appears once instead of repeating or covering content.
  • Both default to false. Web fonts are always awaited (up to 3 s) before capture.

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.

{
  "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

EventPrice
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 main-frame target and baseline navigations, baseline images, and main-frame redirects. Each redirect hop is checked before Chromium requests it. A disallowed hop on the target URL yields an error row with no capture or charge. A disallowed hop on a baseline URL is never requested either: the row keeps the target screenshot (charged as a capture) and reports diff failed: disallowed by robots.txt, with no diff charge. Subframes are not checked against robots.txt.
  • No login, cookies input, CAPTCHA handling, user-supplied proxy, or stealth/anti-detection behavior is provided. The Actor uses its own local security proxy.
  • 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, hideSelectors, scrollToLoad, and hideFixedElements to reduce expected noise.
  • screenshotUrl and diffImageUrl are signed Apify key-value-store links, so they open without an API token even if your account's storage access is restricted. They follow your run storage's retention settings.

Local development

Node.js 20 or later is required.

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.

More lintlab tools

Built by lintlab — small, reliable data tools. Tested before release. Support: open an issue on this Actor's Issues tab here on Apify.

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.

captureHeight (type: integer):

Optional fixed height in CSS pixels, e.g. 768 for an above-the-fold shot. Applies to full page and viewport modes; clamped to the page height. Leave empty for the whole page (full page) or one viewport (viewport).

topOffset (type: integer):

Start the capture this many CSS pixels from the top of the page, e.g. to skip a sticky header or capture a band further down. Applies to full page and viewport modes.

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.

scrollToLoad (type: boolean):

Scroll through the page before capture so lazy-loaded images and sections can render.

hideFixedElements (type: boolean):

For full-page captures, keep fixed and sticky elements visible once without repeating or overlapping later content.

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

{
  "urls": [
    "https://example.com"
  ],
  "mode": "fullPage",
  "topOffset": 0,
  "device": "desktop",
  "waitUntil": "networkidle",
  "delayMs": 500,
  "hideSelectors": [],
  "scrollToLoad": false,
  "hideFixedElements": false,
  "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

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

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

echo '{
  "urls": [
    "https://example.com"
  ]
}' |
apify call lintlab/screenshot-diff --silent --output-dataset

MCP server setup

{
    "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/1gPX7LNx0harNbcbW/openapi.json