Use this when you need to know that a page changed — a competitor’s pricing, a supplier’s terms, a regulator’s guidance — without re-reading it every day. The Request API renders the page as a real user sees it, and a content hash decides whether anything happened.
Executable starters
change-monitor.ts
import { createHash } from 'node:crypto';import { readFile, writeFile } from 'node:fs/promises';const apiKey = process.env.BROWSERCITY_API_KEY;if (!apiKey) { throw new Error('Set BROWSERCITY_API_KEY before running this script.');}const url = 'https://example.com/terms';const stateFile = './change-monitor-state.json';const response = await fetch('https://api.browser.city/v1/requests', { method: 'POST', headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url, markdown: true }),});if (!response.ok) { throw new Error(`Request failed: ${response.status} ${await response.text()}`);}const page = await response.json();const hash = createHash('sha256').update(page.content).digest('hex');const previous = await readFile(stateFile, 'utf-8') .then((raw) => JSON.parse(raw) as { hash: string }) .catch(() => null);if (previous && previous.hash !== hash) { console.log(`CHANGED: ${url}`); // Notify your channel of choice here (webhook, email, queue message).} else if (!previous) { console.log(`Baseline recorded for ${url}`);} else { console.log(`No change for ${url}`);}await writeFile(stateFile, JSON.stringify({ hash, checkedAt: new Date().toISOString() }));import hashlibimport jsonimport osimport pathlibimport urllib.errorimport urllib.requestfrom datetime import datetime, timezoneAPI_KEY = os.environ["BROWSERCITY_API_KEY"]URL = "https://example.com/terms"STATE_FILE = pathlib.Path("./change-monitor-state.json")request = urllib.request.Request( "https://api.browser.city/v1/requests", data=json.dumps({"url": URL, "markdown": True}).encode("utf-8"), headers={ "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", }, method="POST",)try: with urllib.request.urlopen(request, timeout=120) as response: page = json.loads(response.read().decode("utf-8"))except urllib.error.HTTPError as error: detail = error.read().decode("utf-8", errors="replace") raise RuntimeError(f"Request failed: {error.code} {detail}") from errordigest = hashlib.sha256(page["content"].encode("utf-8")).hexdigest()previous = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else Noneif previous and previous["hash"] != digest: print(f"CHANGED: {URL}") # Notify your channel of choice here (webhook, email, queue message).elif previous is None: print(f"Baseline recorded for {URL}")else: print(f"No change for {URL}")STATE_FILE.write_text( json.dumps({"hash": digest, "checkedAt": datetime.now(timezone.utc).isoformat()}))
Production hardening checklist
- Normalize the markdown (strip timestamps, session tokens, rotating banners) before hashing, or every check fires.
- Keep the previous content alongside the hash so alerts can include a diff.
- Run checks from a scheduler (cron, CI, task queue) and treat a failed render as its own alert.
- Track consecutive failures separately from changes — a page going down is not a content change.
Cost and plan notes
Each check is one Request API render. At hourly frequency that is ~720 renders per page per month; put that number and your page weight into /pricing-calculator to see the real monthly figure before adding pages.