﻿> ## Documentation Index
> Fetch the complete documentation index at: https://docs.context.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Create Monitors

> Watch any page, sitemap, or extracted dataset on a schedule. Get signed webhooks when changes are detected or after every completed run.

Context.dev's Monitors API watches a **page**, a **sitemap**, or **extracted structured data** on a schedule you set, compares each run against a baseline, and can send signed webhooks when something changes or after every completed run.

You create a monitor once; Context.dev handles the crawling, diffing, judging, and retrying in the background.

<Prompt description="Integrate Context.dev's Monitors API in your app" icon="sparkles" actions={["copy", "cursor"]}>
  I'm integrating Context.dev's Monitors API into my app to watch websites for changes. Help me:

  1. Install the official SDK for my language (`context.dev` on npm / PyPI / RubyGems, `context-dev/context-dev-php` on Packagist, `github.com/context-dot-dev/context-go-sdk` for Go).
  2. Read the API key from the `CONTEXT_DEV_API_KEY` environment variable. Never hardcode it.
  3. Call `client.monitors.create({ name, target, change_detection, schedule, webhook })`. Supported combinations: `page` + `exact` (visible text diffs), `page` + `semantic` (goal-driven page changes judged against `target.instructions`), `sitemap` + `exact` (URL additions/removals), and `extract` + `semantic` (meaning-level site changes judged against my instructions). Schedule is an interval between 10 minutes and 1 year. Set `webhook.events` to `change.detected`, `run.completed`, or both.
  4. Store the returned `monitor.id` and `monitor.webhook.secret`. On each webhook delivery, route by `X-Context-Event` and verify the `X-Context-Signature: t=<unix>,v1=<hmac>` header by recomputing HMAC-SHA256 over `"{t}.{rawBody}"` with the secret, using a constant-time compare, and rejecting stale timestamps.
  5. Alternatively poll `client.monitors.listChanges(monitorId)` (paginated via `cursor`) and fetch full diffs with `client.monitors.retrieveChange(changeId)`.
  6. Trigger an off-schedule check with `client.monitors.run(monitorId)`, and pause/resume by updating `status` to `paused`/`active`.

  Docs: [https://docs.context.dev/guides/monitor-website-changes](https://docs.context.dev/guides/monitor-website-changes)
</Prompt>

## Prerequisites

* **A Context.dev API key.** Sign up at [context.dev/signup](https://context.dev/signup), copy the key from the [dashboard](https://context.dev/dashboard) (prefix `ctxt_secret_`), and export it:

  ```bash theme={null}
  export CONTEXT_DEV_API_KEY="ctxt_secret_..."
  ```

* **An SDK (optional).** Install for your language, or skip the install and call directly with `curl`:

  <CodeGroup>
    ```bash TypeScript theme={null}
    npm install context.dev
    ```

    ```bash Python theme={null}
    pip install context.dev
    ```

    ```bash Ruby theme={null}
    gem install context.dev
    ```

    ```bash Go theme={null}
    go get github.com/context-dot-dev/context-go-sdk
    ```

    ```bash PHP theme={null}
    composer require context-dev/context-dev-php
    ```
  </CodeGroup>

## Create a monitor

A monitor is defined by four things: a `target` (what to watch), a `change_detection` strategy (how to compare), a `schedule` (how often), and an optional `webhook` (where to notify). This creates a monitor that checks a pricing page every 6 hours:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.context.dev/v1/monitors \
    -H "Authorization: Bearer $CONTEXT_DEV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Acme pricing page",
      "target": { "type": "page", "url": "https://acme.com/pricing" },
      "change_detection": { "type": "exact" },
      "schedule": { "type": "interval", "frequency": 6, "unit": "hours" },
      "webhook": { "url": "https://example.com/webhook" }
    }'
  ```

  ```typescript TypeScript theme={null}
  import ContextDev from "context.dev";

  const client = new ContextDev({ apiKey: process.env.CONTEXT_DEV_API_KEY });

  const monitor = await client.monitors.create({
    name: "Acme pricing page",
    target: { type: "page", url: "https://acme.com/pricing" },
    change_detection: { type: "exact" },
    schedule: { type: "interval", frequency: 6, unit: "hours" },
    webhook: { url: "https://example.com/webhook" },
  });

  console.log(monitor.id, monitor.webhook?.secret);
  ```

  ```python Python theme={null}
  import os
  from context.dev import ContextDev

  client = ContextDev(api_key=os.environ["CONTEXT_DEV_API_KEY"])

  monitor = client.monitors.create(
      name="Acme pricing page",
      target={"type": "page", "url": "https://acme.com/pricing"},
      change_detection={"type": "exact"},
      schedule={"type": "interval", "frequency": 6, "unit": "hours"},
      webhook={"url": "https://example.com/webhook"},
  )

  print(monitor.id, monitor.webhook.secret)
  ```

  ```ruby Ruby theme={null}
  require "context_dev"

  client = ContextDev::Client.new(api_key: ENV.fetch("CONTEXT_DEV_API_KEY"))

  monitor = client.monitors.create(
    name: "Acme pricing page",
    target: {type: :page, url: "https://acme.com/pricing"},
    change_detection: {type: :exact},
    schedule: {type: :interval, frequency: 6, unit: :hours},
    webhook: {url: "https://example.com/webhook"}
  )

  puts monitor.id
  ```

  ```go Go theme={null}
  package main

  import (
      "context"
      "fmt"
      "os"

      contextdev "github.com/context-dot-dev/context-go-sdk"
      "github.com/context-dot-dev/context-go-sdk/option"
  )

  func main() {
      client := contextdev.NewClient(
          option.WithAPIKey(os.Getenv("CONTEXT_DEV_API_KEY")),
      )

      monitor, err := client.Monitors.New(context.TODO(), contextdev.MonitorNewParams{
          Name: "Acme pricing page",
          Target: contextdev.MonitorNewParamsTargetUnion{
              OfPage: &contextdev.MonitorNewParamsTargetPage{
                  URL: "https://acme.com/pricing",
              },
          },
          ChangeDetection: contextdev.MonitorNewParamsChangeDetectionUnion{
              OfExact: &contextdev.MonitorNewParamsChangeDetectionExact{},
          },
          Schedule: contextdev.MonitorNewParamsSchedule{
              Type:      "interval",
              Frequency: 6,
              Unit:      "hours",
          },
          Webhook: contextdev.MonitorNewParamsWebhook{
              URL: "https://example.com/webhook",
          },
      })
      if err != nil {
          panic(err)
      }

      fmt.Println(monitor.ID)
  }
  ```

  ```php PHP theme={null}
  <?php

  use ContextDev\Client;

  $client = new Client(apiKey: getenv('CONTEXT_DEV_API_KEY'));

  $monitor = $client->monitors->create(
      name: 'Acme pricing page',
      target: ['type' => 'page', 'url' => 'https://acme.com/pricing'],
      changeDetection: ['type' => 'exact'],
      schedule: ['type' => 'interval', 'frequency' => 6, 'unit' => 'hours'],
      webhook: ['url' => 'https://example.com/webhook'],
  );

  echo $monitor->id;
  ```
</CodeGroup>

<Badge color="green" icon="coins">Management calls are free; you pay per run: 1 credit for page & sitemap checks, 10 for semantic extract</Badge>

The monitor runs immediately after creation to capture its initial **baseline**: the snapshot every later run is compared against. The response includes the generated webhook signing secret; store it, you'll need it to [verify deliveries](#receive-webhooks):

```json sample response expandable theme={null}
{
  "mode": "web",
  "id": "mon_123",
  "name": "Acme pricing page",
  "target": {
    "type": "page",
    "url": "https://acme.com/pricing",
    "normalize_whitespace": true
  },
  "change_detection": { "type": "exact" },
  "schedule": { "type": "interval", "frequency": 6, "unit": "hours" },
  "webhook": {
    "url": "https://example.com/webhook",
    "secret": "whsec_8f3a...",
    "events": ["change.detected"]
  },
  "status": "active",
  "last_run_at": null,
  "last_change_at": null,
  "next_run_at": "2026-07-09T18:00:00Z",
  "last_error": null,
  "tags": [],
  "created_at": "2026-07-09T12:00:00Z",
  "updated_at": "2026-07-09T12:00:00Z"
}
```

### Request parameters

| Parameter          | Type      | Description                                                                                                                                                                                                                                                                                                        |
| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `name`             | string    | **Required.** Display name, 1–200 characters.                                                                                                                                                                                                                                                                      |
| `target`           | object    | **Required.** What to watch: a `page`, `sitemap`, or `extract` target. See [Pick a target](#pick-a-target).                                                                                                                                                                                                        |
| `change_detection` | object    | Optional. `{ "type": "exact" }` for literal diffs, or `{ "type": "semantic", "confidence_threshold": 0.75 }` for meaning-level changes. Defaults: `semantic` for `extract` targets, `exact` for `sitemap` targets, and — for `page` targets — `semantic` when `target.instructions` is set, otherwise `exact`.     |
| `schedule`         | object    | Optional. `{ "type": "interval", "frequency": 6, "unit": "hours" }`. Units: `minutes`, `hours`, `days`. The total interval must be between 10 minutes and 1 year. Defaults to once per day.                                                                                                                        |
| `webhook`          | object    | `{ "url": "https://...", "events": ["change.detected", "run.completed"] }` — where to POST webhook events. The API generates the signing `secret`; it can't be set by clients. `events` is optional and defaults to `["change.detected"]` — see [Choose which events to receive](#choose-which-events-to-receive). |
| `tags`             | string\[] | Up to 20 tags for grouping and filtering monitors and their changes.                                                                                                                                                                                                                                               |

## Pick a target

Each target type pairs with a supported detection strategy. Supported combinations: `page` + `exact`, `page` + `semantic`, `sitemap` + `exact`, and `extract` + `semantic`. Unsupported pairs (for example `sitemap` + `semantic`) are rejected with a `400`.

### Page: did this page's text change?

Watches a single URL and diffs the visible page text. Whitespace is normalized by default (`normalize_whitespace: true`).

```json theme={null}
{
  "target": { "type": "page", "url": "https://acme.com/pricing" },
  "change_detection": { "type": "exact" }
}
```

#### Page + semantic: alert only on changes you care about

Add `target.instructions` — a plain-language goal describing which page changes matter — and Context.dev judges each confirmed diff against that goal, so ticker widgets, "as of" timestamps, and rotating testimonials stay quiet while a real edit fires. When `instructions` is set and you omit `change_detection`, semantic detection is inferred; passing `change_detection: { type: "exact" }` alongside `instructions` is rejected, and semantic page detection without `instructions` is rejected too.

```json theme={null}
{
  "target": {
    "type": "page",
    "url": "https://acme.com/pricing",
    "instructions": "Report pricing or plan availability changes. Ignore counters, timestamps, testimonials, and navigation."
  }
}
```

Under the hood, a semantic page run compares the current page against the baseline, separates stable content from volatile regions (counters, timestamps, ticker text), re-observes the page in the same run to confirm the diff is real, and only then asks the judge whether the stable change matches your `instructions`. Small numeric drift on values you didn't ask about is ignored; watched numeric changes (for example a price moving from `$29` to `$49`) are promoted even when the delta is small.

Tune sensitivity with `change_detection.confidence_threshold` (0–1, default `0.75`) — raise it to only get notified about changes the judge is more certain matter.

### Sitemap: were URLs added or removed?

Watches a sitemap for URL additions and removals, ideal for catching new blog posts, product pages, or docs. URLs are normalized and scoped to the monitored site and its subdomains; on a detected difference the sitemap is re-fetched within the same run and only URLs both observations agree on are reported, suppressing transient crawl flaps.

```json theme={null}
{
  "target": {
    "type": "sitemap",
    "url": "https://acme.com/sitemap.xml",
    "include": ["/blog/*"],
    "exclude": ["/legal/*"],
    "max_urls": 5000
  },
  "change_detection": { "type": "exact" }
}
```

`include` / `exclude` accept URL path patterns, and `max_urls` caps tracking at up to 10,000 URLs (default 5,000).

### Extract: did the data I care about change?

Watches the *meaning* of a site, not its markup. A crawl guided by your `instructions` (and optional JSON `schema`, the same shape the [Extract API](/guides/extract-structured-data-from-websites) uses) selects up to `max_pages` relevant pages; each run re-checks exactly those pages and judges confirmed changes against your instructions, so a reworded paragraph doesn't fire, but a new pricing tier does.

The `schema` does three things: it steers which pages get selected for tracking, it gives the change judge extra context on which changes matter (alongside your `instructions`), and it defines the shape of the `data` snapshot on the monitor's [baseline](/api-reference/monitors/retrieve) (refreshed by the re-discovery crawl, at most about once a day). It is **not** a response format for alerts — change events and webhook payloads always contain diffs, summaries, and evidence excerpts, never data shaped by your schema. Skip it and a general summary + key-points schema is used.

```json theme={null}
{
  "target": {
    "type": "extract",
    "url": "https://acme.com",
    "instructions": "Extract every pricing plan with its monthly price and included limits.",
    "schema": {
      "type": "object",
      "properties": {
        "plans": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "price": { "type": "string" }
            }
          }
        }
      }
    },
    "max_pages": 10
  },
  "change_detection": { "type": "semantic", "confidence_threshold": 0.75 }
}
```

Raise `confidence_threshold` (0–1, default `0.75`) to only get notified about changes the judge is more certain matter. The tracked page set is refreshed by a periodic re-discovery crawl (at most about once a day).

## Receive webhooks

Context.dev delivers two webhook events. Subscribe to either or both by setting `webhook.events` when you create or update a monitor.

| Event             | Fires                                                                                                        | Use it when                                                                                                                                                         |
| ----------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `change.detected` | Only after runs that detect a change.                                                                        | You just want alerts.                                                                                                                                               |
| `run.completed`   | After **every** completed run, including runs that found no change. Embeds the change when one was detected. | You want a heartbeat that the monitor ran — for uptime dashboards, "last checked" timestamps, or downstream jobs that need to run whether or not something changed. |

Each subscribed event fires independently, so a change-run with both events subscribed produces two deliveries — one per event — with matching `data.change` records.

### Choose which events to receive

`webhook.events` is a list. When you create a webhook, omitting it defaults to `["change.detected"]` for backward compatibility. When you update an existing webhook, omitting `events` preserves its current subscription; send the list explicitly to change it.

```json theme={null}
{
  "webhook": {
    "url": "https://example.com/webhook",
    "events": ["change.detected", "run.completed"]
  }
}
```

### `change.detected` payload

Sent after a run detects a change. `data.change` is the full change record — the same one returned by `GET /monitors/changes/{change_id}`.

```json change.detected payload theme={null}
{
  "event": "change.detected",
  "id": "evt_123",
  "created_at": "2026-07-09T18:00:12Z",
  "data": {
    "change": {
      "mode": "web",
      "id": "chg_123",
      "monitor_id": "mon_123",
      "run_id": "run_123",
      "tags": ["pricing"],
      "target_type": "page",
      "change_detection_type": "exact",
      "title": "Acme pricing page changed",
      "summary": "The Starter plan price changed from $10 to $12.",
      "detected_at": "2026-07-09T18:00:10Z",
      "url": "https://acme.com/pricing",
      "diff": "- Starter: $10 per month\n+ Starter: $12 per month",
      "before_text_excerpt": "Starter: $10 per month",
      "after_text_excerpt": "Starter: $12 per month"
    }
  }
}
```

### `run.completed` payload

Sent after **every** completed run when your monitor subscribes to `run.completed`. `data.run` is a snapshot of the run at delivery time. `data.change` is the same full change record delivered by `change.detected`, or `null` when the run found no change (including baseline runs, where `baseline_created` is `true`).

```json run.completed payload — no change theme={null}
{
  "event": "run.completed",
  "id": "evt_456",
  "created_at": "2026-07-09T18:00:12Z",
  "data": {
    "run": {
      "id": "run_123",
      "monitor_id": "mon_123",
      "status": "completed",
      "run_type": "scheduled",
      "target_type": "page",
      "change_detection_type": "exact",
      "started_at": "2026-07-09T18:00:00Z",
      "completed_at": "2026-07-09T18:00:11Z",
      "change_detected": false,
      "change_id": null,
      "baseline_created": false
    },
    "change": null
  }
}
```

<Tip>
  `data.run` doesn't include `credits_charged` — billing settles after delivery. Fetch the run via `GET /monitors/{monitor_id}/runs` for the final billing record.
</Tip>

### Verify the signature

Every delivery includes an `X-Context-Signature: t=<unix>,v1=<hmac>` header, where the HMAC is SHA-256 over `"{t}.{rawRequestBody}"` keyed by your monitor's webhook `secret`. Recompute it with a constant-time compare and reject stale timestamps to prevent replays:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import crypto from "node:crypto";

  function verifyWebhook(rawBody: string, signatureHeader: string, secret: string): boolean {
    const parts = Object.fromEntries(
      signatureHeader.split(",").map((pair) => pair.split("=")),
    );
    const expected = crypto
      .createHmac("sha256", secret)
      .update(`${parts.t}.${rawBody}`)
      .digest();

    const isFresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
    const received = Buffer.from(parts.v1 ?? "", "hex");
    return (
      isFresh &&
      received.length === expected.length &&
      crypto.timingSafeEqual(expected, received)
    );
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import time

  def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
      parts = dict(pair.split("=", 1) for pair in signature_header.split(","))
      message = parts["t"].encode() + b"." + raw_body
      expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()

      is_fresh = abs(time.time() - int(parts["t"])) < 300
      return is_fresh and hmac.compare_digest(expected, parts["v1"])
  ```
