/api/v1/monitorsMonitors
A monitor is a standing question about one page. Contrie answers it now, answers it again on a schedule, and when the answer changes it records the before and after answers with a diff of the fields that moved — and tells your webhook. Each answer carries its metadata.evidenceStatus; it is signed when that says signed. Every accepted run costs what one extraction costs, changed or not; an unchanged page writes no event.
Parameters
| Field | Type | Description |
|---|---|---|
| url | string | The public page to watch. Required. |
| extract | string | What to watch, in plain language. Required unless schema is given. |
| schema | object | JSON Schema of the answer. |
| every | integer | Minutes between runs, 15 to 10,080. Default 60. |
| webhookUrl | string | https URL to POST changes to. The signing secret is returned once, in the create response. |
Create
curl -X POST https://www.contrie.com/api/v1/monitors \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $CONTRIE_API_KEY" \
-d '{
"url": "https://example.com/widget",
"extract": "price, availability",
"every": 60,
"webhookUrl": "https://hooks.example.com/contrie"
}'The response is 201 with monitor, baseline and, when you gave a webhook, webhookSecret (shown once). If the baseline is refused, the monitor is still created and active, and baseline is { "success": false, "data": null, "error": { "code", "message" } } — the full response when a rejected answer exists. A refusal costs 0 credits. Read baseline.error.code: PAGE_LIMIT_REACHED resumes when credits return; SOURCE_REFUSED usually keeps refusing, so stop that monitor — unless its message says the source is rate-limiting, which can recover on a later run. Past your plan’s cap, creation returns 429 PAGE_LIMIT_REACHED and nothing is created.
Current-slot status
Detail reads with GET /api/v1/monitors/:id or contrie_changes, including history selectors, include monitor.runStatus for one current original schedule slot. Every status object includes scheduledFor, the slot’s UTC millisecond due time.
scheduled: a future slot with no outstanding work.pending: work may still complete or publish. It promises no running process, charge, completion time or recovery.needs_review: includes the requiredreason, eitherresult_unconfirmedorresult_unavailable. These imply neither refund, failed extraction nor semantic correctness.unknown: evidence is ambiguous or insufficient. A recent stopped claim stays unknown because timestamps cannot prove which claim owns the request.
A stopped monitor returns runStatus: null only with complete positive evidence of no outstanding work for its current slot. This does not establish empty history or financial finality. Older and other monitor projections may omit the field; create/stop responses and webhooks keep their existing contracts. Active list reads with GET /api/v1/monitors or contrie_monitors include each monitor’s current-slot status and one statusRead for the account-scoped list. Empty lists are complete snapshots; an unavailable or over-capacity snapshot fails instead of returning a partial list.
{
"monitor": {
"id": "…",
"runStatus": {
"state": "needs_review",
"scheduledFor": "2026-10-01T09:00:00.000Z",
"reason": "result_unconfirmed"
}
},
"events": [],
"statusRead": { "coverage": "complete", "observedAt": "2026-10-02T05:00:00.000Z" }
}statusRead.observedAt is the database statement observation time, not detection or completion time. Its coverage: complete covers the monitor and current-slot status, including an honest unknown, rather than separately read historical events or fleet/history counts. A failed required snapshot returns SERVICE_UNAVAILABLE. Reading does not change monitoring or accounting state; ordinary authentication and rate-limit effects still apply.
A change event
{
"id": "…",
"kind": "change",
"at": "2026-09-08T14:15:03.201Z",
"diff": [ { "path": "$.price", "before": 24.99, "after": 19.99 } ],
"before": { "success": true, "data": { "price": 24.99, "availability": "in stock" }, "metadata": { "evidenceStatus": "signed", "evidence": { "…": "…" } } },
"after": { "success": true, "data": { "price": 19.99, "availability": "in stock" }, "metadata": { "evidenceStatus": "signed", "evidence": { "…": "…" } } }
}Events are baseline, change and error; GET /api/v1/monitors/:id returns the latest 20. When more exist, pass its nextCursor as ?before=EVENT_ID to read the next page. Recover one retained event with ?eventId=EVENT_ID, even after newer events or stopping the monitor. Use only one selector; unknown or foreign event references return 404. History reads cost no extraction credits. An error event’s after is the failure with its public error.code and message. A run that fails exactly like the latest event updates that event instead of adding one: its at becomes the newest failure, so a page that keeps refusing cannot push your changes out of those 20, and a failure after a recovery still shows as the newest event. Pages are a changing history, not a frozen snapshot; deduplicate by event ID when polling. The diff itself is not signed. Verify the before/after response packets and inspect their source fetch times and excerpts before acting: changed extracted values do not prove a fact changed at the source.
A change event may carry changeKind: material when a requested value appears to differ, or cosmetic when the same information appears written differently or only incidental details changed. It is advisory: an automated reading that is not verified, and misreads are not detected. Absent means no judgment. It never suppresses, delays or drops an event, and webhooks do not carry it.
Webhook
One POST per change, with this body:
{
"event": "monitor.change",
"eventId": "the retained change event UUID",
"monitorId": "…",
"url": "https://example.com/widget",
"changedAt": "2026-09-08T14:15:03.412Z",
"diff": [ { "path": "$.price", "before": 24.99, "after": 19.99 } ],
"before": { "success": true, "data": { "…": "…" }, "metadata": { "…": "…" } },
"after": { "success": true, "data": { "…": "…" }, "metadata": { "…": "…" } }
}Only changes are delivered; errors are not. It is signed the way Stripe signs: X-Contrie-Signature: t=<unix>,v1=<hex> where v1 is HMAC-SHA256 of t + "." + body with your monitor’s secret. Contrie enforces no timestamp tolerance: choose one and reject deliveries older than it. Delivery is one attempt; the event is stored either way and readable from the monitor. Keep eventId to recover this exact change with ?eventId=EVENT_ID or contrie_changes. Older webhook deliveries may omit it. Verify the raw body as received; do not remove added fields before checking the signature.
Credits per day
Credits per day = runs per day × credits per accepted run, and runs per day = 1,440 ÷ every. An accepted run costs what one extraction costs: at least 1 credit, at most the plan’s per-request cap (Free 3, Builder 5, Pro 17), rendering included. Refused and rejected runs cost 0 credits. A monitor keeps spending until you stop it.
| every | Runs per day | Credits per day at 1 a run |
|---|---|---|
| 15 | 96 | 96 |
| 60 (default) | 24 | 24 |
| 1440 | 1 | 1 |
One default monitor at 1 credit a run spends the Free plan’s 200 monthly credits in about 8 days; at every: 15, in about 2. Active monitors per plan: 3 Free, 25 Builder, 100 Pro.
List, read and stop
GET /api/v1/monitors lists your active monitors. GET /api/v1/monitors/:id returns one with its events. DELETE /api/v1/monitors/:id stops one; it and its history stay readable by id (active: false). From MCP: contrie_watch creates, contrie_monitors lists, contrie_changes reads a monitor’s events, and contrie_unwatch stops one. Stopping one that is already stopped differs: REST DELETE returns 404, while contrie_unwatch succeeds and returns the monitor unchanged.