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

# Monitor Websites for Changes

> Watch any page, sitemap, or extracted dataset for changes on a schedule. Get signed webhooks when pricing, content, or URLs actually change.

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 notifies you (via signed webhook or the API) only when something actually changes.

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), `sitemap` + `exact` (URL additions/removals), and `extract` + `semantic` (meaning-level changes judged against my instructions). Schedule is an interval between 10 minutes and 1 year.
  4. Store the returned `monitor.id` and `monitor.webhook.secret`. On each `change.detected` webhook delivery, 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..."
  },
  "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    | **Required.** `{ "type": "exact" }` for literal diffs, or `{ "type": "semantic", "confidence_threshold": 0.75 }` for meaning-level changes.                           |
| `schedule`         | object    | **Required.** `{ "type": "interval", "frequency": 6, "unit": "hours" }`. Units: `minutes`, `hours`, `days`. The total interval must be between 10 minutes and 1 year. |
| `webhook`          | object    | `{ "url": "https://..." }` to call when a change is detected. The API generates the signing `secret`; it can't be set by clients.                                     |
| `tags`             | string\[] | Up to 20 tags for grouping and filtering monitors and their changes.                                                                                                  |

## Pick a target

Each target type pairs with a specific detection strategy. Supported combinations: `page` + `exact`, `sitemap` + `exact`, and `extract` + `semantic`.

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

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

When a run detects a change, Context.dev POSTs a `change.detected` event to your webhook URL:

```json webhook 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",
      "target_type": "page",
      "change_detection_type": "exact",
      "title": "Acme pricing page changed",
      "summary": "The visible text on the page changed.",
      "detected_at": "2026-07-09T18:00:10Z",
      "url": "https://acme.com/pricing"
    }
  }
}
```

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>

<Tip>
  The webhook payload carries a lightweight change summary. Fetch the full diff, added/removed URLs, or semantic evidence with `GET /monitors/changes/{change_id}` using the `data.change.id` from the payload.
</Tip>

## 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.
* **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 only after repeated consecutive failures or insufficient-credit skips; resume by updating `status` to `active`.

## 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`)                   | 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.
* **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>
