---
title: Monitors
description: Create, list, get, update, pause, resume, reset and delete monitors — singly or in bulk — with the same validation and plan limits as the dashboard.
canonical: https://watchfor.io/docs/api/monitors
---

# Monitors

Create, list, get, update, pause, resume, reset and delete monitors — singly or in bulk — with the same validation and plan limits as the dashboard.

A **monitor** is the thing you configure: a type, a target, an interval,
the locations it runs from and type-specific `config`. Each execution is
a **check**; confirmed failures open **incidents**. This page covers the
monitor lifecycle; check history and uptime live on
[Checks, uptime & run now](/docs/api/checks-uptime), and the rules that
open incidents on [Alert rules](/docs/api/alert-rules).

| Method | Path | Scope | operationId |
| --- | --- | --- | --- |
| GET | `/v1/monitors` | read | `listMonitors` |
| POST | `/v1/monitors` | write | `createMonitor` |
| GET | `/v1/monitors/{id}` | read | `getMonitor` |
| PATCH | `/v1/monitors/{id}` | write | `updateMonitor` |
| DELETE | `/v1/monitors/{id}` | write | `deleteMonitor` |
| POST | `/v1/monitors/{id}/pause` | write | `pauseMonitor` |
| POST | `/v1/monitors/{id}/resume` | write | `resumeMonitor` |
| POST | `/v1/monitors/{id}/reset` | write | `resetMonitorHistory` |
| POST | `/v1/monitors/bulk` | write | `bulkMonitorAction` |

The monitor object:

```json
{
  "id": "0b0f6a41-…",
  "object": "monitor",
  "name": "Marketing site",
  "type": "http",
  "target": "https://example.com",
  "description": null,
  "interval_seconds": 60,
  "status": "up",
  "active": true,
  "scheduling_strategy": "roundrobin",
  "locations": ["9f31c2ab-…"],
  "tags": ["prod", "web"],
  "config": { "method": "GET", "followRedirects": true },
  "maintenance": false,
  "status_reason": "ok",
  "status_since": null,
  "current_incident": null,
  "alerts_muted": false,
  "alerts_muted_until": null,
  "has_notification_route": true,
  "has_probe_data": true,
  "last_check": {
    "at": "2026-09-22T10:00:00.000Z",
    "success": true,
    "duration_ms": 212,
    "location": "Stockholm",
    "error": null,
    "status_code": 200
  },
  "next_check_estimate_at": "2026-09-22T10:01:00.000Z",
  "notification_groups": [ { "id": "3c1f…", "name": "On-call" } ],
  "locations_detail": [
    { "id": "9f31c2ab-…", "name": "Stockholm", "city": "Stockholm", "region": "eu-north", "country_code": "SE" }
  ],
  "latest": null,
  "created_at": "2026-08-01T09:12:00.000Z",
  "updated_at": "2026-08-20T14:03:11.000Z",
  "history_reset_at": null,
  "available_metrics": [ { "metric": "status_code", "label": "HTTP status code", "…": "…" } ]
}
```

