Built-in LumaBrowser extension

Watch any web page for changes — down to the element.

A real browser, on your machine, polling the URLs you care about. Pick the exact element to watch with a click. Get a webhook the second it updates, plus a line-level diff and a desktop notification. No cloud account, no per-check pricing, no rendering quirks.

Download LumaBrowser See it in action

Real browser, JS-rendered pages

Every check runs inside a Chromium tab, so React/Vue/SPA content is visible. Cloud HTML-fetch tools miss it.

Element picker, not URL guessing

Click the price, the stock badge, the inbox row. The monitor watches that selector, not the whole page.

Local-first, free to run

Monitors, history, and checksums live in a local SQLite database. No subscription, no usage caps.

Webhook in, webhook out

Every change POSTs a structured JSON payload — pipe it to Slack, Discord, n8n, Zapier, or your own service.

What is the Page Change Detector?

The Page Change Detector is a built-in LumaBrowser extension that monitors web pages for content changes. You point it at a URL, optionally click a few elements you care about, and it checks the page on a schedule. When something changes, you get a webhook POST and an optional desktop notification, with a human-readable diff of what was added and removed.

It is the open, local-first alternative to cloud services like Distill.io, Visualping, ChangeTower, Wachete, and Hexowatch — with one important advantage: checks run inside a real browser, so JavaScript-rendered pages, single-page apps, and authenticated dashboards work without contortions.

What people watch
  • Price & stock alerts — pick the price element on a product page and get a webhook the moment it drops or comes back in stock.
  • Competitor pricing pages — watch the pricing tier rows. Forward to a Slack channel for go-to-market visibility.
  • Documentation & release notes — know the second a vendor updates their changelog, deprecates an API, or ships a new version.
  • Job boards & listings — monitor a search results page for new postings, properties, tickets, or auction items.
  • Status pages & incident banners — pick the status badge selector and pipe state changes to your alerting stack.
  • Internal dashboards — with no-refresh mode, watch a metric tile on a dashboard you already keep open without ever opening another tab.
  • Government & regulatory pages — legal filings, permit lists, public notices, tariff schedules.
  • Tickets, releases, drops — concert seats, sneaker restocks, beta signups, registration windows opening.
Why developers prefer LumaBrowser for change detection
ConcernCloud monitors (Visualping, Distill, etc.)LumaBrowser Page Change Detector
JavaScript-rendered pages Often costs extra; sometimes unsupported entirely Built in — every check runs in a real Chromium tab
Authenticated pages & dashboards Requires sharing credentials or upgrading to enterprise Use the same logged-in browser session you already have
Pricing model Per-check / per-monitor / per-frequency tiers Free with LumaBrowser. Run as many monitors as your machine handles
Where the data lives On a vendor's servers In a local SQLite database on your machine
Element picker Yes (varies by tier) Yes — in-page overlay, multi-select, persisted per monitor
Webhook on change Often a paid feature Built in — structured JSON to any URL
Programmability Web UI only Full REST API: create, update, history, manual check, run picker
Scheduling jitter Fixed intervals Optional ±jitter % so monitors don't fire in lockstep
How detection works
  1. Open the page. A silent (hidden) tab is created for the URL so it never clutters your tab bar. If you've enabled no-refresh mode, the existing open tab is reused instead.
  2. Extract the text. Either the full page text, or the concatenated text of the specific elements you picked.
  3. Hash it. A SHA-256 checksum is computed and compared to the last snapshot stored in SQLite.
  4. If it changed, a line-level diff is computed (added vs. removed lines, with snippet previews) and a record is appended to the monitor's history.
  5. Notify. Optionally fire a desktop notification, and POST a structured JSON payload to your webhook URL.
  6. Sleep, then repeat. The next interval is scheduled with optional ±jitter to avoid every monitor firing on the same second.
Sample diff summary
+2 / -1 + $19.99 — In stock, ships in 1–2 days + Limited time: 15% off applied at checkout - $24.99 — Out of stock, notify me
Point-and-click element picker

Watching the whole page is easy — and noisy. A page header re-render, a tracking pixel, a footer copyright year tick — any of them will trip a full-page checksum. The fix is to watch only the elements that matter.

Open a monitor's settings, click Pick elements, and the page opens in a real tab with a live overlay. Hover to highlight, click to select, click again to deselect. The picker captures stable CSS selectors (preferring IDs, data attributes, and structural paths) and persists them on the monitor.