</CodeGroup>

Every delivery also sets an `X-Context-Event` header (`change.detected` or `run.completed`) so you can route events without parsing the body first, and an `X-Context-Id` header that matches the payload's top-level `id`.

<Tip>
  For the full payload schemas, see the [`change.detected` reference](/api-reference/monitors/webhook-payload) and the [`run.completed` reference](/api-reference/monitors/webhook-run-completed). Type-specific change fields include text diffs for exact page monitors, added and removed URLs for sitemap monitors, and confidence plus evidence for semantic extract monitors.
</Tip>

### Inspect delivery status

Each monitor run records the outcome of every webhook it attempted, so you can debug misbehaving endpoints without wiring up your own logs. The run object returned by `GET /monitors/{monitor_id}/runs` (and `GET /monitors/runs`) includes a `webhook_deliveries` array — one entry per subscribed event that fired for that run:

```json theme={null}
{
  "id": "run_123",
  "monitor_id": "mon_123",
  "status": "completed",
  "change_detected": true,
  "change_id": "chg_123",
  "webhook_deliveries": [
    {
      "event_id": "evt_123",
      "event": "change.detected",
      "status": "delivered",
      "http_status": 200,
      "attempted_at": "2026-07-09T18:00:12Z",
      "error": null
    },
    {
      "event_id": "evt_456",
      "event": "run.completed",
      "status": "delivered",
      "http_status": 200,
      "attempted_at": "2026-07-09T18:00:12Z",
      "error": null
    }
  ]
}
```

