Every team that deploys more than once a week eventually ships broken content to production. Not a crashed server — the server returns 200 just fine. Broken content. A heading left over from last month's promo, a signup button that vanished after a merge conflict, a form missing half its fields. Someone notices an hour later, sometimes a day later. The analyze endpoint turns that manual spot-check into a single API call: send a URL after each deploy, get back structured JSON with every heading, button, form, image, and navigation element on the page, and run your expectations against it in three seconds flat.
import requests
def verify_page(url, checks):
resp = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"url": url, "include": list(checks.keys())},
timeout=60,
)
data = resp.json()["result"]
failures = []
for category, assertions in checks.items():
for assertion in assertions:
if not assertion(data.get(category, [])):
failures.append(assertion.__doc__ or f"Failed check on {category}")
return failures
What deploy verification actually needs to catch
Most broken deploys don't crash the server. They break content. A merge conflict leaves the wrong heading on a landing page. A CMS update publishes a draft version of the pricing section. A CSS change pushes the signup button below the fold where nobody scrolls. A third-party script injection adds an invisible iframe. These are the failures that slip past unit tests, integration tests, and health checks — because the page technically works fine.
It catches them because it reads what's actually rendered in the browser. Not what's in the database, not what the template engine outputs, but what a visitor sees after JavaScript executes, CSS applies, and the layout settles. If the H1 says the wrong thing after a deploy, the JSON will show it. If a form field disappeared, the form's field count drops. If an image broke, it's missing from the images array.
Post-deploy smoke tests without test scripts
A typical Selenium or Playwright smoke test for a landing page runs 40–60 lines: launch browser, go to URL, wait for selector, assert text, assert element exists, assert visibility, screenshot for evidence, close browser. That test breaks whenever the page structure changes — and on a marketing site, it changes often.
With the analyze endpoint, the same verification is a data comparison. You define what you expect (H1 text, number of form fields, CTA button present above y=800) and check the JSON response against those expectations. No Puppeteer instance to babysit, no fragile selectors, no test runner to wire up.
# Post-deploy smoke test for a SaaS landing page
failures = verify_page("https://yoursite.com", {
"headings": [
lambda h: any(x["level"] == 1 for x in h),
# ^ "Page must have at least one H1"
lambda h: "pricing" in " ".join(x["text"].lower() for x in h),
# ^ "Pricing section heading must exist"
],
"interactive": [
lambda els: any(
e["tag"] == "button" and e["rect"][1] + e["rect"][3] <= 800
for e in els
),
# ^ "At least one button must be above the fold"
],
"forms": [
lambda f: any(x["fields"] >= 1 for x in f),
# ^ "Signup form must have at least one field"
],
"images": [
lambda imgs: len(imgs) >= 3,
# ^ "Page must have at least 3 visible images"
],
})
if failures:
print(f"DEPLOY FAILED VERIFICATION: {len(failures)} issues")
for f in failures:
print(f" ✗ {f}")
else:
print("✓ All checks passed")
This runs in 3–4 seconds per page. Fast enough to fit inside a CI/CD pipeline as a post-deploy gate. If any check fails, the pipeline stops and someone looks at it before traffic hits the broken page.
Verifying after CMS changes
Content management systems introduce a specific kind of risk. A copywriter edits a page in WordPress or Contentful, hits publish, and something unexpected happens: the hero image gets replaced with a thumbnail, a heading level changes from H1 to H2, a CTA button disappears because the block editor dropped it during a copy-paste. The CMS doesn't validate visual output — it stores structured content and trusts the frontend to render it correctly.
Run an analysis after every CMS publish event (via webhook) and you catch these problems instantly. The JSON response tells you exactly what changed: heading text, image count, form field count, navigation link count. No human needs to open the page and eyeball it.
// Webhook handler: verify page after CMS publish
app.post("/webhooks/cms-publish", async (req, res) => {
const { url, expectedH1, minImages } = req.body;
const analysis = await fetch("https://api.screenshotrun.com/v1/analyze", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SR_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url,
include: ["headings", "images", "interactive"],
}),
}).then(r => r.json());
const issues = [];
const h1 = analysis.result.headings.find(h => h.level === 1);
if (!h1) {
issues.push("Missing H1 heading");
} else if (expectedH1 && !h1.text.includes(expectedH1)) {
issues.push(`H1 mismatch: expected "${expectedH1}", got "${h1.text}"`);
}
if (analysis.result.images.length < (minImages || 1)) {
issues.push(`Only ${analysis.result.images.length} images (expected ${minImages}+)`);
}
if (issues.length > 0) {
await sendSlackAlert(`Page verification failed for ${url}:\n${issues.join("\n")}`);
}
res.json({ verified: issues.length === 0, issues });
});
E-commerce product page checks
Product pages have a predictable structure: title, price, images, add-to-cart button, description. When any of these elements break or vanish, it directly costs revenue. A missing add-to-cart button on a high-traffic product page for even ten minutes during peak hours is real money lost.
Verify the structural contract of a product page without hardcoding CSS selectors that'll break at the next frontend update:
def verify_product_page(url):
data = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"url": url,
"include": ["headings", "interactive", "images"],
"scope": "main",
},
timeout=60,
).json()["result"]
issues = []
# Product title (H1) exists
h1s = [h for h in data["headings"] if h["level"] == 1]
if not h1s:
issues.append("No product title (H1)")
# Add-to-cart button present
cart_btn = [
e for e in data["interactive"]
if e["tag"] == "button" and any(
kw in (e.get("text", "") + e.get("label", "")).lower()
for kw in ["add to cart", "buy now", "add to bag"]
)
]
if not cart_btn:
issues.append("No add-to-cart button found")
# At least 2 product images
if data["stats"]["images"] < 2:
issues.append(f"Only {data['stats']['images']} product images")
return issues
Run this across your top 50 product URLs nightly, or after every deploy. Spot the broken pages before customers do.
Multi-page verification at scale
One page check is useful. Checking 200 pages after a deploy is where this approach really pays off. A full Selenium suite for 200 pages takes minutes to run (browser launches, navigation, waits). The analyze endpoint processes each page in 2–4 seconds, and you can parallelize requests.
from concurrent.futures import ThreadPoolExecutor
critical_pages = [
{"url": "https://yoursite.com/", "min_headings": 3, "min_buttons": 2},
{"url": "https://yoursite.com/pricing", "min_headings": 4, "has_form": True},
{"url": "https://yoursite.com/signup", "has_form": True, "min_fields": 3},
{"url": "https://yoursite.com/docs", "min_headings": 5},
# ... 200 pages
]
def check_page(spec):
data = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"url": spec["url"], "include": ["headings", "interactive", "forms"]},
timeout=60,
).json()["result"]
issues = []
if data["stats"]["headings"] < spec.get("min_headings", 1):
issues.append(f"Expected {spec['min_headings']}+ headings, got {data['stats']['headings']}")
if spec.get("has_form") and data["stats"]["forms"] == 0:
issues.append("Expected a form, found none")
if spec.get("min_fields"):
total_fields = sum(f["fields"] for f in data["forms"])
if total_fields < spec["min_fields"]:
issues.append(f"Expected {spec['min_fields']}+ form fields, got {total_fields}")
return {"url": spec["url"], "issues": issues}
with ThreadPoolExecutor(max_workers=10) as pool:
results = list(pool.map(check_page, critical_pages))
broken = [r for r in results if r["issues"]]
print(f"\n{len(broken)}/{len(critical_pages)} pages failed verification")
for r in broken:
print(f"\n {r['url']}:")
for issue in r["issues"]:
print(f" ✗ {issue}")
200 pages checked in under two minutes with 10 concurrent workers. Each result is a structured pass/fail with specific failure reasons — not a screenshot you have to squint at.
Combining structural checks with visual proof
Sometimes the JSON says everything looks right but the page is visually broken — overlapping elements, wrong colors, a CSS regression that doesn't change the DOM structure. For these cases, pair the analyze call with a screenshot capture. The with_screenshot parameter grabs a PNG alongside the JSON in the same request.
Store the screenshot as evidence. When a check fails, attach the screenshot to the alert so the person investigating can see exactly what the page looked like at the moment of failure. For compliance and legal requirements, certified screenshots provide tamper-proof visual records with cryptographic hashes.
Geo-specific verification
A page might render correctly from your datacenter in Virginia but show different content — or break entirely — when accessed from Germany, Japan, or Brazil. CDN caching, geo-targeted content, regional A/B tests, and GDPR consent flows all vary by location. Use the geolocation parameter to verify pages from multiple regions:
regions = [
{"lat": 37.77, "lng": -122.42, "label": "San Francisco"},
{"lat": 51.51, "lng": -0.13, "label": "London"},
{"lat": 35.68, "lng": 139.69, "label": "Tokyo"},
]
for region in regions:
data = requests.post(
"https://api.screenshotrun.com/v1/analyze",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"url": "https://yoursite.com/pricing",
"include": ["headings", "interactive", "forms"],
"geolocation": {"latitude": region["lat"], "longitude": region["lng"]},
},
timeout=60,
).json()["result"]
print(f"\n{region['label']}:")
print(f" Headings: {data['stats']['headings']}")
print(f" Buttons: {len([e for e in data['interactive'] if e['tag'] == 'button'])}")
print(f" Forms: {data['stats']['forms']}")
If the heading count or form structure differs between regions, something is wrong with your localization or CDN configuration. Catch it before users in that region report it.
Scheduling recurring checks
One-off verification after deploys handles the obvious failure mode. But content degrades between deploys too: third-party scripts break, CDN caches serve stale content, CMS editors accidentally unpublish pages, SSL certificates expire and browsers inject warning interstitials. Set up scheduled checks that run your verification suite hourly or daily. When a check that passed yesterday fails today, something changed — and you find out from your monitoring, not from a customer complaint.
Fitting into CI/CD pipelines
Since it's a REST API, the analyze endpoint slots into any pipeline that can make HTTP requests. GitHub Actions, GitLab CI, Jenkins, CircleCI — all of them support POST requests natively or through a curl step. Add a verification stage after your deploy step. If the checks fail, mark the pipeline as failed and trigger a rollback or at least a Slack notification.
# GitHub Actions: post-deploy verification
- name: Verify production pages
run: |
python scripts/verify_pages.py \
--url https://yoursite.com \
--checks config/page-checks.json
env:
SCREENSHOTRUN_KEY: ${{ secrets.SCREENSHOTRUN_KEY }}
Keep your check definitions in a JSON or YAML file that lives in the repo alongside the code. When a developer adds a new page or changes a heading, they update the check spec in the same PR. The verification stays in sync with the codebase.
What this replaces (and what it doesn't)
Page content verification through the analyze endpoint replaces a specific slice of QA work: the manual spot-check. The "open the page, scroll through it, make sure nothing obvious is broken" routine that someone does after every deploy. It's not a replacement for full end-to-end testing — it won't test user flows, form submissions, or JavaScript business logic. But it catches the class of bugs that end-to-end tests usually don't cover: wrong content, missing elements, broken images, shifted layouts.
Uptime monitors tell you the server responds. End-to-end tests tell you the application logic works. Content verification tells you the page looks right. All three layers matter, and this fills the gap between "the server is up" and "the full test suite passes."
Why ScreenshotRun for page verification
ScreenshotRun runs a real Chromium browser for every request. JavaScript executes, CSS renders, lazy-loaded images appear, single-page applications hydrate. The JSON you get back reflects what a visitor actually sees — not what the server sent as raw HTML. That makes the verification results trustworthy for React, Vue, Next.js, and any other framework that renders client-side.
You get six element categories out of the box — interactive elements, forms, headings, images, text blocks, and navigation — each with pixel coordinates. No custom selectors to write, no extraction logic to build. The scope parameter narrows the scan to a specific container when you need it. Stealth mode handles sites with bot detection. Geolocation lets you verify from any region. And with_screenshot captures visual proof alongside the structured data in a single request.
The free tier gives you twenty verification calls per month — enough to test the workflow on your most critical pages. When you're ready to cover your full site after every deploy, paid plans scale to thousands of checks per day. Start in the Playground with any URL, see the JSON, and decide if this fits your pipeline.
FAQ#
Send a URL to the analyze endpoint after each deploy or CMS publish. The API renders the page in a real Chromium browser, scans the DOM for headings, buttons, forms, images, text blocks, and navigation elements, and returns structured JSON. Compare the JSON against your expected values — heading text, element counts, button positions — to detect broken content automatically.
Each page takes 2 to 4 seconds to analyze. With 10 concurrent workers you can verify 200 pages in under two minutes. Fast enough to run as a post-deploy gate in any CI/CD pipeline without blocking releases for long.
Yes. The geolocation parameter lets you specify latitude and longitude for each request. Verify that your pricing page, consent flows, and localized content render correctly from San Francisco, London, Tokyo, or any other region. Differences in heading count or form structure between regions flag CDN or localization issues.
Yes. The API runs a full Chromium browser that executes JavaScript before scanning the DOM. React, Vue, Angular, Next.js, and Svelte applications render completely. The JSON reflects what a visitor actually sees, not the raw HTML the server sends. Enable stealth mode for sites that detect headless browsers.
Wrong headings after a merge conflict, missing form fields after a CMS update, disappeared call-to-action buttons, broken images, shifted layouts that push buttons below the fold, and navigation link count changes. These are the failures that pass health checks and unit tests because the server returns 200 but the content is wrong.
Add a verification step after your deploy step. The analyze endpoint is a REST API so it works with GitHub Actions, GitLab CI, Jenkins, CircleCI, or any tool that can make HTTP requests. Define your checks in a JSON or YAML config file in your repo. If checks fail, mark the pipeline as failed and send a notification.