Skip to content

Change Detection

Turn any monitor into a watcher. When the page you monitor - or the part you select - changes between runs, ScrapeNest notifies you by webhook and email and stores a before/after diff. Use it for price and stock monitoring, competitor and content changes, terms-of-service updates, and regulatory pages.

Change detection is an optional detection block on a monitor. Every run of that monitor is fingerprinted, compared to the previous version, and (on a real change) diffed and notified. A watched run is billed exactly like any other run - there is no separate charge for detection.

How it works

  • The first evaluated run captures a reference version (the baseline) and sends a schedule.monitor_activated confirmation - not a change alert.
  • Each later run is compared to the previous one. Only runs that actually delivered content are evaluated; a blocked or failed run is never treated as a change.
  • The fingerprint is computed over normalized content (whitespace collapsed, scripts/styles and your ignored selectors removed), so rotating ads, timestamps, and session tokens do not cause false alerts.
  • On a change, ScrapeNest records it in the change history, generates a diff, and - if the change clears your thresholds - sends schedule.change_detected by webhook and email.

Plan limits

Plan Change detection Modes Max monitors History retained
Free Not included - - -
Starter Included text, selector, extract, json 5 30 days
Pro Included text, selector, extract, json 25 90 days
Business Included text, selector, extract, json 100 180 days
Enterprise Included text, selector, extract, json Unlimited 365 days

Enabling detection on a plan that does not include it (or a mode it does not allow) is rejected at creation time.

The detection block

Field Type Description
enabled bool Turn change detection on/off for this monitor.
mode string What to fingerprint - see the modes below.
selector string CSS or XPath selector to watch. Required when mode is selector.
ignore_selectors string[] Selectors whose content is masked before fingerprinting (noisy regions).
min_change_ratio number Suppress alerts for changes smaller than this fraction (0-1) of the watched content.
consecutive int Alert only after this many consecutive changed runs (flap control).
cooldown_seconds int Suppress repeat alerts within this many seconds.
notify.webhook bool Emit the schedule.change_detected webhook (default true).
notify.email string[] Email addresses to notify.

Modes

Mode What it watches Best for
text Visible text of the whole page (normalized) General pages
selector Only the nodes matching selector Watching one region (a price, a status)
extract The structured data your extraction hooks produced - a field-level diff (for example, price 49 to 39). Uses the extraction rules already on the job; run-varying metadata is ignored. Precise, high-signal monitoring of specific fields
json The response body as canonical JSON (keys sorted, so reordering is not a change) Monitoring an API/JSON endpoint

extract mode needs extraction hooks configured on the job. If none produced data, the detection health shows selector_missing (it needs attention).

Enable detection

from scrapenest import ScrapeNestClient

client = ScrapeNestClient(api_key="sn_live_...", base_url="https://api.scrapenest.com")

monitor = client.monitors.create(
    name="competitor-pricing",
    cron="0 * * * *",
    job_type="standard",
    target_url="https://competitor.example/pricing",
    detection={
        "enabled": True,
        "mode": "selector",
        "selector": ".price, #stock",
        "ignore_selectors": [".ads", "time"],
        "min_change_ratio": 0.05,
        "notify": {"webhook": True, "email": ["alerts@acme.eu"]},
    },
)
print(monitor.detection)   # config + health (status, last_checked_at, last_change_at)
curl -X POST "https://api.scrapenest.com/api/v1/monitors" \
  -H "X-API-Key: sn_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "competitor-pricing",
    "cron": "0 * * * *",
    "job_type": "standard",
    "target_url": "https://competitor.example/pricing",
    "detection": {
      "enabled": true,
      "mode": "selector",
      "selector": ".price, #stock",
      "ignore_selectors": [".ads", "time"],
      "min_change_ratio": 0.05,
      "notify": {"webhook": true, "email": ["alerts@acme.eu"]}
    }
  }'

Detection health

GET /api/v1/monitors/{id} returns the detection block including a health object so you can answer "is my monitor working?" without guessing:

  • status - ok, or selector_missing when your selector no longer matches the page (the monitor needs attention - the page structure likely changed).
  • last_checked_at - when the monitor last evaluated a run.
  • last_change_at - when it last detected a change.

Review changes

GET /api/v1/monitors/{id}/changes returns the change history, newest first.

for change in client.monitors.iter_changes(monitor.id):
    pct = f"{change.change_ratio * 100:.1f}%" if change.change_ratio is not None else "n/a"
    print(change.detected_at, "changed", pct, "notified:", change.notified_channels)
curl "https://api.scrapenest.com/api/v1/monitors/MONITOR_ID/changes" \
  -H "X-API-Key: sn_live_..."

Each change carries old_fingerprint, new_fingerprint, change_ratio, has_diff, and the notified_channels used (empty when a change was recorded but its notification was debounced).

Updating and turning it off

On update, omit detection to leave it unchanged. Changing the mode or selector resets the baseline (the next run re-captures a reference version). To turn detection off while keeping the monitor running, send "detection": {"enabled": false}.

Notifications

Subscribe to schedule.change_detected and schedule.monitor_activated, and/or list emails in notify.email. The change notification includes the change ratio and a short-lived signed link to the diff.

Digest emails

For high-frequency monitors, set notify.digest to daily or weekly to receive one summary email per period instead of one email per change. Webhooks still fire in real time on every change; only the email is batched.

"notify": { "webhook": true, "email": ["ops@acme.eu"], "digest": "daily" }

Viewing the diff

Every detected change with a stored diff exposes it at GET /api/v1/monitors/{id}/changes/{changeId}/diff (also rendered in the console). Lines prefixed with - were removed and + were added. For extract/json monitors this is a field-level diff:

  {
-   "price": "EUR 49"
+   "price": "EUR 39"
  }