Monitor kinds
A monitor is created with POST /orgs/{org}/monitors and a body like this:
{
"name": "Homepage",
"kind": "http",
"spec": { "url": "https://www.example.com/" },
"interval_s": 60,
"locations": { "mode": "regions", "regions": ["eu", "na"] },
"failure_threshold": 2,
"recovery_threshold": 2,
"reminder_interval_s": 3600,
"labels": ["prod"],
"contact_ids": ["con_01j9..."],
"paused": false
}
The common fields:
| Field | Required | Rules |
|---|---|---|
name | yes | 1-100 characters |
kind | yes | one of the nine kinds below; cannot change later |
spec | yes | the kind's fields (below); unknown fields are rejected |
interval_s | yes | 30, 60, 120, 300, 600, 900, 1800 or 3600, and at least your plan's minimum (below_plan_minimum); domain_expiry must use 86400; for heartbeat, the period between pings, 60-604800 (no plan minimum) |
locations | yes, except domain_expiry and heartbeat, which must leave it out | {"mode": "regions", "regions": [...]} (1-6 of eu, na, sa, as, oc, af) or {"mode": "locations", "codes": [...]} (2-12 codes from GET /locations). Regions expand to their active locations, capped at your plan's maximum per monitor; more codes than the maximum are refused (too_many_locations). At least 2 locations after expansion (too_few_locations). Exactly 2 is accepted with the warning two_locations: with two, an outage seen from one location can never alert. Three or more is better |
failure_threshold | no | 1-10, default 2: consecutive failed checks in a location before it counts as down |
recovery_threshold | no | 1-10, default 2: consecutive good checks before it counts as up again |
degraded_after_ms | no | http_adv only: 100-30000 and below the check's time budget; a slower response makes the monitor degraded. null (default) is off |
reminder_interval_s | no | 900-86400: repeat the down notification while an incident stays open and unacknowledged; null (default) = no reminders |
labels | no | at most 10, each 1-32 characters of a-z 0-9 - _ . |
contact_ids | no | contacts to notify (at most 100); omitted = every contact marked auto_attach |
paused | no | true creates the monitor paused |
Time budget. Each check has a time budget, computed from its spec and
returned as timeout_ms on the monitor. It must be at most
interval_s × 1000 − 10000 ms, so a 30-second monitor allows at most a
20-second budget (out_of_range on the spec's timeout_ms otherwise).
Hosts and URLs. A host is a hostname or an IP address. Hostnames are
stored in their ASCII (punycode) form, lower case. A hostname needs at least
two labels and may not end in localhost, local, internal, lan,
home.arpa, localdomain, test, example, invalid or onion. When
you save, the host is resolved: if every address is private, loopback,
link-local or otherwise reserved, the monitor is refused
(target_not_public); if it does not resolve at all it is accepted with the
warning does_not_resolve. Across your organisation, the checks of all
monitors with the same host may add up to at most 120 a minute
(host_rate_exceeded).
ip_family (every kind except domain_expiry): auto (default) uses
IPv6 and IPv4 as available (Happy Eyeballs; ICMP uses the first IPv4
address, or IPv6 when there is none), ipv4 or ipv6 only that family.
ipv6 monitors can only use locations with IPv6 (ipv6: true in
GET /locations).
Outcomes. Every check gives up, down, degraded or error. error
means our probe could not run the check (it says nothing about your
target) and never alerts. degraded makes a monitor degraded only for
http_adv, ssl_expiry, domain_expiry, imap and smtp; for the other
kinds it counts as up. A failing check gives an error_kind, listed per kind
below.
Defaults are stored with the monitor, so a later change of a default never changes your existing monitors. Responses return the spec with every default filled in.
http
A GET request. Up when the final status is 200-399 and, if keyword is
set, the keyword appears in the first 64 KiB of the body. Up to 5 redirects
are followed; TLS certificates are always verified.
| Field | Required | Default | Rules |
|---|---|---|---|
url | yes | http or https, at most 2048 bytes, no user:pass@, port 1-65535; a #fragment is dropped | |
keyword | no | null | 1-128 bytes, case-sensitive |
timeout_ms | no | 10000 | 1000-30000 |
ip_family | no | auto | auto, ipv4, ipv6 |
{ "kind": "http", "spec": { "url": "https://www.example.com/health", "keyword": "ok" } }
Failures: dns_failure, blocked_address, connect_timeout,
connection_refused, connection_reset, host_unreachable, tls_error,
cert_expired, timeout, protocol_error, status_mismatch,
assertion_failed (keyword missing), too_many_redirects,
redirect_blocked.
http_adv
Any method, headers, body, authentication, expected statuses and assertions on the response.
| Field | Required | Default | Rules |
|---|---|---|---|
url | yes | as for http | |
method | no | GET | GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS |
headers | no | [] | at most 32, 8 KiB together; each {"name", "value", "secret"} (below) |
body | no | null | at most 16 KiB; not allowed with GET or HEAD. Without a Content-Type header it is sent as application/json if it parses as JSON, else text/plain; charset=utf-8 |
auth | no | {"type": "none"} | {"type": "basic", "username", "password"} or {"type": "bearer", "token"} |
expected_status | no | ["200-299"] | 1-16 entries: "200", "200-299" or "2xx", codes 100-599 |
assertions | no | [] | at most 20 (below); all must pass |
follow_redirects | no | true | |
max_redirects | no | 5 | 0-10 |
verify_tls | no | true | false accepts any certificate chain |
cert_expiry_warn_days | no | 14 | 1-90: degraded (cert_expiring) when the certificate expires sooner; null turns the warning off |
timeout_ms | no | 10000 | 1000-30000 |
body_max_bytes | no | 65536 | 1024-262144: how much of the body is read for assertions |
ip_family | no | auto |
Headers. name is an HTTP token of at most 128 bytes; value at most
4096 bytes without CR, LF or NUL. Names must be unique, except Accept,
Cache-Control and X- headers. You may not set Host, Content-Length,
Transfer-Encoding, Connection, Keep-Alive, Upgrade, TE, Trailer,
Proxy-Authorization, Proxy-Connection, or Authorization while auth
is not none. Without your own User-Agent the probe sends
JustMonitorIt-Probe/<version> (+https://justmonitorit.com/probe; loc=<location>).
Secrets. auth.password, auth.token and the value of a header with
"secret": true are write-only. Responses show "password_set": true,
"token_set": true and "secret": true, "value": null instead. When you
update a monitor, omitting a secret or sending null keeps the stored one;
"" clears it. A secret header's name must be unique.
Assertions:
| Assertion | Passes when |
|---|---|
{"type": "body_contains", "value": "..."} | the value (1-256 bytes, case-sensitive) is in the body |
{"type": "body_not_contains", "value": "..."} | the value is not in the body (only the part read, see body_max_bytes) |
{"type": "body_regex", "pattern": "..."} | the regular expression (at most 512 characters, Rust regex syntax) matches the body |
{"type": "json_path", "path": "$.status", "op": "equals", "value": "ok"} | the body is JSON and the value at path satisfies op |
{"type": "header", "name": "content-type", "op": "contains", "value": "json"} | the response header satisfies op |
json_path paths use $, .name, ['name'] and [index] only (no
wildcards or filters), at most 256 characters. Its op is exists,
not_exists, equals, not_equals, contains (substring of a string, or
an element of an array), lt or gt (numbers). exists and not_exists
take no value; the others need one; equality is typed ("1" is not 1).
header ops are exists, not_exists, equals, contains and regex;
the name is case-insensitive, the value comparison case-sensitive.
Slow responses: set the monitor's degraded_after_ms to make it degraded
(slow_response) when a response takes longer.
{
"kind": "http_adv",
"spec": {
"url": "https://api.example.com/v1/health",
"method": "GET",
"headers": [ { "name": "X-Api-Key", "value": "s3cret", "secret": true } ],
"expected_status": ["200"],
"assertions": [
{ "type": "json_path", "path": "$.status", "op": "equals", "value": "ok" },
{ "type": "header", "name": "content-type", "op": "contains", "value": "application/json" }
]
},
"degraded_after_ms": 1500
}
Failures: as http, plus cert_expiring (degraded) and slow_response
(degraded).
tcp
Up when a TCP connection to the port opens. Nothing is sent.
| Field | Required | Default | Rules |
|---|---|---|---|
host | yes | hostname or IP address | |
port | yes | 1-65535 | |
timeout_ms | no | 5000 | 500-30000 |
ip_family | no | auto |
{ "kind": "tcp", "spec": { "host": "db.example.com", "port": 5432 } }
Failures: dns_failure, blocked_address, connect_timeout,
connection_refused, host_unreachable.
icmp
Ping. Sends count echo requests; down when the share lost reaches
loss_threshold_pct (the default 100 means down only when every echo is
lost).
| Field | Required | Default | Rules |
|---|---|---|---|
host | yes | hostname or IP address | |
count | no | 3 | 1-10 |
interval_ms | no | 500 | 200-1000, between echo requests |
timeout_ms | no | 2000 | 500-10000, wait after the last request |
payload_bytes | no | 56 | 16-1024 |
loss_threshold_pct | no | 100 | 1-100 |
ip_family | no | auto |
The time budget is 3000 + (count − 1) × interval_ms + timeout_ms.
{ "kind": "icmp", "spec": { "host": "203.0.113.10", "count": 5, "loss_threshold_pct": 60 } }
Failures: packet_loss, host_unreachable, dns_failure,
blocked_address.
ssl_expiry
Connects, completes a TLS handshake and checks the certificate. Down if the
handshake fails or the certificate has expired; degraded
(cert_expiring) when it expires within cert_expiry_warn_days. Use it for
TLS services that are not websites, or to watch a certificate on its own.
| Field | Required | Default | Rules |
|---|---|---|---|
host | yes | hostname (also sent as SNI) or IP address | |
port | no | 443 | 1-65535 |
cert_expiry_warn_days | no | 14 | 1-90 |
verify_tls | no | true | false checks only the expiry of whatever certificate is served |
timeout_ms | no | 10000 | 1000-30000 |
ip_family | no | auto |
{ "kind": "ssl_expiry", "spec": { "host": "www.example.com", "cert_expiry_warn_days": 21 } }
Failures: transport failures as tcp, tls_error, cert_expired,
timeout, protocol_error; cert_expiring (degraded).
domain_expiry
Looks up the domain's registration at its registry over RDAP, once a day
(interval_s must be 86400). Down when the registration has expired
(domain_expired) or the domain is not registered (domain_not_found);
degraded (domain_expiring) within expiry_warn_days. If the registry
cannot be reached, the result is error (rdap_unavailable) and you are
not alerted.
| Field | Required | Default | Rules |
|---|---|---|---|
domain | yes | the registered domain (example.com), not an IP address; its TLD must have an RDAP server (unsupported_tld otherwise, for example ch and de) | |
expiry_warn_days | no | 30 | 1-365 |
timeout_ms | no | 10000 | 1000-30000 |
{ "kind": "domain_expiry", "interval_s": 86400, "spec": { "domain": "example.com" } }
imap and smtp
Opens a session with a mail server, without logging in or sending mail:
the greeting, EHLO (SMTP) or CAPABILITY (IMAP), optionally STARTTLS,
then a polite goodbye. With TLS, the certificate is checked as for
ssl_expiry.
| Field | Required | Default | Rules |
|---|---|---|---|
host | yes | ||
port | yes | 1-65535 (IMAP usually 993 or 143; SMTP 25, 465 or 587) | |
tls | yes | implicit (TLS from the start), starttls or none | |
verify_tls | no | true | forced to true with tls: none |
cert_expiry_warn_days | no | 14 | 1-90, or null for no warning; forced to null with tls: none |
timeout_ms | no | 10000 | 1000-30000 |
ip_family | no | auto |
{ "kind": "smtp", "spec": { "host": "mx.example.com", "port": 587, "tls": "starttls" } }
SMTP on ports 25 and 465 runs only from probes whose provider allows outbound mail ports; where there is none, another location stands in.
Failures: transport and TLS failures as ssl_expiry, timeout,
protocol_error (detail greeting, ehlo, starttls_unsupported,
starttls_refused, capability or line_too_long); cert_expiring
(degraded).
heartbeat
Checks nothing itself: your job (a cron script, a backup, a queue worker)
requests the monitor's ping URL each time it runs, and the monitor goes
down (heartbeat_missed) when no ping arrives within interval_s plus
grace_s. The next ping brings it back up. A new heartbeat does not
alert until its first ping. interval_s is the period between pings
(60-604800); leave out locations, and the thresholds are 1.
| Field | Required | Default | Rules |
|---|---|---|---|
grace_s | no | 300 | 60-604800: how late a ping may be |
{ "kind": "heartbeat", "interval_s": 86400, "spec": { "grace_s": 1800 } }
The response's ping_url is the URL to request, with GET, HEAD or
POST (a body is ignored), for example at the end of a crontab line:
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://dash.justmonitorit.com/ping/...
To report a failed run, request the same URL followed by /fail: the
monitor goes down at once (heartbeat_failed), and the next successful
ping brings it back up:
0 3 * * * if /usr/local/bin/backup.sh; then curl -fsS -m 10 --retry 5 -o /dev/null https://dash.justmonitorit.com/ping/...; else curl -fsS -m 10 --retry 5 -o /dev/null https://dash.justmonitorit.com/ping/.../fail; fi
GET /orgs/{org}/monitors/{mon}/pings lists the newest pings (100 are
kept): when each arrived, success or fail, the method, the client's
address and user agent.
Only members and above see ping_url; a viewer gets null. It answers
200 once the ping counted, 404 for an unknown URL, and 429
or 503 (with Retry-After) when it should be tried again. At most 60
pings a minute per URL count. The newest ping is the monitor's
state.last_ping_at. Anyone with the URL can ping the monitor, so keep it
private; to change it, delete the monitor and create a new one.
Trying a spec first
POST /orgs/{org}/monitors/preview-check runs a spec once from one location
and returns the result without creating anything:
{ "kind": "http", "spec": { "url": "https://www.example.com/" }, "location": "lon1" }
It applies the same validation as creating a monitor. A heartbeat has
nothing to try (not_allowed).