`status` is one of:

| Status               | Meaning                                                                     |
| -------------------- | --------------------------------------------------------------------------- |
| `delivered`          | Your endpoint returned a 2xx response.                                      |
| `rejected`           | Your endpoint returned a non-2xx response; see `http_status` and `error`.   |
| `failed`             | No HTTP response was received (timeout, DNS, TLS, connection reset).        |
| `skipped_unsafe_url` | The webhook URL failed the public-endpoint safety check and was not called. |

`event_id` matches the `X-Context-Id` header on the delivery, so you can correlate a run with the exact request your server received. `webhook_deliveries` is omitted when no webhook was attempted, including runs that predate delivery tracking.

<Note>
  The legacy top-level `webhook_delivery` field is still returned for backward compatibility. It contains the `change.detected` attempt when present; otherwise it contains the run's sole attempted delivery, such as `run.completed` for a monitor subscribed only to that event. It is **deprecated** — new integrations should read `webhook_deliveries`.
</Note>

### Track consecutive delivery failures

Beyond per-run delivery status, the monitor object itself carries a rolling health signal: `webhook_failure` counts consecutive delivery-attempting runs with at least one failed delivery so you can surface a broken endpoint in your own dashboard. Runs that attempt no webhook leave the streak unchanged.

```json theme={null}
{
  "id": "mon_123",
  "webhook": {
    "url": "https://example.com/webhook",
    "events": ["change.detected", "run.completed"]
  },
  "webhook_failure": {
    "consecutive_failures": 3,
    "last_status": "rejected",
    "last_message": "Webhook endpoint returned HTTP 429.",
    "last_failed_at": "2026-07-10T09:12:44Z"
  }
}
```

