---
title: WatchFor monitor types reference (API catalog)
description: Every WatchFor monitor type — target format, default alert rules, alert metrics and config fields — the per-type reference for the API.
canonical: https://watchfor.io/docs/api/monitor-types
---

# WatchFor monitor types reference (API catalog)

Every WatchFor monitor type — target format, default alert rules, alert metrics and config fields — the per-type reference for the API.

WatchFor supports 30 monitor types. This page lists, for each type, exactly
what to send to the API: the `type` value, the `target` format, the config
fields the type accepts, the alert **metrics** you can build rules from
(with the exact string to pass as the alert-rule `metric`), and the default
rules the system creates automatically.

> **Info**
>
> This reference is also available **machine-readable** at
> [`GET /api/v1/meta/monitor-types`](https://watchfor.io/api/v1/meta/monitor-types)
> (no API key required, `operationId: listMonitorTypes`) — ideal for code
> generation and AI agents; the same document is an MCP resource and the
> `get_monitor_types` tool. Every monitor's own valid metrics are also
> returned inline as `available_metrics` on `GET /v1/monitors/{id}`.

> **Two browser-based types**
>
> **`playwright` is a browser check**: your Playwright Test script runs a
> journey (login, checkout, search) step by step. **`browser` is Core Web
> Vitals**: a Lighthouse audit of one page. The type ids predate the product
> names, so `browser` does not mean browser checks.

How to read the tables:

- **Passes as `metric`** is the exact string for
  [`POST /v1/monitors/{id}/alert-rules`](/docs/api/alert-rules). The
  universal `success` metric (probe passed or failed) is valid on every
  type, even where a type's table omits it.
- **Op** is the operator the dashboard preset uses. Numeric metrics
  accept any of `>`, `<`, `>=`, `<=`, `==`, `!=` (the endpoint's
  top-level `operators` array); `success` takes `==`/`!=` with
  `"true"`/`"false"`; status-code metrics take `in_groups`/`not_in_groups`
  with a JSON array such as `["2xx","3xx"]` or `[200,301]`.
- **Example value** is a sensible threshold in the metric's unit; the
  API rejects values outside the unit's plausible range with a message
  naming the range.
- **Config fields** are the keys of the `config` object on
  [create/update](/docs/api/monitors#create-a-monitor). Keys marked
  **secret** are write-only and come back as `"[redacted]"`. The mailbox,
  SMTP, FTP and SFTP credentials are also stored encrypted: on update,
  omitting one, sending `""` or the `"[redacted]"` mask keeps the stored
  value, `null` removes it, and changing the server, port, TLS mode or login
  it is used with requires sending it again — see
  [credentials](/docs/api/monitors#credentials-write-only).
- **Default alert rules** are seeded on creation and can be edited or
  disabled but not deleted. The default `Availability Check`
  (`success == false`, critical) fires after 3 consecutive failing
  checks confirmed by the monitor's locations; types that observe a
  single global fact (Domain/RDAP, Breach, Heartbeat) skip it in favor of
  their own rules.

Each entry also carries a `description` in the JSON catalog (shown
under the heading here).

## Config keys every type accepts

Besides the per-type fields below, `config` carries four alerting knobs
that apply to **every** rule on the monitor. The dashboard's Alerting tab
edits the same keys, and the type catalog
(`GET /v1/meta/monitor-types`) lists them under each type's
`config_fields`.

| Key | Type | Notes |
|---|---|---|
| `notificationGroupIds` | array | Contact-group ids that receive this monitor's alerts; new rules inherit them. Defaults to the organization's default group |
| `confirmationThreshold` | number | Consecutive failed checks before an incident opens; default `3` (`1` for heartbeat and page integrity) |
| `durationWindow` | number | Seconds a condition must persist before it alerts; `0` (default) alerts on the first confirmed failure |
| `repeatIntervalSeconds` | enum | Repeat the notification while the incident stays open: `3600`, `21600`, `43200` or `86400`; omit for a single notification |

The types that report a certificate or domain expiry — HTTP, SSL / TLS,
Domain (RDAP), MCP and API — also accept `expiryReminderDays` (listed in
their tables below): reminder days for an open expiry incident, validated as
whole days 1–365, at most five, stored deduplicated and sorted. Sending it to
any other type is a `400` with the field path.

Some rule-shaped settings live in `config` rather than on the alert rule —
`failIfBodyNotMatchesRegexp` (HTTP), `expectedValues` (DNS),
`expectedProvider` (CDN), `expectedCms` (CMS), `expectMatchRegexp`
(TCP/UDP), `expectPattern` (WebSocket) and `expectedProtocolVersion` (MCP).
Set the key on the monitor, then create the rule on the metric named in the
key's description.

## TCP

TCP port connectivity + optional payload match.

- **Type value** (`type` field): `tcp`
- **Target**: host:port — e.g. `example.com:443`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Slow TCP Connect — `metrics['tcp.connect_time_ms'] > 1000` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Connect time (ms) | `tcp_connect` | `>` | `1000` | warning |
| TLS handshake time (ms) | `metrics['tcp.tls_handshake_time_ms']` | `>` | `1000` | warning |
| Response regexp match (1=yes) | `metrics['tcp.expect_match_regexp']` | `==` | `0` | critical |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `tls` | boolean | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `query` | string | Payload to send after connecting; the reply feeds `expectMatchRegexp` |
| `expectMatchRegexp` | string | Regexp the reply must match — drives the *Response Must Match* rule |

## DNS

Resolve a record and check answers + timing.

- **Type value** (`type` field): `dns`
- **Target**: domain name — e.g. `example.com`. For `PTR` an IP address (a hostname is resolved first and each address looked up in reverse); for `SRV` the service name, e.g. `_sip._tcp.example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Slow DNS Lookup — `metrics['dns.check_time_ms'] > 1000` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Lookup time (ms) | `dns_time` | `>` | `1000` | warning |
| Answer count | `metrics['dns.answers_count']` | `==` | `0` | warning |
| Error count | `metrics['dns.error_count']` | `>` | `0` | critical |
| Expected value match (1=yes) | `metrics['dns.expected_match']` | `==` | `0` | critical |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `queryType` | enum | one of: `A`, `AAAA`, `CNAME`, `MX`, `NS`, `TXT`, `SOA`, `SRV`, `PTR`, `CAA`; default `A` |
| `resolver` | string | Custom resolver IP |
| `captureRecords` | boolean | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `expectedValues` | array | Expected answers for the *Expected DNS Records* rule (`dns.expected_match`): addresses for A/AAAA, hosts for NS/MX, values for TXT/CNAME/PTR/CAA, target host for SRV |
| `matchMode` | enum | `any_exact` (default), `any_regex`, `any_contains`, `none_contains` (value must NOT appear) |

Expected-value matching (the *Expected DNS Records* rule) compares against the
address (`A`/`AAAA`), host (`NS`, `MX`), value (`TXT`, `CNAME`, `PTR`, `CAA`),
target host (`SRV`) or primary name server / mailbox / serial (`SOA`) of each
answer. See the
[value format per record type](/docs/monitors/network#dns).

## HTTP / HTTPS

Monitor a web page or HTTP endpoint.

- **Type value** (`type` field): `http`
- **Target**: https URL — e.g. `https://example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Unexpected Status Code — `status_code not_in_groups ["2xx", "3xx"]` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| HTTP status code | `status_code` | `not_in_groups` | `["2xx","3xx"]` | critical |
| Response time (ms) | `response_time` | `>` | `2000` | warning |
| Body keyword / regexp match (1 = ok, 0 = failed) | `http.body_regexp_match` | `==` | `0` | critical |
| SSL days until expiry | `ssl_expiry` | `<` | `14` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `method` | enum | one of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`; default `GET` |
| `followRedirects` | boolean | default `true` |
| `authType` | enum | one of: `none`, `basic`, `digest`, `bearer`; default `none` |
| `authUsername` | string | — |
| `authPassword` | string | **secret** (returned redacted) |
| `bearerToken` | string | **secret** (returned redacted) |
| `requestBody` | string | **secret** (returned redacted) |
| `requestHeaders` | array | `Array of { key, value }` |
| `failIfBodyMatchesRegexp` | string or string[] | Regexp(s) that must NOT appear in the body; when one does, `http.body_regexp_match` becomes `0` |
| `failIfBodyNotMatchesRegexp` | string or string[] | Regexp(s) the body MUST contain; when one is missing, `http.body_regexp_match` becomes `0` |
| `transportMode` | enum | `auto` (default), `tcp` (HTTP/1.1 or HTTP/2 over TCP) or `http3` (QUIC) |
| `expiryReminderDays` | array | Whole days (1–365, up to 5), e.g. `[14, 3]`: re-send an open, unacknowledged certificate-expiry incident's notification once when that many days are left; omit or `null` for none — see [reminders](/docs/monitors/certificates#reminders-before-expiry) |

Body-match keys only set the metric — the check itself still succeeds — so
pair them with a rule on `http.body_regexp_match == 0` (the dashboard's
keyword rule does exactly that).

## SSL / TLS

Certificate expiry, chain, hostname + TLS version.

- **Type value** (`type` field): `ssl`
- **Target**: domain or host:port — e.g. `example.com:443`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- SSL Expiry Warning — `metrics['ssl.days_until_expiry'] < 14` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Days until expiry | `metrics['ssl.days_until_expiry']` | `<` | `14` | warning |
| Hostname match (1=yes) | `metrics['ssl.hostname_match']` | `==` | `0` | critical |
| Chain valid (1=yes) | `metrics['ssl.chain_valid']` | `==` | `0` | critical |
| TLS version (12 = 1.2) | `metrics['ssl.tls_version']` | `<` | `12` | warning |
| TLS grade score (A+=6, A=5, B=4, C=3, F=0) | `metrics['tlsgrade.grade_score']` | `<` | `5` | warning |

The TLS grade metric is only populated when the plan includes TLS grading
(`allowSslGrade`) and the monitor has `trackTlsGrade` on — the default for
new SSL monitors on those plans when the key is omitted; on other plans the
flag is ignored. The metric is present on the one check a day that carries
the grade scan (and on every Check now), not on every check, so rules on it
are evaluated then. `"< 5"` fires when the grade drops below A.

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `insecure` | boolean | — |
| `port` | number | — |
| `serverName` | string | — |
| `trackTlsGrade` | boolean | Daily TLS grade (plan-gated). Defaults to `true` on plans that include it when omitted; `false` skips the grade scan. Enables the `tlsgrade.grade_score` metric |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `expiryReminderDays` | array | Whole days (1–365, up to 5), e.g. `[14, 3]`: re-send an open, unacknowledged certificate-expiry incident's notification once when that many days are left; omit or `null` for none — see [reminders](/docs/monitors/certificates#reminders-before-expiry) |

## UDP

UDP query + response match.

- **Type value** (`type` field): `udp`
- **Target**: host:port — e.g. `example.com:53`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Response received (1=yes) | `metrics['udp.response_received']` | `==` | `0` | warning |
| First byte time (ms) | `metrics['udp.first_byte_time_ms']` | `>` | `500` | warning |
| Response regexp match (1=yes) | `metrics['udp.expect_match_regexp']` | `==` | `0` | critical |
| Bytes received | `metrics['udp.bytes_received']` | `<` | `1` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `query` | string | Payload to send |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `queryBase64` | string | Binary payload as base64 (alternative to `query`); must be standard padded base64, anything else is a `400` |
| `expectMatchRegexp` | string | Regexp the reply must match — drives the *Response Must Match* rule |

A check needs something to send: set `query` or `queryBase64`. UDP has no
handshake, so a reply is the only proof the service is there — with
neither key, every check fails with *no payload to send*.

## ICMP (Ping)

Host reachability + packet loss + latency.

- **Type value** (`type` field): `icmp`
- **Target**: domain or IP — e.g. `example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Packet loss (%) | `packet_loss` | `>` | `50` | warning |
| Ping average (ms) | `ping_avg` | `>` | `200` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `count` | number | — |
| `packetSize` | number | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## MTR (Traceroute)

Per-hop path analysis to a destination.

- **Type value** (`type` field): `mtr`
- **Target**: domain or IP — e.g. `example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Destination Unreachable — `metrics['mtr.destination_reached'] == 0` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Destination reached (1=yes) | `metrics['mtr.destination_reached']` | `==` | `0` | critical |
| Destination loss (%) | `metrics['mtr.destination_loss_percent']` | `>` | `5` | warning |
| Destination RTT (ms) | `metrics['mtr.rtt_avg_ms']` | `>` | `250` | warning |
| Max path loss (%) | `metrics['mtr.path_loss_max_percent']` | `>` | `10` | warning |
| Unresponsive hops | `metrics['mtr.unresponsive_hops_count']` | `>` | `3` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `count` | number | — |
| `maxHops` | number | — |
| `port` | number | — |
| `protocol` | string | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## Sitemap

Crawl a sitemap.xml and check for broken URLs.

- **Type value** (`type` field): `sitemap`
- **Target**: https URL — e.g. `https://example.com/sitemap.xml`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Sitemap Missing — `metrics['sitemap.found'] == 0` (critical)
- Broken URL Ratio — `metrics['sitemap.broken_percent'] > 20` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Sitemap found (1=yes) | `metrics['sitemap.found']` | `==` | `0` | critical |
| Broken URLs (%) | `metrics['sitemap.broken_percent']` | `>` | `20` | warning |
| Broken URL count | `metrics['sitemap.broken_urls']` | `>` | `5` | warning |
| Total URL count | `metrics['sitemap.url_count']` | `<` | `10` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `checkUrlCount` | number | — |
| `captureBrokenUrls` | boolean | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## Favicon

Check a site's favicon is served correctly.

- **Type value** (`type` field): `favicon`
- **Target**: https URL — e.g. `https://example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Favicon Missing — `metrics['favicon.found'] == 0` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Favicon found (1=yes) | `metrics['favicon.found']` | `==` | `0` | warning |
| Content-type match (1=yes) | `metrics['favicon.content_type_match']` | `==` | `0` | warning |
| Favicon size (bytes) | `metrics['favicon.bytes']` | `<` | `100` | warning |
| Favicon changed (1=yes) | `metrics['favicon.changed']` | `==` | `1` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `timeoutSeconds` | number | Per-check timeout in seconds |

## CDN

Watch how a URL is delivered: which CDN fronts it, whether the edge is
really caching it, and how fast it answers.

- **Type value** (`type` field): `cdn`
- **Target**: https URL — e.g. `https://example.com/static/app.js`. Give it
  the URL you care about, not just the hostname: caching rules are set per
  path far more often than per host.

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- CDN Not Detected — `metrics['cdn.detected'] == 0` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| CDN detected (1=yes) | `metrics['cdn.detected']` | `==` | `0` | warning |
| Provider match (1=yes) | `metrics['cdn.provider_match']` | `==` | `0` | warning |
| Cache hit ratio (0-1) | `metrics['cdn.cache_hit_ratio']` | `<` | `0.5` | warning |
| Time to first byte (ms) | `metrics['cdn.ttfb_ms']` | `>` | `1000` | warning |
| HTTP status code | `metrics['cdn.status_code']` | `>=` | `400` | critical |

`cdn.cache_hit_ratio` is only emitted when the edge published a cache
verdict, so a rule on it stays silent for providers that publish none
rather than firing constantly. A `DYNAMIC` or `BYPASS` verdict *is* an
answer and counts as a non-hit, so a deliberately uncacheable URL sits at 0
forever — put hit-ratio rules on assets you expect to be cached.

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `expectedProvider` | string | Provider the *Provider Must Be* rule expects (e.g. `cloudflare`); compared case-insensitively with the detected provider |
| `trackCache` | boolean | Measure cache behaviour by making two sequential requests per check instead of one, which is what makes `cdn.cache_hit_ratio` available. On unless explicitly set to `false` |

## Brotli

Check Brotli compression support.

- **Type value** (`type` field): `brotli`
- **Target**: https URL — e.g. `https://example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Brotli Not Supported — `metrics['brotli.supported'] == 0` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Brotli supported (1=yes) | `metrics['brotli.supported']` | `==` | `0` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `timeoutSeconds` | number | Per-check timeout in seconds |

## Breach

Watch a domain for newly reported data breaches.

- **Type value** (`type` field): `breach`
- **Target**: domain — e.g. `example.com` (a URL is accepted too; only its
  host is used). `ipVersion` is ignored: the check reads a public breach
  catalog rather than connecting to the domain.

**Default alert rules** (created automatically):

- New Breach Within Window — `metrics['breach.last_added_age_hours'] < 48` (critical)

No Availability Check is seeded: a failed check means the breach feed could
not be fetched or the domain did not resolve — a lookup problem, not a
breach. A `success == false` rule can still be created (it is the optional
**Probe Status** preset in the dashboard).

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Hours since newest breach | `metrics['breach.last_added_age_hours']` | `<` | `48` | critical |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `timeoutSeconds` | number | Per-check timeout in seconds |

## CMS

Detect the CMS powering a site.

- **Type value** (`type` field): `cms`
- **Target**: https URL — e.g. `https://example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| CMS detected (1=yes) | `metrics['cms.detected']` | `==` | `0` | warning |
| Provider match (1=yes) | `metrics['cms.provider_match']` | `==` | `0` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `expectedCms` | string | Platform the *Provider Must Be* rule expects (e.g. `wordpress`); compared case-insensitively with the detected CMS |

## SMTP

Mail server connectivity, relay + STARTTLS checks.

- **Type value** (`type` field): `smtp`
- **Target**: domain or host:port — e.g. `mail.example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Open Relay Detected — `metrics['smtp.open_relay'] == 1` (critical)
- With a `username` (authenticated submission) also: Login Failed — `metrics['smtp.auth_ok'] == 0` (critical) and Certificate Expiring — `metrics['smtp.cert_days_left'] < 14` (warning)

The login, certificate and TLS metrics are reported only by authenticated checks (a `username` is set); those checks skip the open-relay test.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Connect time (ms) | `metrics['smtp.connect_time_ms']` | `>` | `1000` | warning |
| Open relay (1=yes) | `metrics['smtp.open_relay']` | `==` | `1` | critical |
| STARTTLS supported (1=yes) | `metrics['smtp.tls_supported']` | `==` | `0` | warning |
| PTR valid (1=yes) | `metrics['smtp.ptr_valid']` | `==` | `0` | warning |
| Banner match (1=yes) | `metrics['smtp.banner_match']` | `==` | `0` | warning |
| Login OK (1=yes; only with a username) | `metrics['smtp.auth_ok']` | `==` | `0` | critical |
| Login time (ms) | `metrics['smtp.auth_ms']` | `>` | `2000` | warning |
| Certificate days left (-1 = no TLS) | `metrics['smtp.cert_days_left']` | `<` | `14` | warning |
| Certificate valid (1=yes) | `metrics['smtp.cert_valid']` | `==` | `0` | warning |
| TLS version (0 = no TLS) | `metrics['smtp.tls_version']` | `<` | `1.2` | warning |
| TLS handshake (ms) | `metrics['smtp.tls_time_ms']` | `>` | `1000` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `port` | number | Server port (default 25 for the banner/relay check). With a `username` set it is required: 587 (STARTTLS) or 465 (implicit TLS) — 25 is refused for logins |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `checkOpenRelay` | boolean | Test whether the server relays mail for foreign domains (*Open Relay* rule) |
| `checkPtr` | boolean | Verify the server IP has a matching reverse-DNS (PTR) record (*PTR* rule) |
| `username` | string | Optional submission login (SMTP AUTH after TLS). Requires port 587 or 465 — never 25 |
| `password` | string | **secret** (write-only, stored encrypted). Submission password (required with a username) |
| `tlsMode` | enum | With a username: `starttls` (587, default) or `implicit` (465) |
| `verifyTls` | boolean | With a username: fail when the certificate does not verify; default `true` |

## IMAP

Mailbox server check: greeting, TLS and certificate, login, mailbox, folders and quota.

- **Type value** (`type` field): `imap`
- **Target**: hostname — e.g. `imap.example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Certificate Expiring — `metrics['imap.cert_days_left'] < 14` (warning)
- With a `username` also: Login Failed — `metrics['imap.auth_ok'] == 0` (critical), Mailbox Almost Full — `metrics['imap.quota_used_pct'] >= 90` (warning), Mailbox Full — `metrics['imap.quota_used_pct'] >= 99` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Check failed — connection, TLS or login (count) | `metrics['imap.error_count']` | `>` | `0` | critical |
| Login OK (1=yes; only when a username is set) | `metrics['imap.auth_ok']` | `==` | `0` | critical |
| Certificate days left (-1 = no TLS) | `metrics['imap.cert_days_left']` | `<` | `14` | warning |
| Certificate valid (1=yes) | `metrics['imap.cert_valid']` | `==` | `0` | warning |
| TLS version (1.0–1.3; 0 = no TLS) | `metrics['imap.tls_version']` | `<` | `1.2` | warning |
| Login offered without encryption (1=yes) | `metrics['imap.plaintext_login_allowed']` | `==` | `1` | warning |
| STARTTLS offered on the plain port (1=yes) | `metrics['imap.starttls_offered']` | `==` | `0` | warning |
| Total check time (ms) | `metrics['imap.check_time_ms']` | `>` | `5000` | warning |
| Connect time (ms) | `metrics['imap.connect_time_ms']` | `>` | `1000` | warning |
| TLS handshake (ms) | `metrics['imap.tls_time_ms']` | `>` | `1000` | warning |
| Server greeting (ms) | `metrics['imap.greeting_ms']` | `>` | `2000` | warning |
| Login time (ms) | `metrics['imap.auth_ms']` | `>` | `2000` | warning |
| Messages in the mailbox | `metrics['imap.messages']` | `>` | `10000` | warning |
| Age of the newest message (s) | `metrics['imap.newest_message_age_s']` | `>` | `86400` | warning |
| Unseen messages | `metrics['imap.unseen']` | `>` | `1000` | warning |
| Folders | `metrics['imap.folders']` | `<` | `1` | warning |
| Mailbox open time (ms) | `metrics['imap.select_ms']` | `>` | `2000` | warning |
| Mailbox quota used (%) — the higher of storage and message count; only when the server reports a quota | `metrics['imap.quota_used_pct']` | `>=` | `90` | warning |
| Mailbox size (bytes) — the quota's used storage, else STATUS SIZE | `metrics['imap.mailbox_bytes']` | `>` | `524288000` | warning |
| Quota storage used (bytes) | `metrics['imap.quota_used_bytes']` | `>` | `524288000` | warning |
| Quota storage limit (bytes; absent when unlimited) | `metrics['imap.quota_limit_bytes']` | `<` | `104857600` | warning |
| Server reports a quota (1=yes) | `metrics['imap.quota_supported']` | `==` | `0` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `port` | number | Server port (default 993 for implicit TLS, 143 for STARTTLS / none) |
| `tlsMode` | enum | `implicit` = TLS from the first byte; `starttls` = upgrade the plain connection; `none` = no encryption (connection-only — credentials are never sent unencrypted); one of: `implicit`, `starttls`, `none`; default `implicit` |
| `verifyTls` | boolean | Fail the check when the certificate does not verify; default `true` |
| `username` | string | Mailbox login. Empty = connection-only check (greeting, TLS, capabilities) |
| `password` | string | **secret** (write-only, stored encrypted). Mailbox password (required with a username). Gmail needs an app password; Microsoft 365 no longer accepts passwords for IMAP |
| `checkNewestAge` | boolean | Read the date of the newest message (no subject or sender is read or stored); default `true` |
| `timeoutSeconds` | number | Per-connection timeout in seconds; default `15` |
| `mailbox` | string | Mailbox to open (read-only EXAMINE); default `"INBOX"` |
| `listFolders` | boolean | List the folders (names and special-use flags); default `true` |
| `ipVersion` | enum | IP version used to reach the target; one of: `any`, `ip4`, `ip6` (`any` = IPv4 first, IPv6 fallback) |

Dashboard guide: [IMAP monitors](/docs/monitors/imap).

## POP3

Mailbox server check: greeting, TLS and certificate, login and mailbox size.

- **Type value** (`type` field): `pop3`
- **Target**: hostname — e.g. `pop.example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Certificate Expiring — `metrics['pop3.cert_days_left'] < 14` (warning)
- With a `username` also: Login Failed — `metrics['pop3.auth_ok'] == 0` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Check failed — connection, TLS or login (count) | `metrics['pop3.error_count']` | `>` | `0` | critical |
| Login OK (1=yes; only when a username is set) | `metrics['pop3.auth_ok']` | `==` | `0` | critical |
| Certificate days left (-1 = no TLS) | `metrics['pop3.cert_days_left']` | `<` | `14` | warning |
| Certificate valid (1=yes) | `metrics['pop3.cert_valid']` | `==` | `0` | warning |
| TLS version (1.0–1.3; 0 = no TLS) | `metrics['pop3.tls_version']` | `<` | `1.2` | warning |
| Login offered without encryption (1=yes) | `metrics['pop3.plaintext_login_allowed']` | `==` | `1` | warning |
| STARTTLS offered on the plain port (1=yes) | `metrics['pop3.stls_offered']` | `==` | `0` | warning |
| Total check time (ms) | `metrics['pop3.check_time_ms']` | `>` | `5000` | warning |
| Connect time (ms) | `metrics['pop3.connect_time_ms']` | `>` | `1000` | warning |
| TLS handshake (ms) | `metrics['pop3.tls_time_ms']` | `>` | `1000` | warning |
| Server greeting (ms) | `metrics['pop3.greeting_ms']` | `>` | `2000` | warning |
| Login time (ms) | `metrics['pop3.auth_ms']` | `>` | `2000` | warning |
| Messages in the mailbox | `metrics['pop3.messages']` | `>` | `10000` | warning |
| Age of the newest message (s) | `metrics['pop3.newest_message_age_s']` | `>` | `86400` | warning |
| Mailbox size (bytes, STAT) — POP3 has no quota | `metrics['pop3.mailbox_bytes']` | `>` | `524288000` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `port` | number | Server port (default 995 for implicit TLS, 110 for STARTTLS / none) |
| `tlsMode` | enum | `implicit` = TLS from the first byte; `starttls` = upgrade the plain connection; `none` = no encryption (connection-only — credentials are never sent unencrypted); one of: `implicit`, `starttls`, `none`; default `implicit` |
| `verifyTls` | boolean | Fail the check when the certificate does not verify; default `true` |
| `username` | string | Mailbox login. Empty = connection-only check (greeting, TLS, capabilities) |
| `password` | string | **secret** (write-only, stored encrypted). Mailbox password (required with a username). Gmail needs an app password; Microsoft 365 no longer accepts passwords for POP3 |
| `checkNewestAge` | boolean | Read the date of the newest message (no subject or sender is read or stored); default `true` |
| `timeoutSeconds` | number | Per-connection timeout in seconds; default `15` |
| `ipVersion` | enum | IP version used to reach the target; one of: `any`, `ip4`, `ip6` (`any` = IPv4 first, IPv6 fallback) |

Dashboard guide: [POP3 monitors](/docs/monitors/pop3).

## Email round-trip

Sends a test message over SMTP submission and finds it in an IMAP/POP3 mailbox: delivery time, inbox vs spam, SPF/DKIM/DMARC, hops.

- **Type value** (`type` field): `email_roundtrip`
- **Target**: hostname of the sending (SMTP submission) server — e.g. `smtp.example.com`

Minimum interval: 300 seconds (every check sends a real message).

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Message Not Delivered — `metrics['roundtrip.delivered'] == 0` (critical, after 2 checks)
- Landed in Spam — `metrics['roundtrip.in_spam'] == 1` (warning, after 2 checks)

SPF / DKIM / DMARC, spam score and antivirus metrics are present only when the receiving server wrote a verdict header.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Delivered (1=yes) | `metrics['roundtrip.delivered']` | `==` | `0` | critical |
| Delivery time (ms) | `metrics['roundtrip.delivery_ms']` | `>` | `60000` | warning |
| Landed in spam (1=yes) | `metrics['roundtrip.in_spam']` | `==` | `1` | warning |
| SPF pass (1=yes) | `metrics['roundtrip.spf_pass']` | `==` | `0` | warning |
| DKIM pass (1=yes) | `metrics['roundtrip.dkim_pass']` | `==` | `0` | warning |
| DMARC pass (1=yes) | `metrics['roundtrip.dmarc_pass']` | `==` | `0` | warning |
| Spam score | `metrics['roundtrip.spam_score']` | `>` | `5` | warning |
| Relay hops | `metrics['roundtrip.hops']` | `>` | `10` | warning |
| Slowest hop (ms) | `metrics['roundtrip.max_hop_delay_ms']` | `>` | `30000` | warning |
| Every hop encrypted (1=yes) | `metrics['roundtrip.all_hops_tls']` | `==` | `0` | warning |
| Check failed (count) | `metrics['roundtrip.error_count']` | `>` | `0` | critical |
| Send time (ms) | `metrics['roundtrip.send_ms']` | `>` | `10000` | warning |
| Total check time (ms) | `metrics['roundtrip.check_time_ms']` | `>` | `120000` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `smtpPort` | number | Submission port: 587 (STARTTLS) or 465 (implicit TLS); default 587, or 465 when `smtpTlsMode` is `implicit`. 25 is refused; default `587` |
| `smtpTlsMode` | enum | one of: `starttls`, `implicit`; default `starttls` |
| `smtpVerifyTls` | boolean | default `true` |
| `smtpUsername` | string | Sending login (required) |
| `smtpPassword` | string | **secret** (write-only, stored encrypted). Sending password (required) |
| `fromAddress` | string | Envelope + header From (default: the SMTP username); the SMTP login or another address at its domain |
| `toAddress` | string | Mailbox the test message is sent to (required) — the mailbox the check reads, or an alias at its domain |
| `receiveProtocol` | enum | one of: `imap`, `pop3`; default `imap` |
| `receiveHost` | string | IMAP/POP3 server of the receiving mailbox (required) |
| `receivePort` | number | Default 993 (IMAP) / 995 (POP3) for implicit TLS, 143 / 110 for STARTTLS |
| `receiveTlsMode` | enum | one of: `implicit`, `starttls`; default `implicit` |
| `receiveVerifyTls` | boolean | default `true` |
| `receiveUsername` | string | Receiving mailbox login (required unless `receiveSameCredentials`). When it is an address, `toAddress` must be at its domain |
| `receivePassword` | string | **secret** (write-only, stored encrypted). Receiving mailbox password (required unless `receiveSameCredentials`) |
| `receiveSameCredentials` | boolean | Receive with the sending login (username and password); default `false` |
| `mailbox` | string | default `"INBOX"` |
| `checkSpamFolder` | boolean | IMAP only: also look in the Junk/Spam folder; default `true` |
| `deliveryTimeoutSeconds` | number | How long to wait for the message (10–240 s); default `120` |
| `deleteAfter` | boolean | Delete the test message once found (leftovers older than 1 h are swept anyway); default `true` |
| `timeoutSeconds` | number | Per-connection timeout in seconds; default `15` |

Dashboard guide: [Email round-trip monitors](/docs/monitors/email-round-trip).

## FTP

FTP connect, auth, list, write + speed.

- **Type value** (`type` field): `ftp`
- **Target**: domain, IP or host:port — e.g. `ftp.example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Connection Failed — `metrics['ftp.connect_success'] == 0` (critical)
- Authentication Failed — `metrics['ftp.authentication_success'] == 0` (critical)
- List Failed — `metrics['ftp.list_success'] == 0` (warning)

The write test (`ftp.write_success`) is not seeded — anonymous and mirror FTP accounts are often read-only; enable the *Write Test Failed* preset for writable accounts.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Connect OK (1=yes) | `metrics['ftp.connect_success']` | `==` | `0` | critical |
| Auth OK (1=yes) | `metrics['ftp.authentication_success']` | `==` | `0` | critical |
| Write OK (1=yes) | `metrics['ftp.write_success']` | `==` | `0` | critical |
| List OK (1=yes) | `metrics['ftp.list_success']` | `==` | `0` | warning |
| Connect time (ms) | `metrics['ftp.connect_time_ms']` | `>` | `2000` | warning |
| Login time (ms) | `metrics['ftp.login_time_ms']` | `>` | `5000` | warning |
| TLS upgrade completed (1=yes; requires `secure`) | `metrics['ftp.is_secure']` | `==` | `0` | warning |
| Upload throughput (bits/s) | `metrics['ftp.upload_speed_bps']` | `<` | `800000` | warning |
| Download throughput (bits/s) | `metrics['ftp.download_speed_bps']` | `<` | `800000` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `port` | number | — |
| `username` | string | — |
| `password` | string | **secret** (write-only, stored encrypted) |
| `secure` | boolean | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## SFTP

SSH file transfer connect, auth, list, write + speed.

- **Type value** (`type` field): `sftp`
- **Target**: domain, IP or host:port — e.g. `sftp.example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Connection Failed — `metrics['sftp.connect_success'] == 0` (critical)
- Authentication Failed — `metrics['sftp.authentication_success'] == 0` (critical)
- List Failed — `metrics['sftp.list_success'] == 0` (warning)

The write test (`sftp.write_success`) is not seeded — many SFTP accounts
are read-only and the test would fail on every check. Create the rule when
the account is writable.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Connect OK (1=yes) | `metrics['sftp.connect_success']` | `==` | `0` | critical |
| Auth OK (1=yes) | `metrics['sftp.authentication_success']` | `==` | `0` | critical |
| Write OK (1=yes) | `metrics['sftp.write_success']` | `==` | `0` | critical |
| List OK (1=yes) | `metrics['sftp.list_success']` | `==` | `0` | warning |
| Connect time (ms) | `metrics['sftp.connect_time_ms']` | `>` | `3000` | warning |
| Login time (ms) | `metrics['sftp.login_time_ms']` | `>` | `5000` | warning |
| Upload throughput (bits/s) | `metrics['sftp.upload_speed_bps']` | `<` | `800000` | warning |
| Download throughput (bits/s) | `metrics['sftp.download_speed_bps']` | `<` | `800000` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `port` | number | — |
| `username` | string | — |
| `password` | string | **secret** (write-only, stored encrypted) |
| `privateKey` | string | **secret** (write-only, stored encrypted). SSH private key (PEM) |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## WebSocket

WebSocket handshake, ping + pattern match.

- **Type value** (`type` field): `websocket`
- **Target**: ws:// or wss:// URL — e.g. `wss://example.com/ws`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Handshake time (ms) | `metrics['websocket.handshake_time_ms']` | `>` | `1000` | warning |
| Ping RTT (ms) | `metrics['websocket.ping_rtt_ms']` | `>` | `500` | warning |
| Total time (ms) | `metrics['websocket.total_time_ms']` | `>` | `2000` | warning |
| Pattern match (1=yes) | `metrics['websocket.pattern_match']` | `==` | `0` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `checkPing` | boolean | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `sendMessage` | string | Message to send after the handshake (optional) |
| `expectPattern` | string | Regexp the reply must match — drives the *Response Must Match* rule |

## Domain (RDAP)

Domain registration + expiry via RDAP.

- **Type value** (`type` field): `rdap`
- **Target**: domain — e.g. `example.com`

**Default alert rules** (created automatically):

- Domain Expiry — `metrics['rdap.days_until_expiry'] < 30` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Days until domain expiry | `metrics['rdap.days_until_expiry']` | `<` | `30` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `expiryReminderDays` | array | Whole days (1–365, up to 5), e.g. `[14, 3]`: re-send an open, unacknowledged domain-expiry incident's notification once when that many days are left; omit or `null` for none — see [reminders](/docs/monitors/certificates#reminders-before-expiry) |

## Blacklist (RBL)

Check an IP/domain against DNS blacklists.

- **Type value** (`type` field): `blacklist`
- **Target**: domain or IP — e.g. `example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Listed on Any RBL — `metrics['blacklist.listed_count'] > 0` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Blacklists listing this host | `metrics['blacklist.listed_count']` | `>` | `0` | critical |
| Failed RBL lookups | `metrics['blacklist.failed_count']` | `>=` | `2` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `resolver` | string | — |
| `rblsText` | string | Newline-separated RBL zones |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## NTP

Time server offset, sync + stratum.

- **Type value** (`type` field): `ntp`
- **Target**: domain, IP or host:port — e.g. `pool.ntp.org`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Clock Offset Too High — `metrics['ntp.offset_abs_ms'] > 100` (critical)
- Server Not Synchronized — `metrics['ntp.leap_unsync'] > 0` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Absolute clock offset (ms) | `metrics['ntp.offset_abs_ms']` | `>` | `100` | critical |
| Unsynchronized (1=yes) | `metrics['ntp.leap_unsync']` | `>` | `0` | critical |
| Stratum | `metrics['ntp.stratum']` | `>` | `4` | warning |
| Round-trip delay (ms) | `metrics['ntp.delay_ms']` | `>` | `100` | warning |
| Offset jitter across samples (ms) | `metrics['ntp.jitter_ms']` | `>` | `50` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `count` | number | — |
| `maxAbsOffsetMs` | number | — |
| `requireSynced` | boolean | — |
| `timeoutSeconds` | number | Per-check timeout in seconds |

## Email Security

SPF, DMARC + DKIM policy checks for a domain.

- **Type value** (`type` field): `email_policy`
- **Target**: domain — e.g. `example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- SPF Record Missing — `metrics['spf.present'] == 0` (critical)
- DMARC Record Missing — `metrics['dmarc.present'] == 0` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| SPF present (1=yes) | `metrics['spf.present']` | `==` | `0` | critical |
| SPF exceeds the 10 DNS-lookup limit (1=yes) | `metrics['spf.too_many_lookups']` | `>` | `0` | warning |
| DMARC present (1=yes) | `metrics['dmarc.present']` | `==` | `0` | critical |
| DMARC policy strength (0=none, 1=quarantine, 2=reject) | `metrics['dmarc.policy']` | `<` | `1` | warning |
| DKIM present (1=yes) | `metrics['dkim.present']` | `==` | `0` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `checkSpf` | boolean | — |
| `checkDmarc` | boolean | — |
| `checkDkim` | boolean | — |
| `dkimSelectors` | string | DKIM selectors to check |
| `timeoutSeconds` | number | Per-check timeout in seconds (`timeoutMs` is still accepted as a legacy alias) |

## Core Web Vitals

Full Lighthouse audit of one page — performance, Core Web Vitals, SEO. For scripted journeys (browser checks) use type `playwright`.

- **Type value** (`type` field): `browser`
- **Target**: https URL — e.g. `https://example.com`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- LCP Poor — `metrics['lighthouse.lcp_ms'] > 4000` (critical)
- CLS Poor — `metrics['lighthouse.cls'] > 0.25` (critical)

No INP metric: a Lighthouse navigation audit measures LCP, CLS, FCP, TBT,
TTI and Speed Index, but Interaction to Next Paint needs real-user
interaction data, so synthetic checks never produce it. There is no
`lighthouse.inp_ms` metric and the API rejects rules on it.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Largest Contentful Paint (ms) | `metrics['lighthouse.lcp_ms']` | `>` | `4000` | critical |
| Cumulative Layout Shift | `metrics['lighthouse.cls']` | `>` | `0.25` | critical |
| Performance score (0–1) | `metrics['lighthouse.performance_score']` | `<` | `0.5` | warning |
| SEO score (0–1) | `metrics['lighthouse.seo_score']` | `<` | `0.85` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `preset` | enum | one of: `mobile`, `desktop` |

## Heartbeat / Cron

Dead-man's switch — your job pings a URL; we alert if it goes silent.

- **Type value** (`type` field): `heartbeat`
- **Target**: server-assigned ping URL (do not set) — e.g. `/p/<token> (generated on create)`

**Default alert rules** (created automatically):

- Missed Schedule — `metrics['heartbeat.missed'] >= 1` (critical)
- Run Failed — `metrics['heartbeat.run_failed'] >= 1` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Missed check-ins | `metrics['heartbeat.missed']` | `>=` | `1` | critical |
| Run reported failure | `metrics['heartbeat.run_failed']` | `>=` | `1` | warning |
| Exit code | `metrics['heartbeat.exit_code']` | `==` | `137` | critical |
| Run duration (ms) | `metrics['heartbeat.duration_ms']` | `>` | `600000` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `scheduleMode` | enum | one of: `period`, `cron`; default `period` |
| `periodSeconds` | number | default `300` |
| `graceSeconds` | number | default `300` |
| `cronExpression` | string | — |
| `timezone` | string | default `UTC` |

## MCP Server

Model Context Protocol handshake, tools + drift.

- **Type value** (`type` field): `mcp`
- **Target**: https URL — e.g. `https://mcp.example.com/mcp`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Tool Inventory Changed — `metrics['mcp.inventory_changed'] == 1` (warning)
- No Tools Exposed — `metrics['mcp.tools_count'] == 0` (critical)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| Tool inventory changed (1=yes) | `metrics['mcp.inventory_changed']` | `==` | `1` | warning |
| Tools exposed | `metrics['mcp.tools_count']` | `==` | `0` | critical |
| Handshake time (ms) | `metrics['mcp.handshake_time_ms']` | `>` | `2000` | warning |
| Protocol version match (1=yes) | `metrics['mcp.version_match']` | `==` | `0` | warning |
| SSL days until expiry (https endpoints) | `metrics['mcp.ssl_days_until_expiry']` | `<` | `14` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `bearerToken` | string | **secret** (returned redacted) |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `expectedProtocolVersion` | string | Protocol version the server must negotiate (e.g. `2025-11-25`); the check offers this version — drives the *Protocol Version Mismatch* rule |
| `trackInventory` | boolean | Fingerprint the tool inventory every check and alert on drift (default `true`) |
| `expiryReminderDays` | array | Whole days (1–365, up to 5), e.g. `[14, 3]`: re-send an open, unacknowledged certificate-expiry incident's notification once when that many days are left; omit or `null` for none — see [reminders](/docs/monitors/certificates#reminders-before-expiry) |

## API

Full HTTP request with JSON/status assertions and value extraction.

- **Type value** (`type` field): `api`
- **Target**: https URL — e.g. `https://api.example.com/health`

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- HTTP Status Evaluation — `api.status_code not_in_groups ["2xx", "3xx"]` (critical)
- Assertions Failed — `metrics['api.assertions_failed'] > 0` (critical)
- SSL Expiry Warning — `metrics['api.ssl_days_until_expiry'] < 30` (warning)

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| HTTP status code | `api.status_code` | `not_in_groups` | `["2xx","3xx"]` | critical |
| Failed assertions | `metrics['api.assertions_failed']` | `>` | `0` | critical |
| Response time (ms) | `metrics['api.response_time_ms']` | `>` | `2000` | warning |
| SSL days until expiry | `metrics['api.ssl_days_until_expiry']` | `<` | `14` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `method` | enum | one of: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`; default `GET` |
| `followRedirects` | boolean | default `true` |
| `authType` | enum | one of: `none`, `basic`, `digest`, `bearer`; default `none` |
| `authUsername` | string | — |
| `authPassword` | string | **secret** (returned redacted) |
| `bearerToken` | string | **secret** (returned redacted) |
| `requestBody` | string | **secret** (returned redacted) |
| `requestHeaders` | array | `Array of { key, value }` |
| `ipVersion` | enum | one of: `ip4`, `ip6` |
| `assertions` | array | `Array of { source, property, operator, value } (max 50)` |
| `extractions` | array | `Array of { name, path } JSONPath (max 20)` |
| `timeoutSeconds` | number | Per-check timeout in seconds |
| `transportMode` | enum | `auto` (default), `tcp` or `http3` |
| `expiryReminderDays` | array | Whole days (1–365, up to 5), e.g. `[14, 3]`: re-send an open, unacknowledged certificate-expiry incident's notification once when that many days are left; omit or `null` for none — see [reminders](/docs/monitors/certificates#reminders-before-expiry) |

## Page integrity

Defacement and tampering detection on a list of pages, against an accepted baseline.

- **Type value** (`type` field): `integrity`
- **Target**: https URL — e.g. `https://example.com`

Every finding is measured against a **fixed accepted baseline**, not the
previous check. The first check of each page records its baseline; after
an intended change (a deploy), accept the new state with
[`POST /v1/monitors/{id}/baseline`](/docs/api/monitors#page-integrity-baseline).
Scripts, frames and forms from the page's own site never count as new —
only third-party sources do.

**Default alert rules** (created automatically):

- Availability Check — `success == false` (critical)
- Unknown script source — `metrics['integrity.new_script_origins'] > 0` (critical)
- Form sends data elsewhere — `metrics['integrity.new_form_targets'] > 0` (critical)
- Tamper keywords — `metrics['integrity.keyword_hits'] > 0` (critical)
- Redirects elsewhere — `metrics['integrity.redirect_changed'] > 0` (critical)
- Security header weakened — `metrics['integrity.headers_weakened'] > 0` (critical)
- Third-party script changed — `metrics['integrity.scripts_changed_third_party'] > 0` (critical)
- Page content changed — `metrics['integrity.pages_changed'] > 0` (warning)
- Security headers changed — `metrics['integrity.headers_changed_other'] > 0` (warning)
- First-party script changed — `metrics['integrity.scripts_changed_first_party'] > 0` (warning)
- New data destination — `metrics['integrity.new_connect_origins'] > 0` (warning)
- New embedded frame — `metrics['integrity.new_iframe_origins'] > 0` (warning)
- New external links — `metrics['integrity.new_link_domains'] >= 5` (warning)
- Pages unreachable — `metrics['integrity.pages_unreachable_partial'] > 0` (warning)

The security rules (critical above, plus *New data destination*) close only
after 3 clean checks in a row, so one clean check from another location
never resolves a change that is served to some visitors only.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Up / Down (probe status) | `success` | `==` | `false` | critical |
| New third-party script origins | `metrics['integrity.new_script_origins']` | `>` | `0` | critical |
| New third-party form targets | `metrics['integrity.new_form_targets']` | `>` | `0` | critical |
| New tamper keywords in the page text | `metrics['integrity.keyword_hits']` | `>` | `0` | critical |
| Pages redirecting to a different host | `metrics['integrity.redirect_changed']` | `>` | `0` | critical |
| Security headers removed or loosened (CSP, HSTS, XFO, XCTO) | `metrics['integrity.headers_weakened']` | `>` | `0` | critical |
| Third-party scripts serving different code | `metrics['integrity.scripts_changed_third_party']` | `>` | `0` | critical |
| Largest visible-text change vs baseline (%) | `metrics['integrity.text_change_pct_max']` | `>=` | `25` | warning |
| Pages changed beyond the sensitivity | `metrics['integrity.pages_changed']` | `>` | `0` | warning |
| Security headers changed vs baseline | `metrics['integrity.headers_changed']` | `>` | `0` | warning |
| Security headers changed without being weakened | `metrics['integrity.headers_changed_other']` | `>` | `0` | warning |
| Own scripts serving different code | `metrics['integrity.scripts_changed_first_party']` | `>` | `0` | warning |
| Scripts serving different code (any origin) | `metrics['integrity.scripts_content_changed']` | `>` | `0` | warning |
| New data destinations (sites the page sends data to) | `metrics['integrity.new_connect_origins']` | `>` | `0` | warning |
| New embedded frame origins | `metrics['integrity.new_iframe_origins']` | `>` | `0` | warning |
| New external link domains | `metrics['integrity.new_link_domains']` | `>=` | `5` | warning |
| Pages that could not be fetched | `metrics['integrity.pages_unreachable']` | `>` | `0` | warning |
| Pages that could not be fetched while others answered | `metrics['integrity.pages_unreachable_partial']` | `>` | `0` | warning |
| Pages fetched | `metrics['integrity.pages_checked']` | `<` | `1` | warning |
| Pages without an accepted baseline | `metrics['integrity.baseline_missing']` | `>` | `0` | warning |

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `pages` | array | Absolute URLs to watch (1–50, same registrable domain as the target); defaults to `[target]`. The per-monitor page count is plan-limited |
| `textChangeThreshold` | number | Percent of visible text that may change before a page counts as changed (Low `50`, Normal `25`, High `10`) |
| `pageThresholds` | object | Per-page sensitivity: `{ "<page URL>": percent 1–100 }` for pages in `pages`; the others use `textChangeThreshold` |
| `ignoreSelectors` | array | CSS selectors (max 20 × 200 chars) removed before comparing text, links, forms and frames — never scripts |
| `checkHeaders` | boolean | Compare the pages' security headers with the baseline (default `true`) |
| `checkScriptContent` | boolean | Hash external scripts and report when the same URL serves different code (default `true`; plan-dependent) |
| `render` | boolean | Render every page in a real browser — catches scripts injected at runtime and the sites a page sends data to, and photographs pages that differ from their baseline. At most 10 pages; browser monitors are plan-limited |
| `trustedOrigins` | array | Script, frame and form sources never reported as new: `https://host`, or a bare host for any scheme |
| `keywords` | array | Extra tamper keywords added to the built-in list |
| `autoAcceptAfterHours` | enum | `0` (never, default), `1`, `3`, `6` or `24`: accept an unreviewed change as the new baseline after this long. The incident still opens at once |
| `autoAcceptSecurity` | boolean | Let `autoAcceptAfterHours` also accept security findings (default `false`) |
| `timeoutSeconds` | number | Per-check timeout in seconds (max `60`) |

## Browser check

A Playwright Test script (login, checkout, search) run in a sandboxed Chromium, step by step, with screenshots and a trace on failure. For a Lighthouse / Core Web Vitals audit of one page use type `browser`.

- **Type value** (`type` field): `playwright`
- **Target**: https URL — e.g. `https://shop.example.com` (the script reads it as `process.env.TARGET_URL`)

The script is validated on save: a syntax error, a disallowed import or more
than 10 tests is rejected with `400` naming the line. Test it first with
[`POST /v1/playwright/test-runs`](/docs/monitors/browser-checks/api). Runs,
their failed steps and the saved script versions have their own endpoints —
see [Browser checks](/docs/api/monitors#browser-checks).

**Default alert rules** (created automatically):

- Journey failed — `metrics['playwright.failed_tests'] > 0` (critical), after 2 failed runs in a row: the scheduled run and a confirmation re-run about a minute later. A run that passes on the in-run retry is *flaky*, not failed

There is no generic Availability Check on this type: a run that could not
start on our side is recorded as an infrastructure error and never opens an
incident.

**Alert metrics** — values for the `metric` field of [`POST /monitors/{id}/alert-rules`](/docs/api/alert-rules):

| Metric | Passes as `metric` | Op | Example value | Severity |
|---|---|---|---|---|
| Failed tests (after the in-run retry) | `metrics['playwright.failed_tests']` | `>` | `0` | critical |
| Journey duration (ms) | `metrics['playwright.duration_ms']` | `>` | `20000` | warning |
| Console errors | `metrics['playwright.console_errors']` | `>` | `0` | warning |
| Failed requests (4xx/5xx or network error) | `metrics['playwright.failed_requests']` | `>` | `0` | warning |
| Slowest Largest Contentful Paint across pages (ms) | `metrics['playwright.lcp_ms_max']` | `>` | `2500` | warning |
| Worst Cumulative Layout Shift across pages | `metrics['playwright.cls_max']` | `>` | `0.1` | warning |
| Passed only on retry (1 = flaky) | `metrics['playwright.flaky']` | `>` | `0` | warning |
| Script error (syntax, import, no test) | `metrics['playwright.script_error']` | `>` | `0` | critical |
| One step's duration (ms) | `metrics['playwright.step.<slug>.duration_ms']` | `>` | `3000` | warning |

`<slug>` is the step's key (lowercase letters, digits and `_`, made from the
step title); the Alerting tab's step-duration preset fills it in for you.

**Config fields** (`config` object on create/update):

| Key | Type | Notes |
|---|---|---|
| `script` | string | The Playwright Test file (TypeScript or JavaScript, at most 64 KB, 1–10 tests). Imports: `@playwright/test`, `otpauth`, `@faker-js/faker` and Node built-ins. Read organization variables as `process.env.NAME`; `TARGET_URL` is the check's URL |
| `timeoutSeconds` | number | Whole-journey timeout in seconds (10–180; the plan caps it with `maxPlaywrightTimeoutSeconds`); default `60` |
| `retry` | boolean | Retry a failed test once inside the same run; a pass on retry is reported as flaky, not failed; default `true` |
| `trace` | boolean | Keep a Playwright trace for failed runs and full-capture runs (30 days); default `true` |
| `runtime` | enum | `2026.09` (default). Playwright, Chromium and Node versions are reported with every run |

---

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