API Reference
The Watchmint base URL is https://api-watchmint.plxi.us. Every endpoint accepts and returns JSON and is authenticated with your API key.
curl -X POST https://api-watchmint.plxi.us/v1/diff/text \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"after": "Pro plan: $59/month\nFree trial: 14 days\nSupport: email and chat",
"before": "Pro plan: $49/month\nFree trial: 14 days\nSupport: email"
}'Authentication
Send an X-API-Key header with every request. Keys are created and revoked in the dashboard; a key is shown once, when it is created, so store it somewhere safe.
X-API-Key: sk_live_...
Plans & limits
Limits apply per account, not per key: every key on the account (and teammates working in it) shares them. There is a per-minute rate, a cap on requests in flight at once, and a monthly quota. On the Free plan the quota resets on the 1st of each month (UTC); on paid plans it resets on your billing-period anniversary. Requests that fail validation are never charged.
| Plan | Price | Requests / min | In flight | Checks / month |
|---|---|---|---|---|
| Free | Free | 10 | 1 | 300 |
| Starter | $10/mo | 60 | 3 | 10,000 |
| Pro | $50/mo | 300 | 6 | 100,000 |
Errors
Errors return JSON with a stable error code, an optional message, and, on rate-limit and monthly-quota 429s, reset_at. A request that returns an error is never charged.
{
"error": "rate_limit_exceeded",
"reset_at": "2026-07-22T18:04:00Z"
}Fix the request
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | bad_request | The body is not valid JSON. | Send the body as JSON. |
| 400 | invalid_input | A body field failed the operation’s checks; message says which one and why. | Correct that field. |
| 401 | invalid_api_key | The X-API-Key header is missing, malformed, revoked or not a key we issued. | Check the key, or create a new one in the dashboard. |
| 403 | account_blocked | The account is suspended. | Contact [email protected]. |
| 404 | not_found | No endpoint at that path for that method: operations take POST, /v1/usage takes GET. | Check the path and the method. |
| 413 | payload_too_large | The body is larger than the endpoint takes: 1 MiB unless it says otherwise. | Send a smaller body. |
Wait, then retry
| Status | Code | When | What to do |
|---|---|---|---|
| 429 | rate_limit_exceeded | The per-minute request limit is reached (per account, across all its keys). Requests that then fail, for example on invalid input, count toward it too. | Retry after reset_at, the start of the next minute. |
| 429 | concurrency_limit_exceeded | The account has as many requests in flight as its plan allows. | Retry with backoff once one of them finishes. There is no reset_at. |
| 429 | monthly_quota_exceeded | The quota for this billing period is used up. | Retry after reset_at: the 1st of next month (UTC) on Free, the end of your billing period on paid plans. Or upgrade. |
Temporary: retry with backoff
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | body_read_failed | The connection failed while the body was being sent. | Retry. |
| 408 | request_body_timeout | The body took too long to arrive. | Retry, sending the whole body at once. |
| 500 | internal_error | Something failed on our side. | Retry with backoff. |
| 503 | capacity_unavailable | The API is handling as many requests as it can. | Retry with backoff. |
| 503 | database_unavailable limits_unavailable | Our database, or the store that counts rate limits, is unavailable. | Retry with backoff. |
| 503 | auth_busy | API key checks are busy. | Retry with backoff. |
| 503 | billing_period_stale billing_period_invalid | On a paid plan, the current billing period can’t be confirmed, for example because a renewal hasn’t been recorded yet. | Retry later. If it lasts, the account owner can open Billing in the dashboard to refresh it, or contact [email protected]. |
| 504 | request_timeout | The operation ran past the API’s time limit. | Retry with backoff; if it keeps happening, send smaller input. |
How checks work
A monitor (POST /v1/monitors/create) is checked every every_minutes: at most hourly on Free, every 5 minutes on Starter and every minute on Pro. Each check costs one check, text or visual. A check that can’t read the page is free.
- Baseline. The first check (due as soon as the monitor is made) is what later checks compare against. When a change is found it becomes the new baseline, so each change is reported once.
- Text. The page is fetched and its visible text read, one block per line (scripts, styles and hidden elements left out), or only what
selectormatches. Spacing doesn’t count, andignore_selectorsandignore_patterns(regular expressions, matched per line) are taken out first. A page built by script, with no text until it runs, is read in a browser;render: truealways does. - Visual. A screenshot (the whole page unless
full_pageis false) is shrunk to 400 pixels wide and compared pixel by pixel with the baseline’s. It’s a change when at leastthresholdpercent of the picture differs (1% by default). - Confirm. With
confirm: true, a change is reported only when two checks in a row see it: rotating banners and A/B tests stop alerting. - Status.
active,paused(by you),paused_quota(the month’s checks ran out: it tries again every 10 minutes and resumes by itself in the next period or after a top-up), orfailing(three failed checks in a row; it’s still checked and turns active again on the next good check). - What’s kept. The last 30 snapshots of each monitor and 90 days of changes. Screenshot links last 30 days. Bot checks and CAPTCHA walls aren’t bypassed; they count as failed checks.
Use with AI agents (MCP)
Watchmint is also an MCP server, so Claude and other AI agents can call its main operations as tools (listed below). Tool calls are charged exactly like API calls, to the same plan and quota, and failed calls are free.
https://api-watchmint.plxi.us/mcp
Claude (claude.ai and the desktop app): Settings → Connectors → Add custom connector, then paste the URL above. Claude asks you to sign in with your Plexsicity account; calls are then charged to your own account.
Claude Code, Cursor, VS Code and other clients: send an API key from the dashboard as a header. Calls are charged to the account that owns the key, so a team’s key spends the team’s quota. Clients that only take a bearer token can send Authorization: Bearer sk_live_… instead.
1. Connect your agent
claude mcp add --transport http watchmint https://api-watchmint.plxi.us/mcp \
--header "X-API-Key: sk_live_..."2. Tell it when to use the tools
Add this to the repository’s AGENTS.md or CLAUDE.md. Agents also get these rules from the server when they connect; the file makes them part of the project.
## Watchmint
- To be told when a page changes, call `watch_page` instead of re-reading the page yourself; read what changed later with `get_changes`.
- To see what changed on a page since you last read it, call `compare_page` with the `text` it returned last time.
POST/v1/diff/text
Compare two versions of a text, as a check compares a page: added and removed lines and words, and a unified diff. Costs 1 check.
| Field | Type | Description |
|---|---|---|
| after | string | Required. The later version. |
| before | string | Required. The earlier version. |
| context? | integer | Unchanged lines shown around each change in the diff. Default 3. |
| ignore_patterns? | array | Regular expressions whose matches are left out of both texts, e.g. "Updated \\d+ minutes ago". |
{
"after": "Pro plan: $59/month\nFree trial: 14 days\nSupport: email and chat",
"before": "Pro plan: $49/month\nFree trial: 14 days\nSupport: email"
}{
"added": [
"Pro plan: $59/month",
"Support: email and chat"
],
"added_lines": 2,
"added_words": 3,
"changed": true,
"credits": 1,
"diff": "--- before\n+++ after\n@@ -1,3 +1,3 @@\n-Pro plan: $49/month\n+Pro plan: $59/month\n Free trial: 14 days\n-Support: email\n+Support: email and chat\n",
"diff_truncated": false,
"removed": [
"Pro plan: $49/month",
"Support: email"
],
"removed_lines": 2,
"removed_words": 1,
"summary": "2 lines added, 2 removed"
}curl -X POST https://api-watchmint.plxi.us/v1/diff/text \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"after": "Pro plan: $59/month\nFree trial: 14 days\nSupport: email and chat",
"before": "Pro plan: $49/month\nFree trial: 14 days\nSupport: email"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: diff_text.
POST/v1/pages/compare
Read a web page now and compare it with text you kept from it before. Nothing is stored. Costs 1 check.
| Field | Type | Description |
|---|---|---|
| ignore_patterns? | array | Regular expressions whose matches are left out of both versions. |
| ignore_selectors? | array | CSS selectors of parts to leave out. |
| render? | boolean | Read the page in a browser with its scripts run. Default false. |
| selector? | string | Read only what this CSS selector matches. |
| text? | string | The page's text from before; leave it out to just read the page. |
| url | string | Required. The page to read (http or https). |
{
"selector": "h1",
"text": "The World Wide Web project",
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html"
}{
"added": [
"World Wide Web"
],
"added_lines": 1,
"added_words": 0,
"changed": true,
"credits": 1,
"diff": "--- before\n+++ after\n@@ -1 +1 @@\n-The World Wide Web project\n+World Wide Web\n",
"diff_truncated": false,
"final_url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
"removed": [
"The World Wide Web project"
],
"removed_lines": 1,
"removed_words": 2,
"rendered": false,
"status": 200,
"summary": "1 line added, 1 removed",
"text": "World Wide Web",
"text_truncated": false,
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html"
}curl -X POST https://api-watchmint.plxi.us/v1/pages/compare \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"selector": "h1",
"text": "The World Wide Web project",
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: compare_page.
POST/v1/monitors/check
Check one of your monitors now instead of waiting for its next scheduled check. Costs 1 check.
| Field | Type | Description |
|---|---|---|
| monitor_id | string | Required. The monitor to check. |
{
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}{
"baseline": false,
"change": null,
"changed": false,
"credits": 1,
"final_url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b",
"pending_confirmation": false,
"status": 200,
"summary": "No change",
"text_changed": false,
"visual_changed_pct": null
}curl -X POST https://api-watchmint.plxi.us/v1/monitors/check \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: check_now.
POST/v1/monitors/create
Start watching a page: Watchmint checks it on a schedule and tells you when it changes. Costs 0 checks.
| Field | Type | Description |
|---|---|---|
| confirm? | boolean | Report a change only when two checks in a row see it. Default false. |
| every_minutes? | integer | How often to check, in minutes. Default 60. |
| full_page? | boolean | For visual checks: the whole page, not just the first screen. Default true. |
| ignore_patterns? | array | Regular expressions whose matches don't count as changes. |
| ignore_selectors? | array | CSS selectors of parts to leave out (ads, tickers). |
| name? | string | A name for the monitor. |
| render? | boolean | Read the text in a browser with the page's scripts run (visual checks always do). Default false. |
| selector? | string | Watch only what this CSS selector matches. |
| threshold? | number | For visual checks: the percent of the page that must look different. Default 1. |
| url | string | Required. The page to watch (http or https). |
| viewport_height? | integer | Default 800. |
| viewport_width? | integer | Default 1280. |
| watch? | "text" | "visual" | "both" | What counts as a change: the page's text, its look, or either. Default "text". |
| webhook_id? | string | One of the account's webhooks, to get monitor.changed events. |
{
"every_minutes": 60,
"name": "The first web page",
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
"watch": "text"
}{
"credits": 0,
"monitor": {
"changes": 0,
"checks": 0,
"confirm": false,
"created_at": "2026-10-31T12:00:00Z",
"every_minutes": 60,
"full_page": true,
"id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b",
"ignore_patterns": [],
"ignore_selectors": [],
"last_change_at": null,
"last_check_at": null,
"last_error": null,
"name": "The first web page",
"next_check_at": "2026-10-31T12:00:00Z",
"render": false,
"selector": null,
"status": "active",
"threshold": 1,
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
"viewport_height": 800,
"viewport_width": 1280,
"watch": "text",
"webhook_id": null
}
}curl -X POST https://api-watchmint.plxi.us/v1/monitors/create \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"every_minutes": 60,
"name": "The first web page",
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
"watch": "text"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: watch_page.
POST/v1/monitors/list
The account's monitors, newest first, with their status and last check and change. Costs 0 checks.
| Field | Type | Description |
|---|---|---|
| limit? | integer | Default 50. |
| offset? | integer | Default 0. |
| status? | "active" | "paused" | "paused_quota" | "failing" | Only monitors with this status. |
{
"limit": 20
}{
"credits": 0,
"monitors": [],
"total": 0
}curl -X POST https://api-watchmint.plxi.us/v1/monitors/list \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"limit": 20
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: list_watches.
POST/v1/monitors/update
Change a monitor's settings, or pause and resume it. Costs 0 checks.
| Field | Type | Description |
|---|---|---|
| confirm? | boolean | |
| every_minutes? | integer | |
| full_page? | boolean | |
| ignore_patterns? | array | |
| ignore_selectors? | array | |
| monitor_id | string | Required. |
| name? | string | null | |
| paused? | boolean | true pauses the monitor; false resumes it. |
| render? | boolean | |
| selector? | string | null | |
| threshold? | number | |
| url? | string | |
| viewport_height? | integer | |
| viewport_width? | integer | |
| watch? | "text" | "visual" | "both" | |
| webhook_id? | string | null |
{
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b",
"paused": true
}{
"credits": 0,
"monitor": {
"every_minutes": 60,
"id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b",
"name": "The first web page",
"status": "paused",
"url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
"watch": "text"
}
}curl -X POST https://api-watchmint.plxi.us/v1/monitors/update \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b",
"paused": true
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: update_watch.
POST/v1/monitors/changes
The changes your monitors found, newest first, with what was added and removed. Costs 0 checks.
| Field | Type | Description |
|---|---|---|
| limit? | integer | Default 10. |
| monitor_id? | string | Only this monitor's changes. |
| since? | string | Only changes found after this ISO 8601 date-time, e.g. 2026-10-31T09:00:00Z. |
{
"limit": 5
}{
"changes": [],
"credits": 0
}curl -X POST https://api-watchmint.plxi.us/v1/monitors/changes \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"limit": 5
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: get_changes.
POST/v1/monitors/delete
Delete a monitor, with its snapshots and changes. Costs 0 checks.
| Field | Type | Description |
|---|---|---|
| monitor_id | string | Required. |
{
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}{
"credits": 0,
"deleted": true,
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}curl -X POST https://api-watchmint.plxi.us/v1/monitors/delete \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}'The response is the operation’s result plus credits, what the request was charged. Try it in the Playground. MCP tool: stop_watching.
GET/v1/usage
Your plan and usage this billing period, for alerting before you reach the quota.
curl https://api-watchmint.plxi.us/v1/usage \
-H "X-API-Key: sk_live_..."{
"plan": "starter",
"used": 487,
"limit": 10000,
"period_start": "2026-07-18T09:12:44Z",
"period_end": "2026-08-18T09:12:44Z"
}Webhooks
Add an HTTPS endpoint under Webhooks in the dashboard, then give its id to a monitor as webhook_id. Each endpoint gets its own signing secret (whsec_…), shown once when you add it. A monitor’s webhook gets:
monitor.changed: a check found a change, with the added and removed lines, a unified diff (up to 20,000 characters) and, for visual checks, the percent that changed and before, after and highlighted image links.monitor.error: the third failed check in a row (the page is down, answers with an error, or shows a bot check). The monitor is nowfailingand keeps being checked.monitor.recovered: the first good check after that.
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Watchmint-Webhooks/1
X-Watchmint-Event: monitor.changed
X-Watchmint-Event-ID: 6f1c2a7e-…:0d4e9b2c-…
X-Watchmint-Signature: sha256=3b5f0c9e41d7…
{"event": "monitor.changed", "created_at": "2026-10-31T09:00:04Z",
"data": {"monitor": {"id": "7c3f1a2e-…", "name": "Competitor pricing", "url": "https://competitor.example/pricing", "watch": "text"},
"check": {"status": 200, "final_url": "https://competitor.example/pricing"},
"change": {"id": "…", "kind": "text", "summary": "1 line added, 1 removed",
"added": ["Pro: $59/month"], "removed": ["Pro: $49/month"],
"diff": "--- before\n+++ after\n@@ -3 +3 @@\n-Pro: $49/month\n+Pro: $59/month\n",
"visual_changed_pct": null, "images": {"before": null, "after": null, "diff": null}}}}Every endpoint on the account also gets request.completed after each charged call, scheduled checks included, except the check whose event went to that endpoint:
{"data": {"ms": 4, "credits": 1, "endpoint": "diff.text"}, "event": "request.completed", "created_at": "2026-09-29T14:03:11.482107+00:00"}X-Watchmint-Signature is sha256=<hex>: the HMAC-SHA256 of the raw body, keyed with the endpoint’s full secret including whsec_. Compute it over the bytes you received, before parsing:
import crypto from 'node:crypto';
// rawBody: the request body exactly as received (Buffer or string). Verify it
// before parsing -- JSON that has been parsed and re-serialised won't match.
// secret: the endpoint's full signing secret, "whsec_" prefix included.
function verifySignature(rawBody, signatureHeader, secret) {
const expected =
'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const given = Buffer.from(String(signatureHeader || ''));
const want = Buffer.from(expected);
return given.length === want.length && crypto.timingSafeEqual(given, want);
}
// Express: take this route's body raw, verify, then parse.
app.post('/your/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifySignature(req.body, req.get('X-Watchmint-Signature'), process.env.WEBHOOK_SECRET);
if (!ok) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body);
res.sendStatus(200);
});X-Watchmint-Event-ID, so use it to ignore duplicates. Recent attempts are listed under Deliveries on the Webhooks page. Endpoints must be public HTTPS URLs.