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"
}'
New accounts include 300 free checks per month, no card required. Create an account and grab your key from the dashboard.

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_...
Keep keys on your server. Anyone with your key can spend your quota, so never put it in browser JavaScript or a public repository.

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.

PlanPriceRequests / minIn flightChecks / month
FreeFree101300
Starter$10/mo60310,000
Pro$50/mo3006100,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

StatusCodeWhenWhat 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

StatusCodeWhenWhat 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

StatusCodeWhenWhat 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.
A response without a JSON body comes from the proxies in front of the API, for example a 413 for a body far over the limit or a 502 while the API restarts. Retry the 5xx ones with backoff.

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 selector matches. Spacing doesn’t count, and ignore_selectors and ignore_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: true always does.
  • Visual. A screenshot (the whole page unless full_page is false) is shrunk to 400 pixels wide and compared pixel by pixel with the baseline’s. It’s a change when at least threshold percent 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), or failing (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.
Tools: diff_text, compare_page, check_now, watch_page, list_watches, update_watch, get_changes, stop_watching. Tools take URLs, not file uploads. Results are JSON, also returned as structured content.

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.

FieldTypeDescription
afterstringRequired. The later version.
beforestringRequired. The earlier version.
context?integerUnchanged lines shown around each change in the diff. Default 3.
ignore_patterns?arrayRegular expressions whose matches are left out of both texts, e.g. "Updated \\d+ minutes ago".
Example request body
{
  "after": "Pro plan: $59/month\nFree trial: 14 days\nSupport: email and chat",
  "before": "Pro plan: $49/month\nFree trial: 14 days\nSupport: email"
}
Example response · 200
{
  "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.

FieldTypeDescription
ignore_patterns?arrayRegular expressions whose matches are left out of both versions.
ignore_selectors?arrayCSS selectors of parts to leave out.
render?booleanRead the page in a browser with its scripts run. Default false.
selector?stringRead only what this CSS selector matches.
text?stringThe page's text from before; leave it out to just read the page.
urlstringRequired. The page to read (http or https).
Example request body
{
  "selector": "h1",
  "text": "The World Wide Web project",
  "url": "https://info.cern.ch/hypertext/WWW/TheProject.html"
}
Example response · 200
{
  "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.

FieldTypeDescription
monitor_idstringRequired. The monitor to check.
Example request body
{
  "monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}
Example response · 200
{
  "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.

FieldTypeDescription
confirm?booleanReport a change only when two checks in a row see it. Default false.
every_minutes?integerHow often to check, in minutes. Default 60.
full_page?booleanFor visual checks: the whole page, not just the first screen. Default true.
ignore_patterns?arrayRegular expressions whose matches don't count as changes.
ignore_selectors?arrayCSS selectors of parts to leave out (ads, tickers).
name?stringA name for the monitor.
render?booleanRead the text in a browser with the page's scripts run (visual checks always do). Default false.
selector?stringWatch only what this CSS selector matches.
threshold?numberFor visual checks: the percent of the page that must look different. Default 1.
urlstringRequired. The page to watch (http or https).
viewport_height?integerDefault 800.
viewport_width?integerDefault 1280.
watch?"text" | "visual" | "both"What counts as a change: the page's text, its look, or either. Default "text".
webhook_id?stringOne of the account's webhooks, to get monitor.changed events.
Example request body
{
  "every_minutes": 60,
  "name": "The first web page",
  "url": "https://info.cern.ch/hypertext/WWW/TheProject.html",
  "watch": "text"
}
Example response · 200
{
  "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.

FieldTypeDescription
limit?integerDefault 50.
offset?integerDefault 0.
status?"active" | "paused" | "paused_quota" | "failing"Only monitors with this status.
Example request body
{
  "limit": 20
}
Example response · 200
{
  "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.

FieldTypeDescription
confirm?boolean
every_minutes?integer
full_page?boolean
ignore_patterns?array
ignore_selectors?array
monitor_idstringRequired.
name?string | null
paused?booleantrue 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
Example request body
{
  "monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b",
  "paused": true
}
Example response · 200
{
  "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.

FieldTypeDescription
limit?integerDefault 10.
monitor_id?stringOnly this monitor's changes.
since?stringOnly changes found after this ISO 8601 date-time, e.g. 2026-10-31T09:00:00Z.
Example request body
{
  "limit": 5
}
Example response · 200
{
  "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.

FieldTypeDescription
monitor_idstringRequired.
Example request body
{
  "monitor_id": "7c3f1a2e-5b4d-4e8f-9a6b-2d1c0e9f8a7b"
}
Example response · 200
{
  "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_..."
Response 200
{
  "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 now failing and 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"}
Verify the signature

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);
});
Reply with any 2xx within 8 seconds. Anything else, including a redirect, a timeout or a connection error, is retried after 1, 2, 4, 8, 16 and 32 minutes, then 1 hour: 8 attempts over about two hours. Every attempt carries the same 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.