# Monitors

Detect page changes on any URL — by screenshot or scrape — and receive webhook notifications.

Monitors periodically capture a URL and notify you when the page changes. Each monitor uses one
of two capture types: `screenshot` (perceptual/visual change detection) or `scrape` (content and
markdown change detection). Use them for price tracking, content alerts, uptime monitoring, or
competitor surveillance.

## Create a monitor

```bash
curl -X POST https://api.bytekit.com/v1/monitors \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "interval_type": "hourly",
    "webhook_url": "https://your-server.example.com/webhooks/bytekit"
  }'
```

| Field              | Description                                                                                                                                                         |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `url`              | The page to monitor (required)                                                                                                                                      |
| `interval_type`    | Check frequency — one of `hourly`, `daily`, `weekly`, or `cron` (required)                                                                                          |
| `webhook_url`      | HTTPS webhook endpoint to receive notifications. `http://` is rejected with a 400 `invalid_url` (reason `scheme_unsupported`) (required)                            |
| `cron_expression`  | 5-field cron expression. Required when `interval_type` is `cron`                                                                                                    |
| `type`             | Capture mode — `screenshot` (default) or `scrape`                                                                                                                   |
| `change_threshold` | Percentage of change required to trigger a notification (0–100, default `5`). Screenshot monitors only                                                              |
| `notify_on`        | `change` (default — notify only when content changed) or `every` (notify on every capture)                                                                          |
| `webhook_secret`   | Optional secret used to sign webhook deliveries. Deliveries carry `X-Webhook-Signature` and `X-Webhook-Timestamp` — see [Webhooks](https://bytekit.com/docs/guides/webhooks) to verify them |
| `webhook_headers`  | Optional custom headers (max 20) to include on each webhook delivery                                                                                                |

A successful response returns the monitor object with an `id` prefixed `mon_`:

```json
{
  "id": "mon_...",
  "type": "screenshot",
  "url": "https://example.com/pricing",
  "status": "active",
  "interval_type": "hourly",
  "cron_expression": null,
  "next_capture_at": "2026-01-01T01:00:00Z",
  "notify_on": "change",
  "webhook_url": "https://your-server.example.com/webhooks/bytekit",
  "captures_count": 0,
  "changes_count": 0,
  "created_at": "2026-01-01T00:00:00Z"
}
```

To monitor on a schedule, set `interval_type` to `cron` and supply a 5-field `cron_expression`:

```bash
curl -X POST https://api.bytekit.com/v1/monitors \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/pricing",
    "interval_type": "cron",
    "cron_expression": "0 9 * * 1-5",
    "webhook_url": "https://your-server.example.com/webhooks/bytekit"
  }'
```

## List monitors

```bash
curl https://api.bytekit.com/v1/monitors \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE"
```

## Get a monitor

```bash
curl https://api.bytekit.com/v1/monitors/mon_... \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE"
```

## Update a monitor

```bash
curl -X PATCH https://api.bytekit.com/v1/monitors/mon_... \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"interval_type": "daily"}'
```

You can update any subset of the monitor's mutable fields — for example `interval_type`,
`cron_expression`, `webhook_url`, `notify_on`, `change_threshold`, or `status`
(`active` / `paused`). Switching to a `cron` schedule requires a `cron_expression`:

```bash
curl -X PATCH https://api.bytekit.com/v1/monitors/mon_... \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"interval_type": "cron", "cron_expression": "*/30 * * * *"}'
```

## Delete a monitor

```bash
curl -X DELETE https://api.bytekit.com/v1/monitors/mon_... \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE"
```

## Monitor captures

Each time a monitor fires it records a capture event — on **every** run, whether or not the
content changed. Each capture carries a `has_change` flag marking whether the content differed
from the previous capture. Webhook delivery is separate: the capture is always recorded, but
whether a webhook is _sent_ depends on `has_change` and the monitor's `notify_on` setting (see
[Webhook payload](#webhook-payload) below). Retrieve the capture history with:

```bash
curl https://api.bytekit.com/v1/monitors/mon_.../captures \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE"
```

Each capture in the list represents one capture event and includes change metadata — the
timestamp, whether the content changed (`has_change`), the percentage of content that changed
(`change_pct`), and the perceptual hash distance from the previous capture. Captures record
_that_ the content changed and by how much; they do not include a line-by-line textual diff.

### First-capture behavior (silent baseline for both types)

The very first capture of a monitor has **no previous capture to compare against**, so both
monitor types record it as the baseline instead of alerting:

| Monitor type | First capture   | `has_change` | `change_pct` | Webhook on first capture                                                                           |
| ------------ | --------------- | ------------ | ------------ | -------------------------------------------------------------------------------------------------- |
| `scrape`     | Silent baseline | `false`      | `null`       | **No** change alert (no `scrape.completed` with `has_change: true`; the baseline is just recorded) |
| `screenshot` | Silent baseline | `false`      | `null`       | **No** `monitor.change_detected` webhook (the baseline is just recorded)                           |

Neither a **scrape** nor a **screenshot** monitor alerts you on its first run — both only alert
once a _subsequent_ capture differs (scrape by exact content-hash inequality, screenshot by at
least `change_threshold`). A monitor with `notify_on: "every"` still sends a webhook on the first
capture — that is the per-capture notification, not a change alert. A screenshot monitor sends
`monitor.captured`; a scrape monitor sends `scrape.completed` with `has_change: false`.
**Repointing a monitor's `url`** with `PATCH` starts over the same way: the next capture is
treated as a fresh first capture, as in the table above, because the stored comparison baseline
belongs to the old URL, and steady-state alerting resumes from the _second_ capture after the
repoint. A capture already
in flight against the old URL when the `PATCH` lands can still write its result back as the new
baseline, which defers the first-capture treatment to the _next_ capture.

## Webhook payload

When a monitor fires, ByteKit sends a `POST` to your `webhook_url`. Every delivery carries an
`X-ByteKit-Event` header naming the event type — dispatch on that header rather than inspecting
the body shape. The event type and payload depend on the monitor's `type`.

### Screenshot monitors (`type: "screenshot"`, default)

Screenshot captures send `monitor.change_detected` when the page changed (`X-ByteKit-Event:
monitor.change_detected`) and `monitor.captured` when there was no change but
`notify_on: "every"` is set (`X-ByteKit-Event: monitor.captured`). The body carries the capture
metadata:

```json
{
  "monitor_id": "mon_...",
  "screenshot_id": "ss_...",
  "has_change": true,
  "change_pct": 42.0,
  "hash_distance": 18,
  "url": "https://example.com/pricing"
}
```

| Field           | Description                                                              |
| --------------- | ------------------------------------------------------------------------ |
| `has_change`    | `true` when the captured content differs from the previous capture       |
| `change_pct`    | Percentage of content that changed (0–100), or `null` on a first capture |
| `hash_distance` | Perceptual-hash distance from the previous capture                       |
| `screenshot_id` | The screenshot that triggered the notification                           |

### Scrape monitors (`type: "scrape"`)

Scrape monitors never send `monitor.change_detected` or `monitor.captured`. Every delivered scrape
capture sends `scrape.completed` (`X-ByteKit-Event: scrape.completed`) with the full scrape result
envelope inline, and `has_change` tells a change from a no-change capture. With
`notify_on: "change"` only captures where `has_change` is `true` are delivered; with
`notify_on: "every"` every capture is.

```json
{
  "success": true,
  "type": "scrape.completed",
  "id": "mon_...",
  "scrape_id": "sc_...",
  "has_change": true,
  "change_pct": 100.0,
  "was_preflight": false,
  "metadata": {},
  "scrape": {
    "status": "success",
    "id": "sc_...",
    "url": "https://example.com/pricing",
    "final_url": "https://example.com/pricing",
    "content_length": 1256,
    "status_code": 200,
    "formats": {
      "markdown": "..."
    }
  }
}
```

| Field           | Description                                                                                                                                                      |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `has_change`    | `true` when the captured content differs from the previous capture                                                                                               |
| `change_pct`    | `100` when the content hash changed, `0` when it did not (including a `was_preflight: true` capture), or `null` on a first capture                               |
| `was_preflight` | `true` when ByteKit confirmed the page unchanged without a full capture (`has_change` is `false`, `scrape` carries no page content); `false` on a normal capture |
| `scrape`        | Full scrape result envelope (same shape as `POST /v1/scrape` response)                                                                                           |

Your endpoint must respond with `2xx` within 10 seconds. Failed deliveries are retried with
exponential backoff.

## Next steps

- [Webhooks](https://bytekit.com/docs/guides/webhooks) — verify the `X-Webhook-Signature` on every delivery
- [API Reference: Monitors](https://bytekit.com/docs/api/monitors/createmonitor) — full endpoint schema
- [Errors](https://bytekit.com/docs/guides/errors) — error codes returned by monitor endpoints