Guides

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

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"
  }'
FieldDescription
urlThe page to monitor (required)
interval_typeCheck frequency — one of hourly, daily, weekly, or cron (required)
webhook_urlHTTPS webhook endpoint to receive notifications. http:// is rejected with a 400 invalid_url (reason scheme_unsupported) (required)
cron_expression5-field cron expression. Required when interval_type is cron
typeCapture mode — screenshot (default) or scrape
change_thresholdPercentage of change required to trigger a notification (0–100, default 5). Screenshot monitors only
notify_onchange (default — notify only when content changed) or every (notify on every capture)
webhook_secretOptional secret used to sign webhook deliveries
webhook_headersOptional custom headers (max 20) to include on each webhook delivery

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

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

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

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

Get a monitor

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

Update a monitor

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:

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

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 below). Retrieve the capture history with:

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. Both monitor types treat that first run as a silent baseline — one universal "first capture" behavior, no per-type divergence:

Monitor typeFirst capturehas_changechange_pctWebhook on first capture
scrapeSilent baselinefalsenullNo monitor.change_detected-equivalent webhook (the baseline is just recorded)
screenshotSilent baselinefalsenullNo 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 monitor.captured webhook on the first capture — that is the per-capture notification, not a change alert, and it fires for both monitor types. From the second capture onward both types behave the same way each new capture is compared against the previous one — scrape by exact SHA-256 content equality, screenshot by perceptual-hash distance against change_threshold.

Existing scrape monitors: if you created a scrape monitor before this change shipped, its first post-deploy capture will no longer fire a monitor.change_detected-equivalent webhook — the first capture after the change is now the silent baseline, matching screenshot monitors. Steady-state alerting (second capture onward) is unaffected.

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:

{
  "monitor_id": "mon_...",
  "screenshot_id": "ss_...",
  "has_change": true,
  "change_pct": 42.0,
  "hash_distance": 18,
  "url": "https://example.com/pricing"
}
FieldDescription
has_changetrue when the captured content differs from the previous capture
change_pctPercentage of content that changed (0–100)
hash_distancePerceptual-hash distance from the previous capture
screenshot_idThe screenshot that triggered the notification

Scrape monitors (type: "scrape")

Scrape captures send scrape.completed (X-ByteKit-Event: scrape.completed) with the full scrape result envelope inline:

{
  "success": true,
  "type": "scrape.completed",
  "id": "mon_...",
  "scrape_id": "sc_...",
  "has_change": true,
  "change_pct": 100.0,
  "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": "..."
    }
  }
}
FieldDescription
has_changetrue when the captured content differs from the previous capture
change_pctPercentage of content that changed (0–100)
scrapeFull 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