* `consecutive_failures` — number of consecutive delivery-attempting runs whose webhooks did not fully succeed. When a run delivers multiple subscribed events, a single failure among them advances the counter by one.
* `last_status` — outcome of the most recent failed delivery: `rejected` (non-2xx response), `failed` (no HTTP response), or `skipped_unsafe_url` (the URL failed the public-endpoint safety check).
* `last_message` — human-readable description of the most recent failure.
* `last_failed_at` — ISO timestamp of the last failed attempt.

`webhook_failure` is `null` when deliveries are healthy or no webhook is configured. It clears after the next run that attempts at least one webhook and delivers every attempt successfully, and resets when you change or remove the webhook URL via `PATCH /monitors/{monitor_id}`.

### Delivery retries

Every webhook delivery is retried on network errors, `5xx` responses, and `429` rate-limit responses. When the receiver returns a numeric `Retry-After` header, Context.dev waits that long before the next attempt (capped at 10 seconds). If every attempt fails, the run's `webhook_deliveries[].status` and the monitor's `webhook_failure` record the outcome.

### Get notified when your endpoint breaks

Once `consecutive_failures` reaches **3**, Context.dev emails every user in your organization to flag the broken endpoint. You get one email per streak — a run that attempts at least one webhook and delivers every attempt successfully re-arms the notification for the next episode. When several monitors deliver to the same URL, the notification is coalesced: one email covers the endpoint for 24 hours instead of one per monitor.

