1. The basic API call
SnapRender takes a URL and returns a screenshot. No browser installation, no Puppeteer, no Playwright. A single curl command:
# Take a screenshot #E8A0BF">with a single #E8A0BF">curl command
#E8A0BF">curl -X POST #A8D4A0">"https://api.snaprender.dev/v1/screenshot" \
-H #A8D4A0">"x-api-key: $SNAPRENDER_API_KEY" \
-H #A8D4A0">"Content-Type: application/json" \
-d #A8D4A0">'{
#A8D4A0">"url": #A8D4A0">"https://your-preview-url.vercel.app",
#A8D4A0">"format": #A8D4A0">"png",
#A8D4A0">"viewport": { #A8D4A0">"width": 1280, #A8D4A0">"height": 720 },
#A8D4A0">"full_page": true
}' \
--output screenshot.pngThis works in any CI environment that has curl. No Node.js, no Chrome, no 500MB Docker images.
2. GitHub Actions workflow
Here is a complete GitHub Actions workflow that captures desktop and mobile screenshots on every pull request:
#E8A0BF">name: Visual Regression Check
#E8A0BF">on:
#E8A0BF">pull_request:
branches: [main]
#E8A0BF">jobs:
screenshots:
runs-#E8A0BF">on: ubuntu-latest
#E8A0BF">steps:
- #E8A0BF">uses: actions/checkout@v4
- #E8A0BF">name: Wait for preview deploy
#E8A0BF">uses: patrickedqvist/wait-for-vercel-preview@v1.3.2
id: preview
#E8A0BF">with:
token: ${{ secrets.GITHUB_TOKEN }}
- #E8A0BF">name: Capture screenshots
#E8A0BF">env:
SNAPRENDER_KEY: ${{ secrets.SNAPRENDER_API_KEY }}
PREVIEW_URL: ${{ #E8A0BF">steps.preview.outputs.url }}
#E8A0BF">run: |
#E8A0BF">mkdir -p screenshots
# Homepage
#E8A0BF">curl -s -X POST #A8D4A0">"https://api.snaprender.dev/v1/screenshot" \
-H #A8D4A0">"x-api-key: $SNAPRENDER_KEY" \
-H #A8D4A0">"Content-Type: application/json" \
-d #A8D4A0">"{
\"url\#A8D4A0">": \"$PREVIEW_URL\#A8D4A0">",
\"format\#A8D4A0">": \"png\#A8D4A0">",
\"viewport\#A8D4A0">": { \"width\#A8D4A0">": 1280, \"height\#A8D4A0">": 720 },
\"full_page\#A8D4A0">": true
}" --output screenshots/home-desktop.png
# Mobile viewport
#E8A0BF">curl -s -X POST #A8D4A0">"https://api.snaprender.dev/v1/screenshot" \
-H #A8D4A0">"x-api-key: $SNAPRENDER_KEY" \
-H #A8D4A0">"Content-Type: application/json" \
-d #A8D4A0">"{
\"url\#A8D4A0">": \"$PREVIEW_URL\#A8D4A0">",
\"format\#A8D4A0">": \"png\#A8D4A0">",
\"viewport\#A8D4A0">": { \"width\#A8D4A0">": 375, \"height\#A8D4A0">": 812 },
\"full_page\#A8D4A0">": true
}" --output screenshots/home-mobile.png
#E8A0BF">echo #A8D4A0">"Screenshots captured successfully"
- #E8A0BF">name: Upload #E8A0BF">artifacts
#E8A0BF">uses: actions/upload-artifact@v4
#E8A0BF">with:
#E8A0BF">name: screenshots
path: screenshots/Pro tip
Store your SnapRender API key as a repository secret (SNAPRENDER_API_KEY). Never commit API keys to your repository.
3. Multi-page screenshot script
For projects with many pages, use a Node.js script that loops through pages and viewports:
#E8A0BF">const fs = #E8A0BF">require(#A8D4A0">'fs');
#E8A0BF">const SNAPRENDER_KEY = process.#E8A0BF">env.SNAPRENDER_API_KEY;
#E8A0BF">const BASE_URL = process.#E8A0BF">env.PREVIEW_URL || #A8D4A0">'https://localhost:3000';
#E8A0BF">const pages = [
{ path: #A8D4A0">'/', #E8A0BF">name: #A8D4A0">'home' },
{ path: #A8D4A0">'/pricing', #E8A0BF">name: #A8D4A0">'pricing' },
{ path: #A8D4A0">'/docs', #E8A0BF">name: #A8D4A0">'docs' },
{ path: #A8D4A0">'/login', #E8A0BF">name: #A8D4A0">'login' },
];
#E8A0BF">const viewports = [
{ width: 1280, height: 720, label: #A8D4A0">'desktop' },
{ width: 375, height: 812, label: #A8D4A0">'mobile' },
];
#E8A0BF">async #E8A0BF">function captureScreenshots() {
fs.mkdirSync(#A8D4A0">'screenshots', { recursive: true });
for (#E8A0BF">const page of pages) {
for (#E8A0BF">const vp of viewports) {
#E8A0BF">const resp = #E8A0BF">await #E8A0BF">fetch(
#A8D4A0">'https://api.snaprender.dev/v1/screenshot',
{
method: #A8D4A0">'POST',
headers: {
#A8D4A0">'x-api-key': SNAPRENDER_KEY,
#A8D4A0">'Content-Type': #A8D4A0">'application/json',
},
body: JSON.stringify({
url: BASE_URL + page.path,
format: #A8D4A0">'png',
viewport: { width: vp.width, height: vp.height },
full_page: true,
}),
}
);
#E8A0BF">const buffer = Buffer.#E8A0BF">from(#E8A0BF">await resp.arrayBuffer());
#E8A0BF">const filename = `screenshots/${page.#E8A0BF">name}-${vp.label}.png`;
fs.writeFileSync(filename, buffer);
console.log(`Saved: ${filename}`);
}
}
console.log(#A8D4A0">'All screenshots captured');
}
captureScreenshots();4. GitLab CI integration
GitLab CI works the same way. Define a job that captures screenshots and stores them as artifacts:
screenshot-check:
#E8A0BF">stage: test
#E8A0BF">image: node:20-alpine
#E8A0BF">variables:
SNAPRENDER_KEY: $SNAPRENDER_API_KEY
#E8A0BF">script:
- apk add --no-cache #E8A0BF">curl
- #E8A0BF">mkdir -p screenshots
# Capture key pages
- |
for page in #A8D4A0">"/" #A8D4A0">"/pricing" #A8D4A0">"/docs"; do
#E8A0BF">name=$(#E8A0BF">echo $page | tr #A8D4A0">'/' #A8D4A0">'-' | sed #A8D4A0">'s/^-/home/')
#E8A0BF">curl -s -X POST #A8D4A0">"https://api.snaprender.dev/v1/screenshot" \
-H #A8D4A0">"x-api-key: $SNAPRENDER_KEY" \
-H #A8D4A0">"Content-Type: application/json" \
-d #A8D4A0">"{
\"url\#A8D4A0">": \"$CI_ENVIRONMENT_URL$page\#A8D4A0">",
\"format\#A8D4A0">": \"png\#A8D4A0">",
\"viewport\#A8D4A0">": { \"width\#A8D4A0">": 1280, \"height\#A8D4A0">": 720 },
\"full_page\#A8D4A0">": true
}" --output #A8D4A0">"screenshots/$#E8A0BF">name.png"
#E8A0BF">echo #A8D4A0">"Captured: $#E8A0BF">name"
done
#E8A0BF">artifacts:
#E8A0BF">paths:
- screenshots/
expire_in: 30 days
#E8A0BF">only:
- merge_requests5. Visual regression detection
Compare screenshots between builds using pixel-diff to catch visual regressions automatically:
#E8A0BF">const { createCanvas, loadImage } = #E8A0BF">require(#A8D4A0">'canvas');
#E8A0BF">const pixelmatch = #E8A0BF">require(#A8D4A0">'pixelmatch');
#E8A0BF">const fs = #E8A0BF">require(#A8D4A0">'fs');
#E8A0BF">async #E8A0BF">function compareScreenshots(baselinePath, currentPath) {
#E8A0BF">const baseline = #E8A0BF">await loadImage(baselinePath);
#E8A0BF">const current = #E8A0BF">await loadImage(currentPath);
#E8A0BF">const { width, height } = baseline;
#E8A0BF">const canvas = createCanvas(width, height);
#E8A0BF">const baselineData = getImageData(baseline);
#E8A0BF">const currentData = getImageData(current);
#E8A0BF">const diffData = new Uint8ClampedArray(width * height * 4);
#E8A0BF">const mismatchedPixels = pixelmatch(
baselineData, currentData, diffData,
width, height, { threshold: 0.1 }
);
#E8A0BF">const totalPixels = width * height;
#E8A0BF">const diffPercent = (mismatchedPixels / totalPixels) * 100;
console.log(`Diff: ${diffPercent.toFixed(2)}% (${mismatchedPixels} pixels)`);
#E8A0BF">if (diffPercent > 0.5) {
console.error(#A8D4A0">'Visual regression detected!');
process.exit(1);
}
console.log(#A8D4A0">'No significant visual changes detected');
}
compareScreenshots(
#A8D4A0">'screenshots/baseline/home-desktop.png',
#A8D4A0">'screenshots/current/home-desktop.png'
);Set the threshold based on your tolerance. 0.5% catches major layout shifts while ignoring anti-aliasing differences across environments.
Why SnapRender vs local browsers
| Factor | Puppeteer/Playwright | SnapRender API |
|---|---|---|
| Setup | 300-500MB Chrome install | Zero dependencies |
| CI time | +30-60s browser startup | 2-5s per screenshot |
| Docker image | Needs Chromium + deps | Any base image with curl |
| Anti-bot | Manual handling | Built-in bypass |
| Maintenance | Version pinning, crashes | Managed by SnapRender |
Catch visual bugs before production
Add automated screenshots to your pipeline in under 5 minutes. No browser installs, no Docker headaches. Just a single API call.
Get Your API Key — FreeFrequently asked questions
Screenshots catch visual regressions that unit tests miss: broken layouts, CSS conflicts, z-index stacking issues, font loading failures, and responsive breakpoint bugs. They serve as visual documentation of every deployment, making it easy to see exactly what changed.
Playwright requires installing a full browser runtime in CI, which adds 300-500MB to your Docker image and 30-60s to pipeline startup. SnapRender is a single HTTP call with no browser dependencies. It also handles anti-bot protection and renders pages exactly as real users see them.
Yes. Save screenshots as build artifacts, then use pixel-diff tools (pixelmatch, resemble.js) to compare the current build against the previous one. Set a threshold (e.g., 0.1% pixel difference) and fail the build if exceeded.
Absolutely. Most CI platforms expose the preview URL as an environment variable. Pass it to SnapRender instead of the production URL. This is the most common pattern: screenshot the preview deploy before promoting to production.
SnapRender's free tier includes 100 screenshots/month. A typical project running 5 builds/day with 3 screenshots each uses ~450/month, which fits in the Starter plan. The API call takes 2-5 seconds, adding minimal time to your pipeline.