```yaml
product: AlterLab
title: "Change Detection & Page Monitoring API — Track Website Changes Automatically"
comparison_context: "AlterLab is an alternative to Firecrawl, ScrapingBee, and Bright Data."
source_url: https://alterlab.io/docs/guides/monitors
```

Guide

New

# Change Detection

Monitor any webpage for content changes. AlterLab checks your URLs on a schedule, compares snapshots, and notifies you when something changes.

The Monitors dashboard lets you manage all your active monitors, view check history, and see detected changes at a glance.

Built on the Scheduler

Monitors use the same cron-based scheduling engine as [Schedules](https://alterlab.io/docs/guides/scheduler). The same balance-based limits apply for check intervals and active monitor counts.

## How It Works

1

### Create

Define a monitor with a URL, diff mode, check interval, and optional CSS selectors to narrow focus.

2

### Snapshot

At each scheduled time, AlterLab scrapes the page and stores a content snapshot with a hash for fast comparison.

3

### Compare

The new snapshot is compared against the previous one using your chosen diff mode. If content changed, a change record is created.

4

### Notify

When a change is detected (or on every check, depending on your `notify_on` setting), your webhook receives the diff.

## Create a Monitor

POST

`/api/v1/monitors`

Bash

```
curl -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.

JSON

```
{
  "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.

JSON

```
{
  "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.

JSON

```
{
  "diff_mode": "selector",
  "monitor_selectors": [
    ".product-price",
    ".stock-status",
    "h1.product-title"
  ]
}
```

Selectors Required

When using `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](https://alterlab.io/docs/guides/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](https://alterlab.io/docs/guides/scheduler#balance-limits) for details.

## Manage Monitors

### List Monitors

GET

`/api/v1/monitors`

Query parameters: `limit` (1-100, default 20), `offset` (default 0), `active_only` (default false).

Bash

```
curl https://api.alterlab.io/api/v1/monitors?active_only=true \
  -H "Authorization: Bearer your_jwt_token"
```

### Update a Monitor

PATCH

`/api/v1/monitors/{monitor_id}`

Send only the fields you want to change. Changing `check_interval` or `timezone` recomputes `next_run_at`.

Bash

```
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

Bash

```
# 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

DELETE

`/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.

GET

`/api/v1/monitors/{monitor_id}/snapshots`

Returns a paginated list of snapshots, most recent first. Query parameters: `limit` (1-100), `offset`.

JSON

```
{
  "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:

GET

`/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.

GET

`/api/v1/monitors/{monitor_id}/diffs`

Returns a paginated list of detected changes, most recent first.

JSON

```
{
  "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:

GET

`/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

Use `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.

### Webhook Event Types

Monitor webhooks deliver two distinct event types. Both are sent to the `webhook_url` registered on the monitor, and to any account-level registered webhook endpoints subscribed to monitor events:

| Event | Triggered When | notify_on Required |
| --- | --- | --- |
| monitor.changed | A diff is detected since the last snapshot | `change` or `always` |
| monitor.checked | A check completes regardless of whether content changed | `always` only |

JSON

```
// monitor.changed payload
{
  "event": "monitor.changed",
  "timestamp": "2025-01-15T09:00:05.123Z",
  "data": {
    "monitor_id": "mon_550e8400...",
    "monitor_name": "Competitor pricing tracker",
    "url": "https://competitor.com/pricing",
    "diff_mode": "selector",
    "snapshot_id": "snap_abc123...",
    "previous_snapshot_id": "snap_def456...",
    "changes": {
      "added": [".price-card:nth-child(1) > .amount: $49"],
      "removed": [".price-card:nth-child(1) > .amount: $39"],
      "summary": "1 element changed"
    },
    "checked_at": "2025-01-15T09:00:03.000Z"
  }
}

// monitor.checked payload (always mode)
{
  "event": "monitor.checked",
  "timestamp": "2025-01-15T09:00:05.123Z",
  "data": {
    "monitor_id": "mon_550e8400...",
    "monitor_name": "Competitor pricing tracker",
    "url": "https://competitor.com/pricing",
    "snapshot_id": "snap_abc123...",
    "changed": false,
    "status": "ok",
    "checked_at": "2025-01-15T09:00:03.000Z"
  }
}
```

## Python Example

Python

```
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

TYPESCRIPT

```
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)}`);
}
```

[Scheduler](https://alterlab.io/docs/guides/scheduler)[Cloud Storage Export](https://alterlab.io/docs/guides/cloud-storage)

Last updated: June 2026