## Read changes over the API

No webhook? Poll instead. List a monitor's detected changes (paginated via `cursor`, filterable by `since` / `until` / `tag`), then fetch full details for any change: text `diff` for page monitors, `added_urls` / `removed_urls` for sitemaps, and `evidence` with before/after snapshots plus `confidence` and `importance` for semantic monitors.

<CodeGroup>
  ```bash cURL theme={null}
  curl "https://api.context.dev/v1/monitors/mon_123/changes?limit=20" \
    -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"

  curl https://api.context.dev/v1/monitors/changes/chg_123 \
    -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"
  ```

  ```typescript TypeScript theme={null}
  const changes = await client.monitors.listChanges("mon_123", { limit: 20 });

  for (const change of changes.data) {
    const detail = await client.monitors.retrieveChange(change.id);
    console.log(detail.title, detail.diff);
  }
  ```

  ```python Python theme={null}
  changes = client.monitors.list_changes(monitor_id="mon_123", limit=20)

  for change in changes.data:
      detail = client.monitors.retrieve_change(change.id)
      print(detail.title, detail.diff)
  ```

  ```ruby Ruby theme={null}
  changes = client.monitors.list_changes("mon_123", limit: 20)

  changes.data.each do |change|
    detail = client.monitors.retrieve_change(change.id)
    puts detail.title
  end
  ```

  ```go Go theme={null}
  changes, err := client.Monitors.ListChanges(
      context.TODO(),
      "mon_123",
      contextdev.MonitorListChangesParams{},
  )
  if err != nil {
      panic(err)
  }

  for _, change := range changes.Data {
      detail, err := client.Monitors.GetChange(context.TODO(), change.ID)
      if err != nil {
          panic(err)
      }
      fmt.Println(detail.Title)
  }
  ```

  ```php PHP theme={null}
  <?php

  $changes = $client->monitors->listChanges('mon_123', limit: 20);

  foreach ($changes->data as $change) {
      $detail = $client->monitors->retrieveChange($change->id);
      echo $detail->title;
  }
  ```
