# Screenshot Visual Diff (`poyraz_demir/screenshot-visual-diff`) Actor

Capture public pages or compare existing PNG/JPEG images with deterministic changed-pixel metrics and red-highlighted visual diff images.

- **URL**: https://apify.com/poyraz_demir/screenshot-visual-diff.md
- **Developed by:** [Poyraz Demir](https://apify.com/poyraz_demir) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 completed comparisons

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

## Screenshot Visual Diff

Capture public pages or compare existing PNG/JPEG images with deterministic changed-pixel metrics and red-highlighted visual diff images.

### What is Screenshot Visual Diff?

Screenshot Visual Diff captures public web pages or compares two existing images and reports exactly which pixels changed. It is designed for visual regression checks where a team needs a repeatable pass/fail decision plus an image that makes the changed region easy to inspect: for example, staging against production before a release, or today's page against a saved baseline.

The Actor accepts 1 to 10 baseline/current pairs in one run. Every pair produces a dataset record and, when both images are valid, a PNG with changed pixels highlighted in red. One invalid pair returns an error record without cancelling the remaining comparisons.

### How to use it

1. Supply each baseline and current image as base64 with a filename, or give a public HTTP/HTTPS URL to capture. The example input compares two small built-in images, so you can run it as it is.
2. Set `pixelThreshold` to ignore small channel differences, such as anti-aliasing noise.
3. Set `maxDiffRatio` to the largest share of changed pixels your release allows.
4. Run the Actor, read the dataset metrics and open the PNG stored in the key-value store under `diffKey`.

A pixel counts as changed when its largest RGB channel difference is strictly greater than `pixelThreshold`. The overall status is `fail` only when `changedPixels / totalPixels` is strictly greater than `maxDiffRatio`; equality passes. Transparent pixels are composited over white before comparison.

### Input and output example

Compare a staging page with production at the same viewport:

```json
{
  "comparisons": [
    {
      "baseline": {"url": "https://www.example.com/pricing"},
      "current": {"url": "https://staging.example.com/pricing"}
    }
  ],
  "viewportWidth": 1280,
  "viewportHeight": 720,
  "waitMs": 500,
  "pixelThreshold": 16,
  "maxDiffRatio": 0.001
}
```

A completed comparison has this output shape:

```json
{
  "comparisonId": "comparison-0002",
  "baseline": "base64:baseline.png",
  "current": "base64:changed.png",
  "width": 64,
  "height": 32,
  "changedPixels": 272,
  "totalPixels": 2048,
  "diffRatio": 0.1328125,
  "maxChannelDifference": 215,
  "diffBounds": [40, 8, 56, 25],
  "pixelThreshold": 16,
  "maxDiffRatio": 0.001,
  "status": "fail",
  "error": null,
  "diffKey": "diff-comparison-0002.png"
}
```

IDs are positional, so repeated image pairs remain independent. The Actor saves each result and its diff image before processing the next pair.

### Scope and limits

The Actor supports public HTTP/HTTPS pages on default ports and existing PNG or JPEG images. Browser traffic goes through a proxy inside the Actor that allows only public network destinations; Service Workers are blocked. When robots.txt checking is enabled, it covers document redirects too. Capture covers the viewport only. It does not support login, cookies, CAPTCHA solving or protected pages.

Images can be up to 20 MB and 4 million pixels each, and the viewport width times height must not exceed 4 million pixels. Both images in a pair must have identical dimensions; the Actor does not resize them. Base64 image content is never copied into dataset records or error messages.

Use the Actor only for pages you are allowed to access, such as your own sites. Stop if a site's terms or robots.txt rules disallow it or the site owner objects.

### Pricing

Pay-per-event pricing: $0.0015 per Actor start plus $0.004 per completed comparison, whether the result is pass or fail. Both screenshots and the diff image are included. Pairs that end with an error, such as a failed capture or mismatched dimensions, are not charged. If a run reaches your maximum cost per run, the Actor stops after the last comparison that fits.

### Troubleshooting and support

- `DIMENSION_MISMATCH`: capture both images at the same width and height.
- `TOO_MANY_PIXELS`: reduce image dimensions within the 4 million pixel limit.
- `IMAGE_TOO_LARGE`: reduce file size or adjust `maxBytes` within the documented limit.
- `INVALID_BASE64`: supply raw base64 bytes without a data-URL prefix.
- `UNSUPPORTED_IMAGE_FORMAT`: convert the source to PNG or JPEG.
- `CAPTURE_FAILED`, `CAPTURE_TIMEOUT` or `ROBOTS_DISALLOWED`: check that the page is public, loads within `timeoutSecs` and allows automated access.

For bug reports, use Actor Issues in Apify Console. Include the run ID and the returned error code. Do not post private URLs, credentials or personal data.

# Actor input Schema

## `comparisons` (type: `array`):

One to ten baseline/current pairs. Each image contains base64 data and a filename, or a public HTTP/HTTPS URL.

## `maxBytes` (type: `integer`):

Limit applied to each base64 image and browser screenshot.

## `maxPixels` (type: `integer`):

Reject an image whose width times height exceeds this limit before pixel loading.

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

Shared budget for navigation, optional wait and viewport screenshot.

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

Browser viewport width in pixels; width times height must not exceed 4 million.

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

Browser viewport height in pixels; width times height must not exceed 4 million.

## `waitMs` (type: `integer`):

Optional delay before taking the screenshot, within the total capture deadline.

## `respectRobotsTxt` (type: `boolean`):

Check robots rules for each document navigation, including redirects.

## `pixelThreshold` (type: `integer`):

A pixel changes when the largest RGB channel difference is strictly greater than this value.

## `maxDiffRatio` (type: `number`):

FAIL when changedPixels / totalPixels is strictly greater than this value.

## Actor input object example

```json
{
  "comparisons": [
    {
      "baseline": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
        "filename": "baseline.png"
      },
      "current": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
        "filename": "unchanged.png"
      }
    },
    {
      "baseline": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
        "filename": "baseline.png"
      },
      "current": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAe0lEQVR4nO3YsQmAMBBGYSNZwQmsbhwndZyrnMAhzlZBkETlcfB/XSSBPK5RS0QMmY30Bd5SAK2eF5Mtred3X7+7TI/0E1AArT5v+cdm1rR/dr99nn4CCqApgKYAmgJoCqClD7i8zOFfJx3ST0ABtKIfWzAF0BRASx9wAKFiDrBQ8XxcAAAAAElFTkSuQmCC",
        "filename": "changed.png"
      }
    }
  ],
  "maxBytes": 20000000,
  "maxPixels": 4000000,
  "timeoutSecs": 30,
  "viewportWidth": 1280,
  "viewportHeight": 720,
  "waitMs": 0,
  "respectRobotsTxt": true,
  "pixelThreshold": 16,
  "maxDiffRatio": 0.001
}
```

# Actor output Schema

## `results` (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 = {
    "comparisons": [
        {
            "baseline": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
                "filename": "baseline.png"
            },
            "current": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
                "filename": "unchanged.png"
            }
        },
        {
            "baseline": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
                "filename": "baseline.png"
            },
            "current": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAe0lEQVR4nO3YsQmAMBBGYSNZwQmsbhwndZyrnMAhzlZBkETlcfB/XSSBPK5RS0QMmY30Bd5SAK2eF5Mtred3X7+7TI/0E1AArT5v+cdm1rR/dr99nn4CCqApgKYAmgJoCqClD7i8zOFfJx3ST0ABtKIfWzAF0BRASx9wAKFiDrBQ8XxcAAAAAElFTkSuQmCC",
                "filename": "changed.png"
            }
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("poyraz_demir/screenshot-visual-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 = { "comparisons": [
        {
            "baseline": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
                "filename": "baseline.png",
            },
            "current": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
                "filename": "unchanged.png",
            },
        },
        {
            "baseline": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
                "filename": "baseline.png",
            },
            "current": {
                "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAe0lEQVR4nO3YsQmAMBBGYSNZwQmsbhwndZyrnMAhzlZBkETlcfB/XSSBPK5RS0QMmY30Bd5SAK2eF5Mtred3X7+7TI/0E1AArT5v+cdm1rR/dr99nn4CCqApgKYAmgJoCqClD7i8zOFfJx3ST0ABtKIfWzAF0BRASx9wAKFiDrBQ8XxcAAAAAElFTkSuQmCC",
                "filename": "changed.png",
            },
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("poyraz_demir/screenshot-visual-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 '{
  "comparisons": [
    {
      "baseline": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
        "filename": "baseline.png"
      },
      "current": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
        "filename": "unchanged.png"
      }
    },
    {
      "baseline": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAYUlEQVR4nO3XsQ3AIAwAwRBlBYZgHCZlnAzBEE6bFhLpZemvc4HklytKRByZnfQCXxlAu95DbX31/bzHf8vsSH8BA2gG0AygGUAzgGYAzQBa+oDipx5mAM0AmgE0A2jpAx7I0wk71vJJhgAAAABJRU5ErkJggg==",
        "filename": "baseline.png"
      },
      "current": {
        "base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAIAAAAt/+nTAAAAe0lEQVR4nO3YsQmAMBBGYSNZwQmsbhwndZyrnMAhzlZBkETlcfB/XSSBPK5RS0QMmY30Bd5SAK2eF5Mtred3X7+7TI/0E1AArT5v+cdm1rR/dr99nn4CCqApgKYAmgJoCqClD7i8zOFfJx3ST0ABtKIfWzAF0BRASx9wAKFiDrBQ8XxcAAAAAElFTkSuQmCC",
        "filename": "changed.png"
      }
    }
  ]
}' |
apify call poyraz_demir/screenshot-visual-diff --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,poyraz_demir/screenshot-visual-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/6W4jWn3fhDApu33xg/builds/K38mGgNfBQQHoaCEk/openapi.json