From that point forward, the checksum is computed against just those elements — so a price tile that flips from "$24.99" to "$19.99" trips the alert, and an unrelated banner change in the header does not.

Two check modes

Default: silent background tab

Each cycle, a hidden tab navigates to the URL, lets the page load, runs the extraction, then sticks around for next time. It never appears in your tab bar. This is the right default for any page that doesn't update itself.

No-refresh mode: piggy-back on an open tab

Some pages update themselves — status dashboards over WebSockets, live tickers, internal SPAs polling their own backends. For those, no-refresh mode tells the monitor: don't ever open or refresh a tab; just look at whatever's already there. If the page has no open tab, the cycle is skipped quietly.

This is the mode that makes monitoring a dashboard you keep open all day genuinely free — no extra page loads, no extra requests to the backend, no risk of tripping rate limits.

Scheduling & jitter

Each monitor has its own check interval — default 300000 ms (5 minutes), configurable from seconds to days. The next-fire time is computed per cycle, so a slow check doesn't push every following check off the clock.

If you set intervalJitterPercent, every cycle picks a random delay within ±N% of the base interval. With twenty monitors all on a 5-minute schedule, jitter prevents them from firing in lockstep on the same second — useful when each fire spawns a hidden tab and a webhook POST.

// Base 5 min, ±20% jitter → next fire somewhere in [240s, 360s]
{
  "checkIntervalMs": 300000,
  "intervalJitterPercent": 20
}

Quick start

1. Create a monitor

curl -X POST http://localhost:3000/api/page-change-detector \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Pricing Page",
    "url":  "https://acme.com/pricing",
    "checkIntervalMs":      300000,
    "intervalJitterPercent": 10,
    "webhookUrl":           "https://hooks.slack.com/services/XXX/YYY/ZZZ",
    "desktopNotifications": true,
    "enabled":              true
  }'

2. (Recommended) Pick the elements to watch

Open the Monitors side panel in LumaBrowser, expand your monitor, and click Pick elements — then click the price tiles, the headline, the in-stock badge, whatever you actually care about. Or trigger it from the API:

curl -X POST http://localhost:3000/api/page-change-detector/mon_xxx/pick

This opens the URL in a foreground tab, overlays the picker, and waits for you to click Done. The chosen selectors are saved on the monitor.

3. Force the first check

curl -X POST http://localhost:3000/api/page-change-detector/mon_xxx/check

The first check is always recorded as initial snapshot — subsequent checks compare against it. From here on, you'll get webhooks and notifications whenever the chosen text changes.

4. Inspect history

# Most recent 20 entries
curl http://localhost:3000/api/page-change-detector/mon_xxx/history

# Paginated, change events only
curl "http://localhost:3000/api/page-change-detector/mon_xxx/history?page=0&pageSize=25&changedOnly=1"
Webhook payload

When a change is detected and webhookUrl is set, this JSON is POSTed with Content-Type: application/json:

{
  "monitorId":    "mon_1714742312000_a4b1k2",
  "monitorName":  "Acme Pricing Page",
  "url":          "https://acme.com/pricing",
  "timestamp":    "2026-05-03T17:22:08.412Z",
  "checksum":     "9f3c1b2e... (sha256 of the new extracted text)",
  "prevChecksum": "1a8d04bf... (or null on the first detected change)",
  "diffSummary":  "+2 / -1\n+ $19.99 — In stock, ships in 1–2 days\n- $24.99 — Out of stock",
  "textPreview":  "First 2000 characters of the new extracted text…",
  "textLength":   4821,
  "changeCount":  17
}

Two notes:

  • diffSummary is a compact, multi-line string. Line 0 is the stats header (+N / -M, or whitespace / formatting only); subsequent lines are up to four added () and four removed () snippets, each truncated to 140 characters.
  • For the first detected change on a fresh monitor, prevChecksum may be null — treat the first webhook as a baseline notification.
REST API