</CodeGroup>

There's also an account-wide feed across all monitors, `GET /monitors/changes` (`client.monitors.listAccountChanges()`), and the same pair for run history: `GET /monitors/{monitor_id}/runs` and `GET /monitors/runs`.

## Run, pause, and manage

Trigger an off-schedule check (queued and processed asynchronously), pause and resume, or delete:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.context.dev/v1/monitors/mon_123/run \
    -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"

  curl -X PATCH https://api.context.dev/v1/monitors/mon_123 \
    -H "Authorization: Bearer $CONTEXT_DEV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "status": "paused" }'

  curl -X DELETE https://api.context.dev/v1/monitors/mon_123 \
    -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"
  ```

  ```typescript TypeScript theme={null}
  await client.monitors.run("mon_123");

  await client.monitors.update("mon_123", { status: "paused" });

  await client.monitors.delete("mon_123");
  ```

  ```python Python theme={null}
  client.monitors.run("mon_123")

  client.monitors.update(monitor_id="mon_123", status="paused")

  client.monitors.delete("mon_123")
  ```
</CodeGroup>

Two behaviors worth knowing:

* **Updating `target` or `change_detection` resets the baseline.** The next run re-captures it instead of reporting a spurious change. Unsupported target/detection combinations are rejected — see [Pick a target](#pick-a-target) for the allowed pairs.
* **Failures don't kill monitors.** A failed run flips `status` to `failed` (with `last_error` populated), but the monitor keeps running on schedule and flips back to `active` on the next success. Monitors auto-pause after repeated consecutive failures or insufficient-credit skips; resume by updating `status` to `active`.
* **A failed baseline run pauses the monitor immediately.** The baseline is the first run after you create the monitor or edit its `target` or `change_detection` — if that run fails, the configuration itself is probably wrong, so Context.dev sets `status` to `paused` and populates `last_error` on the first failure instead of burning retries. Fix the configuration (or leave it alone if the source was transiently down) and resume with `status: "active"` to clear `last_error` and re-run the baseline.
* **Empty, blocked, error, parked, or collapsed pages don't overwrite the baseline.** If a run's snapshot looks degraded — a bot-block interstitial, an error page, a parked domain, or a page that shrank to almost nothing — the run fails cleanly instead of poisoning the baseline with a bad snapshot and firing a spurious disappearance/recovery alert pair on the next healthy run.

## Pricing

Creating, listing, updating, and reading monitors, runs, and changes is **free**. You pay per run, priced like the equivalent public endpoint:

| Run type                                             | Credits per run |
| ---------------------------------------------------- | --------------- |
| Page check (`page` + `exact` or `page` + `semantic`) | 1               |
| Sitemap check (`sitemap` + `exact`)                  | 1               |
| Semantic extract check (`extract` + `semantic`)      | 10              |
| Skipped or failed runs                               | 0               |

Monitor spend appears on the [usage page](https://context.dev/dashboard) under the path `/v1/monitors/run`, and `GET /monitors/credit-usage` returns a per-monitor credit and run breakdown over any time window:

```bash cURL theme={null}
curl "https://api.context.dev/v1/monitors/credit-usage?since=2026-07-01T00:00:00Z" \
  -H "Authorization: Bearer $CONTEXT_DEV_API_KEY"
