
Automating Visual Regression Checks in GitHub Actions with the Compare API

Snapshot Site Team
12 Apr 2026 - 02 Mins read
Most teams already know they should compare staging against production before merging a pull request. What usually stops them is not the idea, it is wiring it into CI in a way that actually blocks a bad merge instead of just producing a screenshot nobody looks at.
This is a working GitHub Actions workflow that calls the Snapshot Site v3/compare endpoint on every pull request, and fails the check when the mismatch percentage crosses a threshold you choose.
What the Workflow Does
- On every pull request, capture the production page and the preview-deployment page.
- Send both to
POST /api/v3/compare. - Read back
mismatchPercentagefrom the response. - Fail the job if the mismatch is above your threshold, so the PR shows a red check instead of a silent screenshot.
The Workflow File
name: Visual Regression Check
on:
pull_request:
branches: [main]
jobs:
visual-diff:
runs-on: ubuntu-latest
steps:
- name: Compare production vs preview
env:
SNAPSHOT_API_KEY: ${{ secrets.SNAPSHOT_API_KEY }}
PREVIEW_URL: ${{ steps.deploy.outputs.preview_url }}
run: |
RESPONSE=$(curl --silent --request POST \
--url https://api.prod.ss.snapshot-site.com/api/v3/compare \
--header 'Content-Type: application/json' \
--header "x-snapshotsiteapi-key: $SNAPSHOT_API_KEY" \
--data "{
\"before\": {\"url\": \"https://example.com/pricing\", \"width\": 1440, \"fullSize\": true, \"hideCookie\": true},
\"after\": {\"url\": \"$PREVIEW_URL/pricing\", \"width\": 1440, \"fullSize\": true, \"hideCookie\": true},
\"threshold\": 0.1
}")
echo "$RESPONSE" | jq .
MISMATCH=$(echo "$RESPONSE" | jq '.summary.mismatchPercentage')
DIFF_URL=$(echo "$RESPONSE" | jq -r '.diff.link')
echo "Mismatch: $MISMATCH% — diff image: $DIFF_URL"
if (( $(echo "$MISMATCH > 5" | bc -l) )); then
echo "::error::Visual mismatch of $MISMATCH% exceeds the 5% budget. Review $DIFF_URL"
exit 1
fi
Swap PREVIEW_URL for whatever your deploy step actually outputs — a Vercel, Netlify, or internal staging URL all work the same way, since before and after just need a reachable URL each.
Picking the Right Pages to Gate On
Running this against every route on every PR gets noisy fast. Start with the small set of pages where a silent visual regression is expensive:
- homepage
- pricing
- signup and checkout
- the specific page the pull request actually touches
You can add one curl block per page, or loop over a small JSON list of {page, path} pairs inside the same step.
Choosing a Failure Budget
The workflow above fails at 5% mismatch, but that number should not be copy-pasted blindly — a dense dashboard and a mostly-static marketing page tolerate very different amounts of pixel noise before a real regression is hiding in there. We cover how to pick and tune that number in Tuning the threshold Parameter in the Visual Diff API to Avoid False Positives.
Surfacing the Diff, Not Just the Failure
A red CI check with no context gets ignored or force-merged. Posting the diff.link image URL as a PR comment (via gh pr comment or the GitHub API in the same job) turns a build failure into a one-click visual review, which is a much faster path to "yes this is fine, merge it" or "no, revert that CSS change."
This automates the same manual habit covered in How to Compare Staging and Production Screenshots Before a Release — worth reading first if you're not yet doing this check by hand.
Wiring this once means every future pull request gets a visual safety net for free, instead of relying on someone remembering to eyeball the preview link before merging — and Snapshot Site is priced to run comfortably at PR-level frequency.