All endpoints are mounted at /api/page-change-detector on the local LumaBrowser HTTP server (default http://localhost:3000). Responses follow the standard envelope: {"success": true, ...} on the happy path; {"success": false, "error": "..."} on failure.

EndpointDescription
GET /api/page-change-detectorList all monitors. Returns { success, monitors, count }.
POST /api/page-change-detectorCreate a monitor. Body fields below. Returns { success, monitor }.
GET /api/page-change-detector/:idFetch a single monitor by ID.
PATCH /api/page-change-detector/:idPartial update. Any field accepted by create can be patched, plus selectors (an array of CSS selectors, or null to clear).
DELETE /api/page-change-detector/:idDelete a monitor and its history.
GET /api/page-change-detector/:id/historyChange history. Default returns the most recent ?limit=20. Pass ?page, ?pageSize, or ?changedOnly=1 to switch into paginated mode (response then includes total, page, pageSize, changedOnly).
POST /api/page-change-detector/:id/checkForce an immediate check. Useful for verifying a webhook URL or seeding the first snapshot.
POST /api/page-change-detector/:id/pickOpen the URL in a foreground tab and launch the in-page element picker. Resolves when the user clicks Done or Cancel. Response is { success, cancelled, selectors?, monitor? }.

Update example: switch to a tighter schedule

curl -X PATCH http://localhost:3000/api/page-change-detector/mon_xxx \
  -H "Content-Type: application/json" \
  -d '{
    "checkIntervalMs":       60000,
    "intervalJitterPercent": 25
  }'

Update example: clear element selectors (fall back to full page)

curl -X PATCH http://localhost:3000/api/page-change-detector/mon_xxx \
  -H "Content-Type: application/json" \
  -d '{ "selectors": null }'
Monitor fields
FieldTypeDefaultDescription
namestringRequired. Human-readable label for the monitor.
urlstringRequired. The URL to monitor. Bare hostnames are upgraded to https://.
checkIntervalMsinteger (ms)300000 (5 min)How often the monitor checks. Floor of 1000 ms.
intervalJitterPercentinteger (0–100)0Random ±% offset applied per cycle. Prevents lockstep across many monitors.
webhookUrlstringemptyWhere to POST the change payload. Empty = no webhook.
desktopNotificationsbooleantrueShow a desktop notification on detected changes.
enabledbooleantrueRun the schedule. Set false to pause without deleting.
noRefreshRequiredbooleanfalsePiggy-back on an existing open tab; never create a new one. Cycles where the page isn't open are silently skipped.
selectorsarray of strings, or nullnullCSS selectors to extract text from. null or empty means watch the whole page text. Set via the picker, or PATCH with an array directly.

Frequently asked questions

How is this different from setting up a cron job that curls a page?

A cron curl only sees server-rendered HTML. Modern sites render most of their content in the browser with JavaScript, so the price you see and the price curl sees are often completely different. LumaBrowser checks inside a real Chromium tab, so what the monitor sees is what your users see — including content rendered by React, Vue, Svelte, or any other framework. You also get the element picker, structured webhooks, history, and a UI without writing any of it.

Does the monitor actually open visible tabs every five minutes?

No. By default the monitor uses a silent tab — created and reused in the background so it never appears in your tab bar. In no-refresh mode, no tab is created at all; the monitor simply reads whatever tab you already have open at that URL.

Can I monitor a page that requires login?

Yes. The monitor runs in your normal LumaBrowser session, so it inherits cookies and authentication state. Log in once, leave the session active, and the monitor sees the page exactly as you do.

What happens if the page is briefly down or returns an error?

If text extraction fails, the cycle is logged and skipped — no false-positive change is recorded. The next cycle tries again. Brief outages don't pollute your history with phantom diffs.

How long is change history kept?

Indefinitely — every check is appended to a SQLite table on your machine, and the dedicated history view supports paginated queries with a "changes only" filter. Delete the monitor to drop its history; or run a SQL DELETE if you want to prune by age.

Can I trigger anything more sophisticated than a webhook on change?

Yes. The webhook payload includes the diff summary, change count, checksum, and the first 2 KB of the new content — pipe it into n8n, Zapier, an HTTP-triggered Lambda, or your own service. From there you can rerun a fuller LumaBrowser automation (template extraction, AI Chat workflow), open a ticket, send an SMS, anything.

Is there a hard limit on the number of monitors?

None imposed by the extension. Practically, the limit is what your machine can handle — each silent check briefly opens a tab, so hundreds of monitors on tight schedules will compete for memory. Use jitter to smooth out the load and longer intervals where seconds don't matter.