```

## Use cases

* **Competitor price tracking**: a `page` monitor on each competitor's pricing page, with the webhook posting diffs into Slack. Add `target.instructions` like "report pricing or plan availability changes, ignore counters and testimonials" to make it semantic and skip the cosmetic churn.
* **Content ops**: a `sitemap` monitor on a competitor's blog to catch new posts and landing pages the day they ship.
* **Positioning intelligence**: an `extract` + `semantic` monitor with instructions like "track packaging, pricing, and headline feature claims"; cosmetic rewording won't fire it.
* **Compliance watch**: an `extract` monitor over terms or privacy pages, alerting only when obligations meaningfully change.
* **Your own site**: a `sitemap` monitor as a tripwire for pages accidentally dropping out of your sitemap after a deploy.

## Next steps

<CardGroup cols={2}>
  <Card title="Monitors API Reference" icon="tower-broadcast" href="/api-reference/monitors/create">
    Every endpoint, parameter, and response schema.
  </Card>

  <Card title="Extract Structured Data" icon="brackets-curly" href="/guides/extract-structured-data-from-websites">
    The schema-driven extraction that powers extract monitors.
  </Card>

  <Card title="Best Practices" icon="list-check" href="/optimization/best-practices">
    Caching, error handling, and key hygiene.
  </Card>

  <Card title="Troubleshooting" icon="bug" href="/optimization/troubleshooting">
    Status codes, retry patterns, and common errors.
  </Card>
</CardGroup>
