Screenshot Diff
Compare two screenshots pixel-by-pixel and get back the percentage of changed pixels, a pixel count, and a visual diff image with changed regions highlighted in red.
There are two modes:
- URL mode — pass
url_aandurl_b; SnapSharp captures both for you and compares them. - Image mode — pass
image_aandimage_bas base64-encoded PNGs and SnapSharp compares them directly (no captures).
/v1/diffScreenshot Diff requires a Growth plan or higher. Free and Starter requests return 403 plan_required.
Parameters
Exactly one mode is required. You must send either (url_a + url_b) or (image_a + image_b). Mixing the two is not supported and returns 400 validation_error.
| Parameter | Type | Default | Description |
|---|
Image mode tip. image_a / image_b must be valid PNG bytes, base64-encoded. Both images must have the same dimensions — otherwise the API returns 400 validation_error with the mismatch message from the comparator.
Response
Returns JSON with the diff metrics and a base64-encoded visual diff image.
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"diff_percent": 12.45,
"diff_pixels": 114892,
"total_pixels": 921600,
"width": 1280,
"height": 720,
"threshold": 0.1,
"diff_image_base64": "iVBORw0KGgo..."
}| Field | Type | Description |
|---|---|---|
request_id | string | UUID of this request (also in X-Request-Id header) |
diff_percent | number | Percentage of pixels that differ (0–100) |
diff_pixels | number | Absolute count of differing pixels |
total_pixels | number | Total pixels compared (width × height) |
width | number | Width of the compared images in pixels |
height | number | Height of the compared images in pixels |
threshold | number | The threshold value used for this comparison |
diff_image_base64 | string | PNG diff image as base64 — changed pixels highlighted in red |
AI-labeled response
When you call with describe_changes: true and have a default AI provider configured, the response also includes changes, ai_template, ai_provider, and ai_model:
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"diff_percent": 12.45,
"diff_pixels": 114892,
"total_pixels": 921600,
"width": 1280,
"height": 720,
"threshold": 0.1,
"diff_image_base64": "iVBORw0KGgo...",
"changes": {
"summary": "Hero headline copy and CTA button color changed",
"regions": [
{ "area": "hero", "change": "Headline text updated" },
{ "area": "cta-primary", "change": "Background color changed from blue to green" }
]
},
"ai_template": { "id": "…", "name": "Visual Diff Labeler" },
"ai_provider": "anthropic",
"ai_model": "claude-3-5-sonnet-20241022"
}If describe_changes: true is set but no default provider is configured (or the AI call fails), the pixel-diff fields are still returned and changes is null with an ai_error string explaining what happened. The request is never failed over a flaky AI vendor.
| Field | Type | Description |
|---|---|---|
changes | object | null | Structured AI description (JSON). null if AI is disabled or failed. |
ai_error | string | Present only when changes is null and AI was requested. Human-readable hint. |
ai_template | object | { id, name } of the prompt template used |
ai_provider | enum | anthropic / openai / openrouter / custom |
ai_model | string | Model identifier from your provider config |
Examples
Compare two URLs (curl)
curl -X POST "https://api.snapsharp.dev/v1/diff" \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url_a": "https://example.com",
"url_b": "https://staging.example.com",
"width": 1440,
"height": 900,
"threshold": 0.05
}'Compare two URLs (Node.js)
const res = await fetch('https://api.snapsharp.dev/v1/diff', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_live_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
url_a: 'https://example.com',
url_b: 'https://staging.example.com',
width: 1440,
threshold: 0.05,
}),
});
const { diff_percent, diff_pixels, diff_image_base64 } = await res.json();
console.log(`Changed: ${diff_percent.toFixed(2)}% (${diff_pixels} pixels)`);
// Persist the visual diff for review
require('fs').writeFileSync('diff.png', Buffer.from(diff_image_base64, 'base64'));Compare two base64 images (curl)
Useful when you already have both PNGs on your side (e.g. stored snapshots in CI).
IMAGE_A=$(base64 -w0 before.png)
IMAGE_B=$(base64 -w0 after.png)
curl -X POST "https://api.snapsharp.dev/v1/diff" \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"image_a\": \"$IMAGE_A\",
\"image_b\": \"$IMAGE_B\",
\"threshold\": 0.1
}"Compare two base64 images (Node.js)
import { readFileSync, writeFileSync } from 'node:fs';
const imageA = readFileSync('before.png').toString('base64');
const imageB = readFileSync('after.png').toString('base64');
const res = await fetch('https://api.snapsharp.dev/v1/diff', {
method: 'POST',
headers: {
Authorization: 'Bearer sk_live_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
image_a: imageA,
image_b: imageB,
threshold: 0.1,
}),
});
const data = await res.json();
writeFileSync('diff.png', Buffer.from(data.diff_image_base64, 'base64'));
if (data.diff_percent > 1) {
console.error(`Visual regression: ${data.diff_percent.toFixed(2)}% changed`);
process.exit(1);
}AI-labeled diff (curl)
Requires a default AI provider at settings/ai (BYOK — bring your own Anthropic / OpenAI / OpenRouter key).
curl -X POST "https://api.snapsharp.dev/v1/diff" \
-H "Authorization: Bearer sk_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url_a": "https://example.com",
"url_b": "https://staging.example.com",
"describe_changes": true
}'Use cases
- Visual regression testing — compare production vs staging before each deploy
- A/B testing — quantify visual differences between variants
- Brand monitoring — detect unauthorized changes to your public pages
- QA automation — fail a CI job when
diff_percentcrosses a budget
Errors
| Status | Error | When |
|---|---|---|
| 400 | validation_error | Neither (url_a + url_b) nor (image_a + image_b) was provided, or image dimensions mismatch, or body fails Zod validation (e.g. invalid URL, out-of-range threshold) |
| 401 | unauthorized | Missing or invalid API key |
| 403 | ip_not_allowed | API key has an IP whitelist and your IP is not on it |
| 403 | plan_required | Current plan is below Growth (required_plan: "growth" in body) |
| 429 | rate_limit_exceeded | Too many requests per minute for your plan |
| 500 | internal_error | Capture or comparison failure |