Change Detection
Monitor any webpage for content changes. AlterLab checks your URLs on a schedule, compares snapshots, and notifies you when something changes.
Built on the Scheduler
How It Works
Create
Define a monitor with a URL, diff mode, check interval, and optional CSS selectors to narrow focus.
Snapshot
At each scheduled time, AlterLab scrapes the page and stores a content snapshot with a hash for fast comparison.
Compare
The new snapshot is compared against the previous one using your chosen diff mode. If content changed, a change record is created.
Notify
When a change is detected (or on every check, depending on your notify_on setting), your webhook receives the diff.
Create a Monitor
/api/v1/monitorscurl -X POST https://api.alterlab.io/api/v1/monitors \
-H "Authorization: Bearer your_jwt_token" \
-H "Content-Type: application/json" \
-d '{
"name": "Competitor pricing page",
"url": "https://competitor.com/pricing",
"diff_mode": "semantic",
"check_interval": "0 */6 * * *",
"timezone": "America/New_York",
"notify_on": "change",
"webhook_url": "https://your-server.com/monitor-webhook",
"options": {
"wait_for": ".pricing-table",
"timeout": 30
}
}'Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Human-readable name (1-255 chars) |
| url | string | Yes | URL to monitor for changes |
| diff_mode | string | No | semantic (default), exact, or selector |
| check_interval | string | No | Cron expression (default: 0 */6 * * *) |
| timezone | string | No | IANA timezone (default: UTC) |
| monitor_selectors | string[] | Conditional | CSS selectors to watch (required when diff_mode=selector, max 20) |
| notify_on | string | No | change (default) or always |
| formats | string[] | No | Output formats: text, markdown, json, html, rag (default: markdown, json) |
| options | object | No | Scrape options: wait_for, timeout, headers, cookies, proxy_country |
| webhook_url | string | No | URL to receive change notifications |
Diff Modes
Choose how content is compared between snapshots. The diff mode determines what counts as a “change” and how the diff is formatted.
Semantic Default
Compares the structural content of the page, ignoring whitespace changes, ad injection, and layout shifts. Best for tracking meaningful content updates like price changes, product descriptions, or article text.
{
"diff_mode": "semantic"
}Exact
Byte-level comparison. Any change in the rendered content triggers a diff. Use when you need to detect every change, including formatting and whitespace.
{
"diff_mode": "exact"
}Selector
Monitor specific elements using CSS selectors. Only the matched elements are compared between snapshots. Ideal for tracking a specific price, stock status, or headline without being triggered by unrelated page changes.
{
"diff_mode": "selector",
"monitor_selectors": [
".product-price",
".stock-status",
"h1.product-title"
]
}Selectors Required
diff_mode: selector, you must provide at least one CSS selector in monitor_selectors. Maximum 20 selectors, each up to 500 characters.Check Intervals
Monitors use standard 5-field cron expressions. The same balance-based limits from the Scheduler apply.
| Expression | Schedule |
|---|---|
| */15 * * * * | Every 15 minutes |
| 0 */6 * * * | Every 6 hours (default) |
| 0 9 * * * | Every day at 9:00 AM |
| 0 8 * * 1-5 | Weekdays at 8:00 AM |
| 0 0 * * 0 | Every Sunday at midnight |
Minimum interval depends on your account balance. See Balance-Based Limits for details.
Manage Monitors
List Monitors
/api/v1/monitorsQuery parameters: limit (1-100, default 20), offset (default 0), active_only (default false).
curl https://api.alterlab.io/api/v1/monitors?active_only=true \
-H "Authorization: Bearer your_jwt_token"Update a Monitor
/api/v1/monitors/{monitor_id}Send only the fields you want to change. Changing check_interval or timezone recomputes next_run_at.
curl -X PATCH https://api.alterlab.io/api/v1/monitors/{monitor_id} \
-H "Authorization: Bearer your_jwt_token" \
-H "Content-Type: application/json" \
-d '{
"diff_mode": "selector",
"monitor_selectors": [".price", ".availability"],
"check_interval": "0 */2 * * *"
}'Pause & Resume
# Pause a monitor
curl -X POST https://api.alterlab.io/api/v1/monitors/{monitor_id}/pause \
-H "Authorization: Bearer your_jwt_token"
# Resume (recomputes next_run_at, re-checks balance limits)
curl -X POST https://api.alterlab.io/api/v1/monitors/{monitor_id}/resume \
-H "Authorization: Bearer your_jwt_token"Delete a Monitor
/api/v1/monitors/{monitor_id}Soft-deletes the monitor (deactivates it). Returns 204 No Content.
Snapshots
Every check stores a snapshot of the page content. Use snapshots to review historical versions or compare content at different points in time.
/api/v1/monitors/{monitor_id}/snapshotsReturns a paginated list of snapshots, most recent first. Query parameters: limit (1-100), offset.
{
"snapshots": [
{
"id": "snap-uuid-...",
"schedule_id": "monitor-uuid-...",
"run_id": "run-uuid-...",
"url": "https://competitor.com/pricing",
"content_hash": "a1b2c3d4...",
"metadata": { "status_code": 200, "content_length": 42350 },
"created_at": "2026-03-24T12:00:05Z"
}
],
"total": 156
}To retrieve the full content of a specific snapshot:
/api/v1/monitors/{monitor_id}/snapshots/{snapshot_id}Returns the snapshot with the full content field included.
Changes & Diffs
When a snapshot differs from the previous one, a change record is created. Each change includes the full diff and references both the previous and current snapshots.
/api/v1/monitors/{monitor_id}/diffsReturns a paginated list of detected changes, most recent first.
{
"changes": [
{
"id": "change-uuid-...",
"schedule_id": "monitor-uuid-...",
"previous_snapshot_id": "snap-old-...",
"current_snapshot_id": "snap-new-...",
"url": "https://competitor.com/pricing",
"change_type": "content_changed",
"diff": {
"added": ["Enterprise plan now $299/mo"],
"removed": ["Enterprise plan now $199/mo"],
"summary": "Price increase detected on Enterprise plan"
},
"notified": true,
"created_at": "2026-03-24T12:00:06Z"
}
],
"total": 12
}To retrieve a specific change:
/api/v1/monitors/{monitor_id}/diffs/{change_id}Notifications
Configure when your webhook is called using the notify_on parameter:
| Value | Behavior |
|---|---|
| change | Webhook fires only when a diff is detected (default) |
| always | Webhook fires after every check, even when nothing changed |
Tip
notify_on: "always" for uptime monitoring. Your webhook receives a call on every check, so you can detect when a page goes down even if the content hasn't changed.Python Example
import requests
API_URL = "https://api.alterlab.io/api/v1"
HEADERS = {
"Authorization": "Bearer your_jwt_token",
"Content-Type": "application/json",
}
# Create a price monitoring watch
monitor = requests.post(
f"{API_URL}/monitors",
headers=HEADERS,
json={
"name": "Competitor pricing tracker",
"url": "https://competitor.com/pricing",
"diff_mode": "selector",
"monitor_selectors": [".price-card", ".plan-name"],
"check_interval": "0 */6 * * *",
"notify_on": "change",
"webhook_url": "https://your-server.com/price-alerts",
},
).json()
print(f"Monitor created: {monitor['id']}")
print(f"Next check: {monitor['next_run_at']}")
# List active monitors
monitors = requests.get(
f"{API_URL}/monitors?active_only=true",
headers=HEADERS,
).json()
for m in monitors["monitors"]:
print(f" {m['name']} — mode: {m['diff_mode']}, runs: {m['run_count']}")
# Check recent changes
changes = requests.get(
f"{API_URL}/monitors/{monitor['id']}/diffs",
headers=HEADERS,
).json()
for c in changes["changes"]:
print(f" [{c['created_at']}] {c['change_type']}")
if "summary" in c["diff"]:
print(f" Summary: {c['diff']['summary']}")
# Get snapshot history
snapshots = requests.get(
f"{API_URL}/monitors/{monitor['id']}/snapshots?limit=5",
headers=HEADERS,
).json()
for s in snapshots["snapshots"]:
print(f" Snapshot {s['id'][:8]}... hash={s['content_hash'][:12]}")Node.js Example
const API_URL = "https://api.alterlab.io/api/v1";
const headers = {
Authorization: "Bearer your_jwt_token",
"Content-Type": "application/json",
};
// Create a monitor with CSS selector tracking
const monitor = await fetch(`${API_URL}/monitors`, {
method: "POST",
headers,
body: JSON.stringify({
name: "Competitor pricing tracker",
url: "https://competitor.com/pricing",
diff_mode: "selector",
monitor_selectors: [".price-card", ".plan-name"],
check_interval: "0 */6 * * *",
notify_on: "change",
webhook_url: "https://your-server.com/price-alerts",
}),
}).then((r) => r.json());
console.log(`Monitor: ${monitor.id}, next check: ${monitor.next_run_at}`);
// List active monitors
const list = await fetch(`${API_URL}/monitors?active_only=true`, {
headers,
}).then((r) => r.json());
for (const m of list.monitors) {
console.log(` ${m.name} — ${m.diff_mode}, ${m.run_count} runs`);
}
// Get recent changes
const changes = await fetch(
`${API_URL}/monitors/${monitor.id}/diffs`,
{ headers }
).then((r) => r.json());
for (const c of changes.changes) {
console.log(` [${c.created_at}] ${c.change_type}`);
}
// Browse snapshot history
const snapshots = await fetch(
`${API_URL}/monitors/${monitor.id}/snapshots?limit=5`,
{ headers }
).then((r) => r.json());
for (const s of snapshots.snapshots) {
console.log(` Snapshot ${s.id.slice(0, 8)}... hash=${s.content_hash.slice(0, 12)}`);
}