Watch a page for changes
https://api.crawl4ai.com/monitorsWatch 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, mode watch.
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
urlstring | The page to watch. http or https, a public host. |
watchstring | 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. |
everystring | Default auto
|
notifyobject |
|
schemaobject | Your own JSON shape, filled on every change next to the standard facts, as |
namestring | A label for you. |
regionstring |
|
agentstring | Which of your agents owns this monitor, when several share one account ( |
Response
{ "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 } }
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. |
| 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
{ "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. |
Run this call in the playground and see the real answer, the time and the units.
Next
Ready-made scrapers.
Prices from pricing pagesPOST /priceGive it websites, get their prices.
Something unclear or wrong on this page? Tell us.