# Watch a page for changes

`POST https://api.crawl4ai.com/monitors`

Watch a page and get told when it changes in the way you care about. Give the URL and a plain sentence; every real change that matches comes back as typed facts (what changed, from what, to what) with links to both page versions. Try it in the dashboard's [Playground](https://crawl4ai.com/dashboard/#playground), mode `watch`.


curl:

```bash
curl https://api.crawl4ai.com/monitors \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://competitor.com/pricing",
        "watch": "a plan gets more expensive, or a plan is removed",
        "every": "auto",
        "notify": { "webhook": "https://you.example/hook", "secret": "s3cr3t" } }'
```

## Body

| Name | Type | Default | Description |
|---|---|---|---|
| url | string | - | The page to watch. http or https, a public host. |
| watch | string | - | What to tell you about, in your words. Several sentences are fine: "a new article about OpenAI", "the product is back in stock", "a plan gets more expensive, or a plan is removed". We split it into conditions and show them back in the response. Empty = any real change on the page. |
| every | string | auto | `auto` (default): we check as often as the page really changes, at least once a day. Or a fixed interval when you must know within a set time: `4m`, `15m`, `1h`, `6h`, `1d`, `1w` (at least 4 minutes). |
| notify | object | - | `webhook` (a URL we POST each confirmed change to), `secret` (signs the POST, see below), `email`, `trigger` (`{"url", "token"}`: a URL that starts your agent, see [For agents](#for-agents)). All optional; without them, read the changes with the feed or the events call. |
| schema | object | - | Your own JSON shape, filled on every change next to the standard facts, as `custom`. A field the page does not show is `null`. |
| name | string | - | A label for you. |
| region | string | - | `eu`, `sin` or `us`: where the page is fetched from. Default: the region of the gate you called. |
| agent | string | - | Which of your agents owns this monitor, when several share one account (`pricing-bot`): each reads only its own changes. Up to 64 letters, digits, `.`, `_` or `-`. Set once, at creation. |

## Response

json:

```json
{
  "id": "mon_7f3a20c1e9b04d55", "created": true,
  "url": "https://competitor.com/pricing",
  "watch": "a plan gets more expensive, or a plan is removed",
  "conditions": { "any": [ { "id": "c1", "text": "the price of a plan goes up" },
                           { "id": "c2", "text": "a plan is removed" } ] },
  "every": "auto", "region": "eu", "health": "ok",
  "baseline": { "status": "pending" },
  "cost": { "per_check_credits": 1, "per_change_credits": 3 }
}
```



> **Note**
>
> The same `url` and `watch` return the existing monitor with `created: false` (with an `agent`, the same three). `baseline` becomes `{ "status": "ok", "version": "…" }` after the first check, within a minute. Read `conditions`: that is how we understood your sentence; call again with a clearer one if it is not what you meant.

## The other calls

| Call | Description |
|---|---|
| GET /monitors/changes?since=&agent=&limit=&wait= | The feed: the new changes of all your monitors in one list. See [For agents](#for-agents). |
| GET /monitors?agent= | Your monitors, newest first; `agent` keeps one agent's. |
| GET /monitors/{id} | One monitor. |
| PATCH /monitors/{id} | Change `watch`, `every`, `notify`, `schema`, `name`, or `paused: true\|false`. A paused monitor keeps its history and costs nothing. `agent` cannot change: delete the monitor and create it again. |
| DELETE /monitors/{id} | Remove it with its history. |
| GET /monitors/{id}/events?since=&limit= | The timeline, oldest first: every change with its facts, and health events. `since` is ISO 8601 or unix seconds; `limit` 1 to 500, default 100. |
| GET /monitors/{id}/versions/{version} | The page as we saw it at that version, as Markdown: the two `evidence` links of a change. |

## A change

json · one event, the same shape on the timeline and in the webhook:

```json
{
  "id": 4181, "event": "change", "monitor": "mon_7f3a20c1e9b04d55", "agent": "",
  "url": "https://competitor.com/pricing",
  "request": "a plan gets more expensive, or a plan is removed",
  "matched": ["c1"], "confidence": 0.97,
  "summary": "The Pro plan's price went from $49 / month to $59 / month.",
  "facts": [ { "kind": "changed", "entity": "Pro", "field": "price",
               "before": "$49 / month", "after": "$59 / month", "section": "Pricing > Pro" } ],
  "custom": null,
  "first_seen": "2026-09-26T12:05:32Z", "confirmed": true, "confirmed_at": "2026-09-26T12:35:41Z",
  "evidence": { "before": "https://api.crawl4ai.com/monitors/mon_7f3a…/versions/75af60efc867a0cb",
                "after":  "https://api.crawl4ai.com/monitors/mon_7f3a…/versions/e70482929f228688" },
  "cost_credits": 3,
  "delivery": { "webhook": { "status": 200, "attempts": 1, "at": "2026-09-26T12:35:42Z" }, "done": true }
}
```

| Field | Description |
|---|---|
| matched | The ids of the conditions that held. A change that matched nothing is on the timeline too, with `[]` and no cost, so you can widen the sentence. |
| request · confidence | The sentence the change was judged against, and how sure the judge was that it matches (0 to 1; `null` when the match rests on numbers only). |
| facts | One per thing that changed: `kind` is `added`, `removed` or `changed`; `entity` and `field` name it; `before` and `after` are the values as the page shows them, or `"not shown"` when the entity stays but the value is not visible. |
| first_seen · confirmed_at | A change is delivered 30 minutes after it was first seen, when the page has not gone back. The timeline shows it at once. |
| event: "health" | Instead of a change: `health` is `blocked` or `error` with a plain `message`, when the site refused three checks in a row; and `ok` when it answers again. |

## The webhook

One POST per confirmed change, the event above as the body, with the headers `x-c4-event: change`, `x-c4-monitor`, `x-c4-timestamp` (unix seconds) and, when you gave a `secret`, `x-c4-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + body)>`. Answer 2xx. Otherwise we try again after 5 and 30 minutes, then 2 and 12 hours, then stop; the timeline keeps the last status.

## For agents

An agent learns about a change in four ways. All four use the same events.

| Way | How it works |
|---|---|
| The feed | `GET /monitors/changes`: the new changes of all your monitors, oldest first, as `{"changes", "cursor", "more"}`. Reading marks them read; the next call starts after them. `agent` reads one agent's changes; `since` reads again from a cursor; `limit` 1 to 500, default 100. A matched change comes once when found (`"confirmed": false`, about a minute after the check) and once more when confirmed (`"confirmed": true`, about 30 minutes later). Unread changes stay 30 days. |
| Wait | `wait=60`: when nothing is new, the call waits up to 60 seconds and answers as soon as a change comes. Up to 5 waiting calls per account. |
| You have mail | Every answer to a call with your key (scrape, search, extract …) carries `x-c4-watch-changes: N` when N changes wait. Send `x-c4-agent: <name>` to count one agent's. In MCP, the tool answer ends with a line that says so. |
| Trigger | `notify.trigger = {"url", "token"}`: on a confirmed change we POST `{"text", "events"}` to the URL with `Authorization: Bearer <token>`, at most once every 2 minutes per URL, the changes of that time in one call. Made for a Claude Code routine's fire URL. The token is stored encrypted and shown back as `"stored"`. |

The MCP tools: `watch_page`, `watch_changes` (the feed), `list_changes` (one monitor), `stop_watching` (delete, or pause) and `resume_watching`.

## Credits

With `every: "auto"`: 1 credit per check, at most once every 15 minutes. With a fixed interval: 1 credit per check, once per `every`; under 1 hour, 2 credits per check. 3 credits per confirmed change that matched. A blocked or failed check, a change that matched nothing, and a paused monitor cost nothing. Up to 100 monitors per account.

## Errors

| Status | error | Meaning |
|---|---|---|
| 400 | url is not valid · url must start with http:// or https:// · url host is not public · every must be at least 4m · every must be auto, seconds, or 4m, 1h, 6h, 1d, 1w | The URL, a private or local host, or an interval under 4 minutes or unreadable. The `error` text says which. |
| 404 | no such monitor · no such version | Not on your account. |
| 429 | at most 100 monitors per account · at most 20 running monitors checked more often than every 1h per account · this site has too many monitors already | Your account's limits (100 monitors, of which 20 may run faster than hourly), or 500 on one host across all accounts. |
| 400 | agent must be up to 64 letters, digits, '.', '_' or '-' · the agent label is set when the monitor is created | A bad `agent`, or a PATCH that tries to change it. |
| 405 | read the feed with GET | A HEAD on the feed. |
| 503 | could not read the sentence, try again | The split of your sentence failed; call again. |

**Try it with your key**

Run this call in the playground and see the real answer, the time and the units.
[Open the playground](https://crawl4ai.com/#playground)



---
Source: https://crawl4ai.com/docs/watch/
