Check the text of any page on your own schedule, compare it with the last run, and keep a full-page screenshot of every version that differs. Cookie banners and ads are removed before capture, so the diff is about the page, not the overlay.
200 renders a month free, no card. Every feature on every plan.
Try it on your URL
Live capture, no signup. You get the screenshot and the exact API call that produced it.
This is the exact API call that made the image above. Swap in your key and it runs anywhere.
200 renders a month free.
You hit the free demo limit here. The full tool can verify you are human and keep going.
Continue in the full toolChange monitors that compare raw HTML fire on every rotating ad slot, CSRF token and timestamp in the markup. After a week of false alarms nobody reads the alerts. Comparing the rendered text of the page, after scripts have run and consent pop-ups are gone, removes most of that noise.
Self-hosting the browser that renders those pages is the part that breaks. A headless Chromium on a cron box leaks memory, fails silently on sites that need a real user agent, and captures the consent dialog instead of the content. The job keeps running and the archive fills with screenshots of cookie banners.
Hosted monitoring tools solve that, and they charge per monitored page. That is the right trade for a marketing team watching ten pages. It gets expensive for a developer who wants to watch a few hundred URLs and route changes into their own systems.
SnapRender is the capture layer. The schedule and the history stay in your systems, which keeps your data where your policies say it should be.
Two approaches cover most monitoring. A text hash is cheap and ignores rendering noise, so it is the right first signal. A pixel diff catches layout and image changes the text misses. Start with text, add pixels where they matter.
import hashlib, json, os, pathlib, datetime, requests
API = "https://app.snap-render.com"
HEADERS = {"X-API-Key": os.environ["SNAPRENDER_API_KEY"]}
URLS = ["https://example.com/pricing", "https://example.com/terms"]
STATE = pathlib.Path("state.json")
ARCHIVE = pathlib.Path("captures")
state = json.loads(STATE.read_text()) if STATE.exists() else {}
ARCHIVE.mkdir(exist_ok=True)
for url in URLS:
r = requests.get(f"{API}/v1/extract", headers=HEADERS, timeout=60,
params={"url": url, "type": "text"})
r.raise_for_status()
text = r.json()["content"]
digest = hashlib.sha256(text.encode()).hexdigest()
if state.get(url) == digest:
continue # unchanged since the last run
stamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
shot = requests.get(f"{API}/v1/screenshot", headers=HEADERS, timeout=90,
params={"url": url, "format": "png", "full_page": "true"})
shot.raise_for_status()
name = ARCHIVE / f"{url.split('//')[1].replace('/', '_')}-{stamp}.png"
name.write_bytes(shot.content)
if url in state: # first run records a baseline, later runs alert
print(f"CHANGED {url} -> {name}")
state[url] = digest
STATE.write_text(json.dumps(state, indent=2))
Each URL costs one extract call per run, plus one screenshot only when the text changed. The first run records a baseline without alerting.
// npm install pngjs pixelmatch
import fs from 'node:fs';
import { PNG } from 'pngjs';
import pixelmatch from 'pixelmatch';
const url = 'https://example.com';
const params = new URLSearchParams({ url, format: 'png', width: '1280', height: '800' });
const res = await fetch(`https://app.snap-render.com/v1/screenshot?${params}`, {
headers: { 'X-API-Key': process.env.SNAPRENDER_API_KEY },
});
if (!res.ok) throw new Error(`capture failed: ${res.status}`);
const current = PNG.sync.read(Buffer.from(await res.arrayBuffer()));
if (fs.existsSync('previous.png')) {
const previous = PNG.sync.read(fs.readFileSync('previous.png'));
const { width, height } = current;
const diff = new PNG({ width, height });
const changed = pixelmatch(previous.data, current.data, diff.data, width, height, { threshold: 0.1 });
const percent = (changed / (width * height)) * 100;
console.log(`${percent.toFixed(2)}% of pixels changed`);
if (percent > 1) fs.writeFileSync('diff.png', PNG.sync.write(diff));
}
fs.writeFileSync('previous.png', PNG.sync.write(current));
Viewport captures at a fixed 1280 x 800 keep both images the same size, which pixel diffing needs; full-page heights change from run to run.
name: page-monitor
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install requests
- run: python monitor.py
env:
SNAPRENDER_API_KEY: ${{ secrets.SNAPRENDER_API_KEY }}
- name: Keep state and captures in the repo
run: |
git config user.name "page-monitor"
git config user.email "[email protected]"
git add state.json captures
git commit -m "monitor run" || echo "no changes"
git push
GitHub Actions is a free scheduler for this. Committing state.json between runs gives the next run its baseline and keeps a dated history of every change.
Every parameter is in the API reference and the OpenAPI spec. Errors come back as { "error": { "code", "message", "status" } }; a 429 with QUOTA_EXCEEDED means the monthly allowance is used.
A text check is one call per page per run, and screenshots only happen on change. Fifty pages checked daily is about 1,500 calls a month, which fits Starter at $9. Hourly checks on the same 50 pages are about 36,000 a month, which is Business territory.
About 0 renders a month
Free
A month counts as 30 days. Failed captures are refunded. Yearly billing on paid plans is ten months for twelve. All plans.
Get a key and run the first recipe today
Every script on this page works with a free key: 200 renders a month, every feature included, no card. Your first capture takes about two minutes.
Paid plans from $9 for 2,000 renders. Yearly billing is ten months for twelve. Failed captures are refunded.
Start with text. GET /v1/extract?type=text returns what a reader sees after scripts run, so hashing it ignores markup noise such as tokens and ad slots. Add a pixel diff for pages where layout, images or colors are what you care about.
Not today. SnapRender is the capture layer: your cron job, GitHub Actions workflow, cloud scheduler or n8n flow calls the API on the schedule you choose, and you keep the history where you want it.
No. Caching is off by default, so every request renders the page fresh. Batch jobs always render fresh too. Turn caching on with cache=true only for repeat requests where an older copy is acceptable.
Both are removed before capture by default (block_cookie_banners and block_ads are on). For other dynamic areas, such as a rotating testimonial, pass hide_selectors with a CSS selector and that element is hidden before the text or image is taken.
The free plan includes 200 renders a month. That covers six pages checked daily with text hashes, or about forty pages checked weekly, before you need Starter at $9 for 2,000.
200 renders a month free, no card. The API key is on the next screen.