Visual QA tools compare screenshots pixel by pixel. That works until your page has a live price, a user avatar, a timestamp, or an ad slot. Then every test run flags differences that aren't bugs, and someone has to click through a dashboard of false positives before the deploy can proceed. Percy teams review 50 to 100 flagged diffs per sprint. Applitools reduces that with AI-based perceptual diffing, but the problem is structural: pixel comparison treats any visual change as a potential defect, whether it matters or not.
The Page Analysis API takes a different approach. Instead of asking "do the pixels match?", it asks "are the right elements on the page?" Send a URL to /v1/analyze, get back structured JSON listing every heading, button, form, image, link, and navigation element. Each one includes its text content, attributes, and bounding rectangle in pixels. Run your assertions against that JSON. No baseline images to store, no pixel noise to filter.
curl -X POST https://api.screenshotrun.com/v1/analyze \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yoursite.com/pricing",
"include": ["headings", "interactive", "forms", "images"]
}'
What structural QA actually checks
A pixel-diff tool tells you something changed. It doesn't tell you what, or whether it matters. But structural QA tells you facts: the H1 says "Enterprise Plan", there are four pricing cards, the "Start Free Trial" button sits at coordinates [920, 340] and links to /register, all product images have alt text, the contact form has six fields.
Those are verifiable assertions. You write them once, and they hold across browsers, across devices, across deployments. A font rendering difference between Chrome and Firefox? Irrelevant. An anti-aliasing shift on a Retina display? Doesn't exist in the JSON. The structure of the page is the same regardless of how the GPU draws it.
import requests
def analyze_page(url, categories):
resp = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"url": url, "include": categories},
timeout=60,
)
return resp.json()["result"]
# Visual QA checks for a pricing page
result = analyze_page("https://yoursite.com/pricing", [
"headings", "interactive", "forms", "images"
])
# Verify page structure
h1_texts = [h["text"] for h in result["headings"] if h["level"] == 1]
assert "Pricing" in h1_texts[0], f"H1 should contain 'Pricing', got: {h1_texts}"
# Verify CTA exists above the fold
ctas = [e for e in result["interactive"]
if e["tag"] == "a" and "trial" in e.get("text", "").lower()]
assert ctas, "No trial CTA found on pricing page"
assert ctas[0]["rect"][1] < 800, f"CTA below fold at y={ctas[0]['rect'][1]}"
# Verify all images have alt text
images_without_alt = [img for img in result["images"] if not img.get("alt")]
assert not images_without_alt, f"{len(images_without_alt)} images missing alt text"
That script runs in about three seconds. It verifies four things that would take a human reviewer a minute or more to check manually, and it never throws a false positive because the page loaded half a second slower today.
Why does pixel-diff break down?
The limitations of screenshot comparison are well-documented at this point. Percy, Applitools, Chromatic, BackstopJS all face the same core problem: the comparison is visual, so any visual change triggers a review. Teams end up building elaborate masking systems to exclude dynamic content from comparisons.
Timestamps, stock counters, personalized recommendations, ad placements, user avatars, live chat widgets. Each one needs a mask or a threshold adjustment. Playwright's toHaveScreenshot() offers mask and maxDiffPixels parameters. Applitools has "ignore regions." Percy has "snapshot stabilization." Yet every mask is a piece of the page you've decided not to test. And masks need maintenance: when the layout shifts, the mask coordinates break, and the test either fails or silently ignores a real bug.
Structural QA sidesteps this entirely. Does the pricing page have the right number of plan cards? Is the "Enterprise" heading present? Does the form have six fields? Those assertions don't care what the ad banner shows or whether the stock counter says 12 or 14.
Cross-browser and cross-device without the noise
Run the same URL through /v1/analyze with "device": "desktop", then "device": "mobile". You get two sets of JSON describing what's on the page at each viewport. Compare them programmatically: does the mobile version still have the navigation? Are forms accessible on small screens? Did the CTA button move below the fold?
# Compare desktop vs mobile structure
desktop = analyze_page("https://yoursite.com", ["interactive", "headings"])
mobile = analyze_page("https://yoursite.com", ["interactive", "headings"],
device="mobile")
# Check that mobile has all the same headings
desktop_headings = {h["text"] for h in desktop["headings"]}
mobile_headings = {h["text"] for h in mobile["headings"]}
missing = desktop_headings - mobile_headings
if missing:
print(f"Headings missing on mobile: {missing}")
With pixel-diff, running the same page across two viewports and two browsers gives you four images to compare. If the page has any dynamic content, that's four sets of false positives. So the browser rendering engine stops mattering. The structural approach compares facts, not pixels.
CI/CD integration
Visual QA with pixel-diff tools typically requires a test runner (Cypress, Playwright, Storybook), a browser instance, a baseline image repository, and a review dashboard. The infrastructure overhead is real: BackstopJS alone needs Docker for consistent rendering, a JSON config per scenario, and reference images committed to your repo (which bloats it fast).
Or you skip all of that and make an HTTP call. Your CI pipeline sends a POST request after deployment, runs assertions against the JSON response, and either passes or fails the pipeline. No Chromium to install, no baseline images to version, no review dashboard where someone has to click "approve" on fifty green boxes before the merge goes through.
# GitHub Actions — post-deploy visual QA
- name: Visual QA Check
run: |
RESULT=$(curl -s -X POST https://api.screenshotrun.com/v1/analyze \
-H "Authorization: Bearer ${{ secrets.SR_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{
"url": "https://staging.yoursite.com",
"include": ["headings", "interactive", "forms"]
}')
# Check H1 exists
H1_COUNT=$(echo $RESULT | jq '[.result.headings[] | select(.level == 1)] | length')
if [ "$H1_COUNT" -eq 0 ]; then
echo "FAIL: No H1 found on page"
exit 1
fi
# Check signup form exists with expected fields
FORM_FIELDS=$(echo $RESULT | jq '.result.forms[0].fields // 0')
if [ "$FORM_FIELDS" -lt 3 ]; then
echo "FAIL: Signup form has $FORM_FIELDS fields (expected 3+)"
exit 1
fi
echo "Visual QA passed"
That runs in under five seconds and catches the three most common deploy failures: missing headings, broken forms, and absent CTAs. Add more checks as you discover what breaks on your site. And the assertions are just JSON queries, not Playwright selectors that break when someone renames a CSS class.
Accessibility checks built into the same call
Automated accessibility tools (axe-core, Lighthouse, Pa11y) catch about 30-40% of WCAG violations. They're good at finding missing ARIA labels and color contrast issues. They're bad at detecting images without meaningful alt text, buttons without visible labels, or navigation links that just say "click here."
The analyze response already includes the data you need for these checks. Every image comes back with its alt attribute (or lack of one). Every interactive element includes its visible text and ARIA role. Every link has its href and anchor text. You don't need a separate accessibility scanner for the structural checks.
# Accessibility assertions from the same analyze response
result = analyze_page("https://yoursite.com", ["images", "interactive"])
# Check all images have alt text
for img in result["images"]:
if not img.get("alt"):
print(f"Missing alt: {img['src']}")
# Check buttons have visible text
for el in result["interactive"]:
if el["tag"] == "button" and not el.get("text", "").strip():
print(f"Empty button at {el['rect']}")
# Check links aren't just "click here"
vague_links = [e for e in result["interactive"]
if e["tag"] == "a" and e.get("text", "").lower() in
("click here", "read more", "learn more", "here")]
if vague_links:
print(f"{len(vague_links)} links with vague anchor text")
Do you still need screenshots?
Structural QA doesn't replace visual regression testing, and we wouldn't claim otherwise. It replaces the parts of visual regression that generate noise. A CSS change that turns your brand color from blue to orange won't show up in the JSON. A layout collapse where elements overlap won't register as a structural change (the elements are still there, just stacked wrong).
For those cases, combine both. The with_screenshot parameter on the analyze endpoint gives you a screenshot alongside the JSON in the same request. Use the structural data for automated assertions, use the screenshot for human review or visual regression when needed. Two requests' worth of data, one API call.
# Get structural data AND a screenshot in one call
result = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"url": "https://yoursite.com",
"include": ["headings", "interactive", "images"],
"with_screenshot": True,
},
).json()
# Automated structural QA
run_assertions(result["result"])
# Screenshot URL for human review or archival
screenshot_url = result["meta"]["screenshot_url"]
Monitoring production pages on a schedule
Visual QA isn't just a CI/CD gate. Run it against production on a schedule, and you catch problems that don't originate from your deploys: third-party script injections, CDN failures that break images, CMS editors who accidentally publish draft content, ad networks that inject unwanted elements.
Set up a cron job or a webhook that analyzes your critical pages every hour. If the navigation drops below a minimum link count, if forms lose fields, if new iframes appear that you didn't add, fire an alert. The defacement monitoring use case covers this in depth, but the mechanics are the same: periodic structural snapshots compared against expectations.
How this compares to Percy, Applitools, and BackstopJS
Percy and Applitools are visual regression platforms. They capture screenshots, diff them against baselines, and surface changes for review. They're good at what they do, but they solve a different problem: "did anything change?" ScreenshotRun's analyze endpoint solves "is everything correct?"
That distinction matters in practice. Percy charges by screenshot count (5,000 free, paid plans from $599/month). Each browser and viewport multiplies your usage. Applitools charges by "visual checkpoint" ($667/month for 1,000 checkpoints at the Starter tier). Every cross-browser combination burns a checkpoint. With structural QA, one API call per URL gives you the full element map. No per-browser multiplier, no snapshot quotas that grow with your test matrix.
BackstopJS is free (open source), but it's effectively in maintenance mode. No major release since September 2024. Pixel-by-pixel comparison only, no AI diffing, no browser cloud. Reference images bloat your repo. Docker is required for consistent rendering, and even then, font differences between CI environments cause false positives. Teams often spend more time maintaining BackstopJS configs than writing new tests.
Chromatic is tight with Storybook, which makes it great for component libraries but limited for full-page QA. You can't point it at a production URL and ask "is this page correct?" It tests components in isolation, not assembled pages.
Where we fit: not a replacement for visual regression testing when you need pixel-accurate comparisons. We're the layer that handles everything else: content verification, element presence, accessibility attributes, layout structure, form validation. The stuff that generates false positives in pixel-diff tools and gets masked out. Instead of masking it, test it properly with structured data.
Tune the analysis with these parameters
include— array of categories to analyze:headings,interactive,forms,images,text,navigation. Request only what you need to keep response times low.visible_only— whentrue(the default), only returns elements visible in the viewport. Set tofalsefor full-page analysis including below-fold content.scope— CSS selector to limit analysis to a specific container. Useful for testing just the header, just the pricing section, or just a form.with_screenshot— capture a screenshot alongside the analysis for visual reference or archival.device—desktop,mobile, ortablet. Run the same assertions across viewports.block_cookies,block_ads,block_chats— remove noise elements before analysis so your assertions don't break on cookie banners or chat widgets.delay— seconds to wait after page load. Set to 2-3 for pages with animations or delayed rendering.stealth— bypass bot detection for sites that block headless browsers.
The full parameter list matches the Page Analysis documentation. Everything available for screenshots and analysis works here too: custom CSS/JS injection, proxy support, geolocation, timezone, cookies for authenticated pages.
Why run visual QA through ScreenshotRun
Percy and Applitools are visual regression platforms that cost $599-667/month at the lowest paid tiers. They solve one problem well: "did the pixels change between builds?" But they require SDK integration, test suite setup, baseline management, and a review workflow. For teams that need full pixel-diff regression across browser matrices, those tools earn their price.
Most teams don't need that. They need to know that the deploy didn't break the signup form, that the pricing page still has four plan cards, that the mobile nav didn't vanish. That's what we built the analyze endpoint for. One POST request, structured JSON back, assertions you write in whatever language your CI pipeline already uses. We handle the Chromium instance, the page rendering, the JavaScript execution, the element extraction. You get data you can act on in three seconds, not a dashboard full of image diffs to triage.
And because page analysis, screenshots, markdown extraction, and PDF export all run on the same rendered page, you're not stitching together four services. One API key, one set of parameters. The Playground lets you test any URL before you write a single line of code, and twenty free requests per month is enough to validate whether the output works for your specific pages.