- `status` is `up`, `degraded`, `down`, `paused` or `unknown`. It is the
  **effective** status, computed by the same rule as the dashboard: a
  monitor with an open **Critical** incident (firing or acknowledged) is
  `down` even if its stored status has not refreshed; an open **Warning**
  incident, or a failing check with no confirmed incident (the latest check
  failed, or any alert rule — status code, response time, content,
  assertions — is failing while its confirmation is in progress), is
  `degraded`; `paused` reflects operator intent; a new monitor that has not
  reported yet is `unknown` (an open incident overrides it). The API refreshes
  the status from the monitor's latest check on every read, exactly as the
  dashboard does, so a healthy monitor is `up` even if nobody has opened the
  dashboard since it was created — but a check older than the monitor's last
  **resume** or **history reset** is never used for that refresh, so a
  resumed monitor stays `unknown` until a genuinely new result lands, here
  and on every dashboard surface (the dashboard keeps showing its earlier
  history with a **Resuming** marker meanwhile). **Down** always
  means a failure confirmed from several locations — a single failed check
  is `degraded`, never `down`. See [core concepts](/docs/core-concepts#monitor-status).
- `active` is `false` while paused.
- `scheduling_strategy` is `roundrobin` (one location per check, rotating),
  `random` (one location per check, picked at random) or `parallel` (all
  selected locations every check). `roundrobin` and `random` are on every
  plan; `parallel` is plan-gated, and plans cap how many monitors may use it.
- `maintenance` is `true` while an active
  [maintenance window](/docs/api/maintenance-windows) covers the monitor.
  That is planned work, not an outage: alerts are suppressed and the time
  is excluded from uptime per the window's settings, but `status` is
  **not** neutralised — a monitor that is down during its maintenance still
  reads `down`. Right after a write (create, pause, resume) the field
  reports `false`; read the monitor back for the live value.
- `history_reset_at` is set by [reset history](#reset-history): uptime
  windows start there instead of at `created_at`. `null` otherwise.
- `ping_url` is present on `heartbeat` monitors only — the full URL your
  job must call (`target` holds just the relative `/p/<token>` path).
- `config` holds the type-specific settings, with the same keys the
  dashboard uses. Secret values (passwords, private keys, bearer tokens,
  request bodies, `Authorization`/token/cookie-like request headers) are
  returned as `"[redacted]"` — the API never exposes stored credentials.
  On create and on update, `config` is **validated against the monitor
  type**: a wrong value type, an unknown enum value or an out-of-range
  number returns `400` naming the key (`config.method: …`,
  `config.timeoutSeconds: …`), and keys the type does not use are dropped
  rather than stored. The [catalog](/docs/api/monitor-types) lists every
  accepted key per type; `ipVersion` accepts `any` (auto, prefer IPv4),
  `ip4` or `ip6`.
- `available_metrics` (only on `GET /v1/monitors/{id}`) lists the alert
  metrics valid for this monitor's type — exactly what
  [`POST …/alert-rules`](/docs/api/alert-rules) accepts.

### Why it is what it is

The fields below say what the monitor page says next to the status
badge. They are computed from the same sources as the dashboard and are
present on `GET /v1/monitors/{id}`, on `GET /v1/monitors` items (the first
group only) and on the `POST` / `PATCH` responses; the pause and resume
responses omit them.

| Field | On | Meaning |
| --- | --- | --- |
| `status_reason` | list + detail | Why `status` is what it is: `critical_incident` (a confirmed critical incident is open — the only way to be `down`), `warning_incident` (an open warning incident), `failed_check` (a check is failing — the latest check failed or an alert rule such as status code or response time is failing — not yet confirmed: `degraded`), `paused`, `no_data` (`unknown`: nothing reported yet), `ok` |
| `status_since` | list + detail | When the open incident behind a `critical_incident` / `warning_incident` started; `null` otherwise |
| `current_incident` | list + detail | The open (firing or acknowledged) incident, or `null`: `{ id, severity, status, started_at, message }`, with `message` humanised exactly as [`GET /v1/incidents`](/docs/api/incidents) returns it. When several are open, the critical one, then the earliest started |
| `alerts_muted`, `alerts_muted_until` | list + detail | The dashboard's **Mute**: notifications are suppressed until `alerts_muted_until` while incidents keep opening and closing. Unlike pause, checks continue |
| `has_notification_route` | list + detail | `true` when at least one alert rule is routed to a contact group or the monitor has its own notification groups. `false` is the dashboard's "silent monitor" warning: incidents open but nobody is told |
| `alerting_state` | detail | What the alerting engine sees right now, or `null` when it reports nothing (every rule healthy, or no answer in time): `{ alerted, recovering, failing, failing_since, confirming }`. `alerted` — a rule's incident is still open in the engine; while `alerted` and `recovering` is `null`, the rule behind the incident is **still failing**. `recovering: { count, threshold }` — healthy checks since it fired, of how many close it ("Recovering 1/3"). `failing` / `confirming: { count, threshold }` — a rule failing below its confirmation threshold, no incident yet ("Confirming 2/3"); `failing_since` is that streak's first failed check |
| `has_probe_data` | detail | The monitor has produced at least one check (within a year). While `false`, uptime and SLA figures are meaningless (the dashboard shows "—") |
| `last_check` | detail | The most recent check, or `null`: `{ at, success, duration_ms, location, error, status_code? }`. `location` is the location's name (the prober's name when it is not a listed location); `status_code` is present for `http` and `api` monitors. Best effort — if the analytics store cannot be reached the field is `null`, the request still succeeds |
| `next_check_estimate_at` | detail | `last_check.at` + `interval_seconds` — an estimate; the scheduler owns the real deadline, and it can sit in the past when checks have stalled. `null` for `heartbeat` (the job pings you), paused monitors and monitors without a check |
| `notification_groups` | detail | The monitor's own notification groups resolved to `{ id, name }` (from `config.notificationGroupIds`, in config order); rules' contact groups are on [`GET …/alert-rules`](/docs/api/alert-rules) |
| `locations_detail` | detail | The `locations` ids resolved to `{ id, name, city, region, country_code }`, in the monitor's order — the same objects as [`GET /v1/locations`](/docs/api/locations) minus `country` |
| `latest` | detail | A type-aware summary of the page's top card, or `null`. `ssl`: `{ days_until_expiry, not_after, issuer, protocol, valid }`; `rdap` (domain): `{ expires_at, days_until_expiry, registrar, nameservers }`; `heartbeat`: `{ last_ping_at, next_expected_at, missed, last_exit_code }` — `last_ping_at` is the last finish/plain ping (starts and scheduler-fired misses do not count), `next_expected_at` is `last_ping_at` + period for period schedules and `null` for cron schedules, `missed` is `true` while the latest event is a miss, `last_exit_code` is `null` when the job did not report one. Every other type returns `null`; the per-type figures live on [checks](/docs/api/checks-uptime) |

## List monitors

`GET /v1/monitors` — newest first, cursor-paginated.

| Query | Meaning |
| --- | --- |
| `limit` | 1–100, default 50; anything else (`0`, `-1`, `abc`, `500`) returns `400` |
| `cursor` | The previous page's `next_cursor` — an opaque position (creation time + id), so the next page is stable even if the monitor the cursor pointed at has since been deleted. Cursors issued before September 2026 keep working |
| `sort` | Not supported — returns `400`; the list is always newest first |
| `type` | One monitor type (`http`, `ssl`, `heartbeat`, …); an unknown value returns `400` listing the valid types |
| `status` | `up`, `degraded`, `down`, `paused` or `unknown` — matched on effective status, so `?status=down` agrees with [`GET /v1/summary`](/docs/api/summary) |
| `tag` | Monitors carrying this tag |
| `q` | Case-insensitive substring search over name and target |

List items are **slim** — `id`, `object`, `name`, `type`, `target`,
`status`, `maintenance`, `interval_seconds`, `tags`, plus the triage
flags `status_reason`, `status_since`, `current_incident`,
`alerts_muted`, `alerts_muted_until` and `has_notification_route` (see
[why it is what it is](#why-it-is-what-it-is)) — so large organizations
page quickly and AI-agent tool results stay small. The full object comes
from `GET /v1/monitors/{id}`.

```bash
curl "https://watchfor.io/api/v1/monitors?status=down&limit=20" \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

```json
{
  "object": "list",
  "data": [
    { "id": "0b0f6a41-…", "object": "monitor", "name": "Marketing site", "type": "http",
      "target": "https://example.com", "status": "down", "maintenance": false,
      "status_reason": "critical_incident", "status_since": "2026-09-22T02:25:07.000Z",
      "current_incident": { "id": 1891, "severity": "critical", "status": "firing",
        "started_at": "2026-09-22T02:25:07.000Z",
        "message": "Received an unexpected HTTP Status Code (expected: 2xx, 3xx)" },
      "alerts_muted": false, "alerts_muted_until": null, "has_notification_route": true,
      "interval_seconds": 60, "tags": ["prod", "web"] }
  ],
  "has_more": true,
  "next_cursor": "MGIwZjZh…"
}
```

## Create a monitor

`POST /v1/monitors` — requires `write` scope. Runs the exact same path
as the dashboard: your plan's limits apply (monitor slots, per-type
caps, minimum interval, number of locations and regions, scheduling
strategy), checks start immediately, and the
[default alert rules](/docs/api/monitor-types) for the type are seeded.

| Field | Required | Notes |
| --- | --- | --- |
| `name` | yes | 1–50 characters |
| `type` | yes | One of the 30 types — see the [catalog](/docs/api/monitor-types). `playwright` = browser checks (scripted Playwright journeys: login, checkout, search); `browser` = Core Web Vitals (a Lighthouse audit of one page). |
| `target` | yes | Format depends on the type (URL, host, `host:port`, domain); up to 2048 characters; must be a publicly reachable host (see below). For `heartbeat` the ping URL is server-assigned — send any placeholder |
| `interval` | yes | Seconds; must meet the plan's minimum for that type (see `min_interval_seconds_by_type` on [`GET /v1/plan`](/docs/api/plan)) |
| `locations` | all types except `heartbeat` | Array of location ids from [`GET /v1/locations`](/docs/api/locations); at least one, up to 200; how many may be used is plan-gated. A `heartbeat` monitor must not send any (a non-empty list is `400`). Optional when `cloneFromMonitorId` is set |
| `schedulingStrategy` | no | `roundrobin` (default), `random` or `parallel`; `parallel` is plan-gated |
| `tags` | no | Up to 10; each 1–32 chars, lowercase letters, digits, `-` and `:` |
| `description` | no | Up to 500 characters; `null` clears it |
| `config` | no | Type-specific keys — see the [catalog](/docs/api/monitor-types); validated per type, unknown keys dropped |
| `cloneFromMonitorId` | no | Copy rules and config from an existing monitor of the same type — see [Clone a monitor](#clone-a-monitor) |
| `isPaused` | no | `true` creates the monitor without starting checks |

```bash
curl -X POST https://watchfor.io/api/v1/monitors \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Idempotency-Key: create-marketing-site" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Marketing site",
    "type": "http",
    "target": "https://example.com",
    "interval": 60,
    "locations": ["9f31c2ab-…"],
    "tags": ["prod", "web"]
  }'
```

Returns `201` with the created monitor. Plan-limit violations return
`403 forbidden` with a message naming the limit; validation failures
return `400 invalid_request` naming the field. Send an
[`Idempotency-Key`](/docs/api/errors#idempotency--safe-retries) so a
retried request cannot create two monitors.

**Targets must be publicly reachable.** Loopback, private (RFC 1918),
link-local, unique-local and cloud-metadata addresses — in any spelling,
including decimal or hex IPv4 and IPv4-mapped IPv6 — and internal
hostnames such as `localhost`, `*.internal`, `*.local`, `*.lan` and
`*.home.arpa` are rejected with `400` ("… — WatchFor probes only reach
public hosts") for every type that connects to its target. DNS, domain
expiry, blacklist, email-policy, breach and heartbeat monitors are exempt
because they never dial the target. The same rule applies when a target
is changed with PATCH. Hosts whose apex is itself a Public Suffix List
entry (`httpbin.org`, `<app>.herokuapp.com`, `<user>.github.io`) are
accepted; unknown top-level domains are not.

**Config that implies an alert rule creates it.** When the config carries
a value that only means something with a rule — DNS `expectedValues`,
CDN `expectedProvider`, CMS `expectedCms`, TCP/UDP `expectMatchRegexp`,
WebSocket `expectPattern`, MCP `expectedProtocolVersion`, HTTP
`failIfBodyMatchesRegexp` / `failIfBodyNotMatchesRegexp` — the matching
preset rule is created with the monitor (honouring `confirmationThreshold`
and `durationWindow` if you set them), so the check actually alerts. The
dashboard's create modal behaves the same way.

For `heartbeat` monitors pass `config.scheduleMode` (`period` with
`periodSeconds`, or `cron` with a five-field `cronExpression` and
`timezone`) and `graceSeconds`; the response carries `ping_url`, the
full URL your job should call. `cronExpression` is validated on save with
the same rules the scheduler uses: five fields (minute, hour, day of
month, month, day of week), `*`, `?`, lists, ranges, steps (`*/5`,
`1-30/10`) and `JAN`–`DEC` / `SUN`–`SAT` names. Descriptors such as
`@daily`, a seconds field and `7` for Sunday (use `0`) are rejected with
`400 config.cronExpression: …`; `timezone` must be a valid IANA name
(`Europe/Vilnius`, `UTC`). See [heartbeat monitoring](/docs/monitors/heartbeat).

## Clone a monitor

Add `cloneFromMonitorId` to `POST /v1/monitors` to copy an existing
monitor of the **same type** in your organization: its alert rules
(thresholds, severity, contact group, enabled state) and its whole
`config` are copied, and `type`, `target`, `interval`, `locations`,
`schedulingStrategy` and `tags` default to the source's values when you
omit them — so `{ "name": "…", "cloneFromMonitorId": "…" }` is a complete
request. Anything you do send wins over the source. An unknown or foreign
id returns `404`; a `type` that differs from the source returns `400`.
The same clone is available in the dashboard (**⋯ → Clone**) and as the
`cloneFromMonitorId` argument of the MCP `create_monitor` tool.

```bash
curl -X POST https://watchfor.io/api/v1/monitors \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Marketing site (EU)", "cloneFromMonitorId": "0b0f6a41-…", "locations": ["7c2e9b10-…"] }'
```

## Get a monitor

`GET /v1/monitors/{id}` — the full object including `config`,
`locations` (and `locations_detail`), timestamps, `available_metrics`,
the open incident and mute state, `last_check`, `next_check_estimate_at`
and the type-aware `latest` block — see
[why it is what it is](#why-it-is-what-it-is).

```bash
curl https://watchfor.io/api/v1/monitors/0b0f6a41-… \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

## Update a monitor

`PATCH /v1/monitors/{id}` — requires `write` scope. Send only the fields
you want to change: `name`, `target`, `description`, `interval`,
`schedulingStrategy`, `locations`, `tags`, `config`. The monitor `type`
is fixed at creation, and `status` is not writable — use
[pause](#pause-a-monitor) / [resume](#resume-a-monitor). Any other
top-level field is a `400` that names it — for example
`interval_seconds`, the name the monitor is *read* with, is refused with
a pointer to `interval` rather than silently ignored. The same plan
gates apply as on create; changes are recorded in the
[activity log](/docs/api/activity).

```bash
curl -X PATCH https://watchfor.io/api/v1/monitors/0b0f6a41-… \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "interval": 300, "tags": ["prod", "web", "critical"] }'
```

**Config merge rules.** `config` is a true partial update. Every key you
omit is **kept** as it is — authentication, request headers, transport
mode, IP version, body, the alerting-owned keys, all of them. A key sent
as `null` is **removed**. Arrays and nested objects you do send replace
the stored value whole (send the complete `requestHeaders` list, not the
one header you are changing). The merged result is then validated against
the monitor type exactly like a create, so a bad value is `400` and the
monitor is left untouched. `description: null` clears the description.
The dashboard's Settings form is different by design: it saves the whole
config it shows.

```bash
curl -X PATCH https://watchfor.io/api/v1/monitors/0b0f6a41-… \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "config": { "timeoutSeconds": 9, "requestHeaders": null } }'
```

Shrinking `locations` clamps any alert rule whose `minFailedProbes` is
now higher than the location count, and records the clamp in the
[activity log](/docs/api/activity) — see [Alert rules](/docs/api/alert-rules).

**A check right after a connection change.** When the update changes how
the monitor connects — the `target`, or a config key the probe uses (port,
credentials, TLS, request headers or body, record type, script, pages…) —
WatchFor dispatches **one immediate check** with the new settings, so the
next result reflects your change within seconds instead of after a full
interval. The response says so with `"check_queued": true`. This check does
not count against the plan's [Run now](/docs/api/checks-uptime#run-a-check-now) budget; it is
capped at 10 per monitor per hour so a script that PATCHes in a loop cannot
turn saves into a stream of checks. Alert-rule changes, name, tags,
description, interval and other cosmetic edits return
`"check_queued": false`, and so do paused, heartbeat and host monitors.

### Credentials (write-only)

Config fields marked **secret** in the [catalog](/docs/api/monitor-types)
are write-only. The mailbox and SMTP passwords of `imap`, `pop3`, `smtp`
and `email_roundtrip` monitors (`password`, `smtpPassword`,
`receivePassword`), the `ftp`/`sftp` `password` and `privateKey`, the
`http`/`api`/`mcp` `authPassword` and `bearerToken`, and the values of
credential request headers (`Authorization`, `Proxy-Authorization`,
`Cookie`, `X-API-Key`, names with *token*, *secret* or *key*) are stored
encrypted, bound to the monitor, and never returned — every read (REST, MCP,
A2A) shows `"[redacted]"`, the activity log records only that one changed,
and check results never contain them.

- **Keep:** omit the key, send `""`, or send back the `"[redacted]"` a read
  returned (any case) — a GET → edit → PATCH round trip keeps the password.
- **Remove:** send `null` (an empty `username` drops the password too).
- **Replace:** send the new value; it is encrypted before it is stored.
- **Re-enter when the destination changes:** a kept password never follows
  the monitor somewhere it was not typed for. Changing the server (`target`,
  or the round trip's `receiveHost`), port, TLS mode or login, or turning
  `verifyTls` off, without sending the password in the same request returns
  `400` `config.password: Re-enter the password: the server or login
  changed` (`config.smtpPassword` / `config.receivePassword` for the round
  trip's legs). Clones follow the same rule.
- An encrypted value copied from another monitor is refused (`400`).
- **HTTP, API and MCP:** headers are matched by name — send the header with
  `"[redacted]"` (or `""`) to keep its stored value; a mask on a header with
  nothing stored is a `400`. The destination is the URL's host, port and
  scheme (plus the basic-auth username for `authPassword`).
- **Variables instead of literals:** `{{NAME}}` in `authUsername`,
  `authPassword`, `bearerToken`, a header value or `requestBody` references an
  [organisation variable or secret](/docs/monitors/browser-checks/variables) and is stored as written
  (reads return it unmasked). An unknown name is a `400` on the field.

```bash
# Move an IMAP monitor to the STARTTLS port: the password goes in the same PATCH
curl -X PATCH https://watchfor.io/api/v1/monitors/0b0f6a41-… \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "config": { "port": 143, "tlsMode": "starttls", "password": "app-password" } }'
```

Mail-type rules worth knowing: logins are never sent unencrypted
(`tlsMode: "none"` with a `username` is `400`), SMTP logins use 587 or 465
(port 25 with a `username` is `400`), and an `email_roundtrip` monitor checks
at most every 5 minutes, sends only to the mailbox it reads (`toAddress` at
the receiving login's domain) and from the sending login's domain.

## Pause a monitor

`POST /v1/monitors/{id}/pause` — requires `write` scope. Stops checks,
closes the monitor's open incidents and **resets its alerting counters**,
so resuming starts clean: after a resume, a new incident needs the full
confirmation threshold again, counted from the first failure after the
resume. Idempotent: pausing an already-paused monitor returns `200` with
the current object.

```bash
curl -X POST https://watchfor.io/api/v1/monitors/0b0f6a41-…/pause \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

Returns the monitor with `"status": "paused"` and `"active": false`.

## Resume a monitor

`POST /v1/monitors/{id}/resume` — requires `write` scope. Restarts
checks; the monitor reports `unknown` (never a stale `up`) until the
first result arrives, and its failure counters start from zero.
Idempotent: resuming an active monitor returns `200` with the current
object.

```bash
curl -X POST https://watchfor.io/api/v1/monitors/0b0f6a41-…/resume \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

## Reset history

`POST /v1/monitors/{id}/reset` — requires `write` scope. Deletes
everything the monitor has ever recorded and starts its statistics over
from now: check and latency history, incidents with their timelines and
notification history, internal notes, post-mortems and the incidents
auto-published to status pages; the failure counters behind its alert
rules are cleared too. The monitor itself is untouched — same `id`,
`config`, alert rules, tags, locations and status-page membership — and
checks continue; it reports `unknown` until the next result arrives.
Uptime is measured from the reset (the dashboard says *since reset*).
This cannot be undone — the dashboard equivalent (owners and admins,
**⋯ → Reset history**) asks you to type the monitor's name.

```bash
curl -X POST https://watchfor.io/api/v1/monitors/0b0f6a41-…/reset \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Idempotency-Key: reset-marketing-site-2026-09-04"
```

```json
{
  "object": "monitor_reset",
  "monitor_id": "0b0f6a41-…",
  "reset_at": "2026-09-04T09:12:00.000Z",
  "incidents_deleted": 14,
  "postmortems_deleted": 1,
  "comments_deleted": 6,
  "status_page_incidents_deleted": 9,
  "check_history_deleted": true,
  "warnings": []
}
```

| Field | Meaning |
| --- | --- |
| `reset_at` | The moment uptime starts counting again — the same clamp as a monitor's creation time |
| `incidents_deleted` | Incidents removed, with their timelines and delivery history |
| `postmortems_deleted`, `comments_deleted` | Post-mortems and internal notes removed with them |
| `status_page_incidents_deleted` | Auto-published status-page incidents removed |
| `check_history_deleted` | `true` when check and latency history was deleted; `false` if there was none, or that step failed (see `warnings`) |
| `warnings` | Short identifiers of best-effort steps that did not complete; empty on a clean reset. The reset itself always takes effect — `reset_at` is set and uptime starts over — but what a failed step owns (for example old check history) may linger. Call the endpoint again to clear it |

Returns `200`. Send an
[`Idempotency-Key`](/docs/api/errors#idempotency--safe-retries) so a
retried request replays this response instead of moving `reset_at` a
second time. The reset is recorded in the [activity log](/docs/api/activity).

## Page integrity baseline

Page integrity monitors compare every check with an **accepted
baseline** of each watched page. Two endpoints let a deploy pipeline
keep it current, so your own releases never alert. Both return `404` for
other monitor types.

### Get the baseline

`GET /v1/monitors/{id}/baseline` — requires `read` scope. One row per
watched page: whether it has an accepted baseline, when it was captured
and how — `auto` (recorded from the page's first check), `manual`
(accepted in the dashboard), `api` (the endpoint below) or `timer`
(accepted automatically after the monitor's `autoAcceptAfterHours`).
`fresh_accept` is the latest [fresh accept](#accept-the-latest-state-deploy-hook)
(kept 24 hours, `null` when there was none): `pending` until its check lands,
then `accepted` (with `accepted`, `skipped`, `incidents_resolved`), `expired`
(no check arrived within 5 minutes — nothing was accepted) or `failed`.

```bash
curl https://watchfor.io/api/v1/monitors/0b0f6a41-…/baseline \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

```json
{
  "object": "integrity_baseline",
  "monitor_id": "0b0f6a41-…",
  "pages": [
    {
      "url": "https://example.com/",
      "has_baseline": true,
      "captured_at": "2026-09-25T09:43:00.000Z",
      "source": "api",
      "accepted_by": "deploy-pipeline"
    },
    {
      "url": "https://example.com/checkout",
      "has_baseline": false,
      "captured_at": null,
      "source": null,
      "accepted_by": null
    }
  ],
  "trusted_origins": ["https://js.stripe.com"],
  "text_change_threshold": 25,
  "fresh_accept": {
    "status": "accepted",
    "requested_at": "2026-09-25T09:42:31.000Z",
    "expires_at": "2026-09-25T09:47:31.000Z",
    "finished_at": "2026-09-25T09:43:00.000Z",
    "pages": null,
    "accepted": ["https://example.com/"],
    "skipped": [],
    "incidents_resolved": [19233],
    "message": null
  }
}
```

### Accept the latest state (deploy hook)

`POST /v1/monitors/{id}/baseline` — requires `write` scope. Takes what
the newest check saw on every watched page — or only the `pages` you
list — as the new baseline, so an intended change stops alerting. Pages no
check has fetched yet come back in `skipped` as `{ url, reason }`.

When the accept leaves nothing to fix — every watched page matches its
baseline in the newest check — the monitor's open integrity incidents are
resolved at once and listed in `incidents_resolved`; the incident timeline
and your channels say "Resolved — baseline accepted by" the API key. If a
page still differs or could not be fetched, nothing is resolved: a check of
the pages starts right away (`check_requested: true`) and the incident closes
on clean checks as usual. Supports
[`Idempotency-Key`](/docs/api/errors#idempotency--safe-retries); each
accept (and each incident it resolves) is recorded in the
[activity log](/docs/api/activity).

**Deploy hook — `?fresh=true`.** Right after a release the newest check
usually saw the old version. Add `?fresh=true` (or `"fresh": true` in the
body) and the server checks the pages first, waits for that check and
accepts what it saw:

```bash
curl -s -X POST "https://watchfor.io/api/v1/monitors/0b0f6a41-…/baseline?fresh=true" \
  -H "Authorization: Bearer wf_live_YOUR_WRITE_KEY"
```

```json
{
  "object": "integrity_baseline_accept",
  "monitor_id": "0b0f6a41-…",
  "status": "accepted",
  "requested_at": "2026-09-25T10:01:41.000Z",
  "accepted": ["https://example.com/", "https://example.com/pricing"],
  "skipped": [],
  "captured_at": "2026-09-25T10:02:00.000Z",
  "check_requested": true,
  "incidents_resolved": [19233]
}
```

- The request waits up to about **85 seconds** (polling every few seconds).
  If the check takes longer it returns **`202`** with
  `{ "status": "pending", "requested_at", "expires_at", "message" }` and the
  accept finishes in the background when the check lands — at most 5 minutes
  after the request; poll `GET …/baseline` → `fresh_accept` for the outcome.
- A page the fresh check could not fetch is skipped — never accepted from an
  older check.
- Each accept may start a check, and a monitor allows **12 per hour**
  (counting the check after every accept). A fresh accept over that gets
  `429` with `Retry-After`; a paused monitor gets `409`.

Omit the body (or send `{}`) to accept every page. Accept only after an
intended change: whatever the check saw — including a script an attacker
injected — becomes "normal".

## Browser checks

A browser check (`type: "playwright"`) is created and changed like any other
monitor: the site is the `target` and the script goes in `config.script`
(with `timeoutSeconds`, `retry`, `trace` — see the
[catalog](/docs/api/monitor-types#browser-check)). Secrets the script reads
live in organization variables, never in the script.

```bash
curl -X POST https://watchfor.io/api/v1/monitors \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Checkout",
    "type": "playwright",
    "target": "https://shop.example.com",
    "interval": 600,
    "locations": ["9f31c2ab-…"],
    "config": { "script": "import { test, expect } from \'@playwright/test\';\n…" }
  }'
```

A script with an error is rejected with `400` naming the line, before
anything runs. Each save keeps a script version (the last 50). The plan
limits the number of checks, the interval, the run timeout and the scheduled
runs per month — worked out from each check's interval and locations when you
save it, so a change that would exceed the month's runs is rejected with
`403` naming the limit.

| Method | Path | What it does |
| --- | --- | --- |
| `POST` | `/v1/playwright/test-runs` | Run a script once without saving it; `202` with an `id` and `missing_variables`. Write scope; counts against test runs per hour |
| `GET` | `/v1/playwright/test-runs/{id}` | The test run: `queued`, `running`, `done` or `error`, with the result when done |
| `GET` | `/v1/monitors/{id}/runs` | A check's runs, newest first; `status=failed` or `flaky`, `limit`, `cursor` |
| `GET` | `/v1/monitors/{id}/runs/{run_id}` | One run: steps, failure with code frame, console and page errors, failed requests, Web Vitals, runtime versions, signed screenshot links |
| `GET` | `/v1/monitors/{id}/script/versions` | The saved script versions |
| `GET` | `/v1/monitors/{id}/script/versions/{version}` | One version's script |
| `GET` | `/v1/variables` | Organization variables; secret values are never returned |
| `PUT` | `/v1/variables/{name}` | Create or change a variable or secret |
| `DELETE` | `/v1/variables/{name}` | Delete a variable |

Examples, the result shape and the SDK calls:
[Browser checks — API, MCP and SDK](/docs/monitors/browser-checks/api).
`POST /v1/monitors/{id}/check-now` starts a run of a saved check right away.

## Delete a monitor

`DELETE /v1/monitors/{id}` — requires `write` scope. Permanently removes
the monitor **and everything it owns**: its schedule, alert rules,
incidents and silences; status pages that referenced it are refreshed.
This cannot be undone.

```bash
curl -X DELETE https://watchfor.io/api/v1/monitors/0b0f6a41-… \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

Returns `200` with `{ "object": "deleted", "id": "…", "deleted": true }`.

## Bulk operations

`POST /v1/monitors/bulk` — requires `write` scope. Pause, resume or
delete 1–100 monitors in one call. Unknown ids (or ids from another
organization) are reported in `skipped`, never as an error, so retrying
a partially applied batch is safe. Already-paused / already-active
monitors count as `succeeded`. Each affected monitor gets its own
[activity log](/docs/api/activity) row (`monitor.paused`, `monitor.resumed`,
`monitor.deleted`, with the batch size in its metadata). Supports
`Idempotency-Key`.

```bash
curl -X POST https://watchfor.io/api/v1/monitors/bulk \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": "pause", "ids": ["0b0f6a41-…", "9c2d1e57-…"] }'
```

```json
{ "object": "bulk_result", "action": "pause", "succeeded": ["0b0f6a41-…"], "skipped": ["9c2d1e57-…"] }
```

For mixed operations across resources (for example three uptime reads
plus one acknowledge) use [`POST /v1/batch`](/docs/api/meta#batch).

## Also via

MCP tools `list_monitors`, `get_monitor`, `create_monitor`,
`update_monitor`, `set_monitor_paused`, `reset_monitor_history`,
`delete_monitor`, `get_integrity_baseline`, `accept_integrity_baseline`,
`test_playwright_script`, `get_playwright_run`, `list_playwright_runs`; A2A skills
`list-monitors`, `explain-monitor`, `create-monitor`, `manage-monitor`;
CLI `watchfor monitors [query]`.

---

Canonical page: https://watchfor.io/docs/api/monitors · All docs: https://watchfor.io/docs · Site guide: https://watchfor.io/llms.txt
