Website change monitoring API — get notified when any page changes
A surprising share of business logic is just “tell me when this page changes.” A competitor’s pricing page. A government tender listing. A “back in stock” section. An API status page. The naive version — reload the page in a loop and diff the HTML — breaks fast: layouts shift, timestamps and A/B tests change, ads rotate, and suddenly everything looks “changed”.
This post shows what a change-monitoring API needs to get right, and how to build it in one request with OtterSnap’s /v1/watch.
The three hard parts nobody mentions
1. The noise problem. Byte-comparing HTML is useless — every load differs. The fix is hashing a normalized reading of the page: render it in a real browser, strip scripts and styles, and hash the extracted text (title + Markdown body). Pixel-identical rendering isn’t the goal; meaningful change detection is.
2. The delivery problem. Polling means someone has to receive the result. Webhooks are the right answer (your server gets a POST), but they must be verifiable — otherwise anyone who learns the URL can fake a “price changed” alert. Sign the payload with HMAC-SHA256 and include the signature in a header.
3. The farming problem. Re-rendering pages on a schedule means running a headless-browser fleet: memory spikes, crashed tabs, slow sites. If you’re not in the rendering business, you don’t want this machine.
One request instead of all that
OtterSnap renders the page, normalizes it, hashes it, and watches it — you just pick the interval and the delivery channel:
curl -X POST https://api.ottersnap.com/v1/watch \
-H "Authorization: Bearer otter_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"interval_minutes": 60,
"mode": "text",
"webhook_url": "https://yourapp.com/hooks/ottersnap"
}'The first (baseline) check runs within a minute. After that, every time the content changes you get a POST like:
X-OtterSnap-Signature: sha256=7f3a91d0…
X-OtterSnap-Event: change
{
"event": "change",
"job_id": "b1e07c2e-…",
"url": "https://example.com/pricing",
"mode": "text",
"detected_at": "2026-09-10T03:12:44.181Z",
"previous_hash": "9d1c4ab2…",
"new_hash": "7f3a91d0…"
}Verifying it is three lines of code:
// use the webhook_secret returned when you created the job
const raw = await req.text(); // raw body, BEFORE JSON.parse
const expected = "sha256=" +
crypto.createHmac("sha256", secret).update(raw).digest("hex");
if (req.headers["x-ottersnap-signature"] !== expected) return res.status(401).end();Modes and selectors
- text (default) — hashes the page’s title and Markdown body. Robust to pixel noise, great for articles, pricing pages, listings.
- selector — pair text mode with a CSS selector like
.priceto watch one element only. Rename a heading elsewhere on the page? No alert. - screenshot — hashes the PNG bytes and attaches a screenshot of the new state to the webhook. Best for dashboards and visually-regulated pages.
No webhook receiver? Pass notify_email instead and OtterSnap emails you on every change.
Plans and limits
Free accounts run 2 monitors checking every 60 minutes; paid plans scale to 50 monitors at 15-minute checks. Every check costs 1 render credit (100/month free). A monitor that fails five times in a row pauses itself and emails you — no runaway alerting.
Try it
Grab a free key at ottersnap.com/#getkey, or play with the API first in the interactive playground. Full parameter reference lives in the docs.