---
title: Checks, uptime & run now
description: Uptime and response-time statistics, raw check history with a failure filter and per-location breakdown, and on-demand checks for a monitor.
canonical: https://watchfor.io/docs/api/checks-uptime
---

# Checks, uptime & run now

Uptime and response-time statistics, raw check history with a failure filter and per-location breakdown, and on-demand checks for a monitor.

A **check** is one execution of a monitor from one location. These
endpoints read the results and let you trigger one on demand.

| Method | Path | Scope | operationId |
| --- | --- | --- | --- |
| GET | `/v1/monitors/{id}/uptime` | read | `getMonitorUptime` |
| GET | `/v1/monitors/{id}/checks` | read | `listMonitorChecks` |
| GET | `/v1/monitors/{id}/checks/{run_id}` | read | `getCheckDetail` |
| POST | `/v1/monitors/{id}/check-now` | write | `runMonitorCheckNow` |

## Uptime & response times

`GET /v1/monitors/{id}/uptime?period=24h` — incident-based uptime and
response-time statistics for one period. `period` is `1h`, `24h`
(default), `7d`, `30d` or `90d`; an unknown value returns `400` listing
the valid periods. Uptime and incidents cover any period on every plan; the
check counts and response times read your plan's chart history (see
[History limits](#history-limits)).

```bash
curl "https://watchfor.io/api/v1/monitors/0b0f6a41-…/uptime?period=7d" \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

```json
{
  "object": "uptime",
  "monitor_id": "0b0f6a41-…",
  "period": "7d",
  "uptime_percent": 100,
  "downtime_ms": 0,
  "check_success_percent": 99.983,
  "total_checks": 10080,
  "successful_checks": 10078,
  "failed_checks": 2,
  "avg_response_time_ms": 100,
  "min_response_time_ms": 67,
  "max_response_time_ms": 1216,
  "last_failure_at": "2026-08-22T04:12:00Z",
  "uptime_24h": 100,
  "uptime_7d": 99.983,
  "uptime_30d": 99.995
}
```

- `uptime_percent` uses the [one formula](/docs/monitors#how-uptime-is-calculated)
  the whole product uses: `100 − share of the period inside confirmed
  critical incidents`, with scheduled maintenance neutral and the window
  clamped to the monitor's creation time (or its last history reset). A failed check that never became
  a confirmed incident does **not** reduce it. `downtime_ms` is the time
  those incidents covered. `uptime_percent` is `null` only before the
  monitor existed.
- `check_success_percent` is the **reachability** signal, not a health
  signal: the share of checks where the probe reached the target and got a
  response. A response that then fails a rule — wrong status code, failed
  assertion, expired certificate — counts as reached here and surfaces as an
  incident instead, so a monitor can report `uptime_percent: 60` with
  `check_success_percent: 100`. Use it for flakiness (timeouts, refused
  connections, DNS failures), and `uptime_percent` for health. It is
  `null` when the period contains no checks (a paused or brand-new monitor
  has no data, and no data is **not** 100%).
- `avg_response_time_ms`, `min_response_time_ms` and
  `max_response_time_ms` cover **successful checks only** — a failed check
  (a timeout, a refused connection) has no response time, so it neither
  pulls the average toward zero nor counts as the minimum. They are `null`
  when no check in the period succeeded.
- Time inside activated [maintenance windows](/docs/api/maintenance-windows)
  marked "exclude from uptime/SLA" is neutral for uptime and removed from
  both numerator and denominator of check success.
- `incident_count` and `incidents[]` are the outages behind the number: every
  counting incident that overlapped the period (`id`, `severity`, `status`,
  `started_at`, `resolved_at`) with the `downtime_ms` it contributed inside
  the window — their sum is `downtime_ms`.
- `series=true` (periods up to `7d`) adds `response_time_series` — the
  period on a real time axis, oldest first, in fixed buckets
  (`series_bucket_seconds`: 300 for `24h`, 900 for `7d`). Only buckets that
  held checks appear: `{ at, checks, failed, avg_ms, max_ms }`, with
  `avg_ms` / `max_ms` over the bucket's successful checks and `null` when
  every check in it failed — draw a gap there.
- `uptime_24h`, `uptime_7d` and `uptime_30d` are convenience headline
  numbers (same incident-based formula) regardless of the requested `period`.
- A monitor that has never been scheduled returns zeros/`null` without
  the headline fields.
- If the metrics store is temporarily unreachable the endpoint returns
  `500 internal_error` ("retry shortly") rather than partial numbers.

## Check history

`GET /v1/monitors/{id}/checks` — individual check results, newest
first, plus a per-location performance breakdown for the same window.

| Query | Meaning |
| --- | --- |
| `hours` | Window size, 1–720 (default 24); must be an integer. Cut to your plan's check history when it reaches further |
| `limit` | Rows of raw checks, 1–100 (default 50); anything else returns `400` |
| `success` | `false` returns only failed checks (one-call failure forensics); `true` only passing ones; omit for all |
| `cursor` | The previous page's `next_cursor`, to keep reading the same window |

```bash
curl "https://watchfor.io/api/v1/monitors/0b0f6a41-…/checks?hours=48&success=false" \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

```json
{
  "object": "list",
  "data": [
    {
      "object": "check",
      "timestamp": "2026-08-24T18:48:25.000Z",
      "success": false,
      "duration_ms": 30012,
      "location": "eu-frankfurt-1",
      "location_name": "Frankfurt, Germany",
      "location_country_code": "DE",
      "error": "request timed out after 30s",
      "status_code": 0
    }
  ],
  "locations": [
    {
      "location": "eu-frankfurt-1",
      "location_name": "Frankfurt, Germany",
      "location_country_code": "DE",
      "checks": 1440,
      "success_percent": 99.931,
      "uptime_percent": 99.931,
      "avg_response_time_ms": 100,
      "min_response_time_ms": 67,
      "max_response_time_ms": 1216
    }
  ],
  "has_more": true,
  "next_cursor": "MjAyNi0wOC0yNFQxODo0ODoyNVp8…"
}
```

- `timestamp` is ISO 8601 / RFC 3339 in UTC (`2026-08-24T18:48:25.000Z`),
  like every other time in the API — on checks, check details and
  browser-check runs alike. (Until October 2026 these three used
  `YYYY-MM-DD HH:MM:SS`, with or without a `Z`.)
- `run_id` addresses the exact probe run; `target` is what was probed.
  Type-specific signals appear only when recorded: `rtt_avg_ms` and
  `packet_loss_pct` (ICMP), `dns_rcode`, `dns_answers_count`, `dns_lookup_ms`
  (DNS), `tcp_connect_ms`, `tcp_tls_handshake_ms`, `tcp_match` (TCP),
  `smtp_connect_ms`, `smtp_tls_supported`, `smtp_open_relay`, `smtp_ptr_valid`
  (SMTP), `sitemap_*` (sitemap), `body_bytes`. Yes/no signals (`tcp_match`,
  `smtp_tls_supported`, `smtp_open_relay`, `smtp_ptr_valid`, `sitemap_found`)
  are `true`/`false`.
- `location` is the probe's name; `location_name` (the city, as the
  dashboard shows it) and `location_country_code` (ISO 3166-1 alpha-2) come
  with it when the probe is one of WatchFor's locations. `status_code` is present for HTTP-style
  types only. `error` is `null` on success.
- `locations` summarizes every location that ran checks in the window,
  independent of `limit` and the `success` filter. `success_percent` is
  that location's successful ÷ total checks — a location has no
  incident-based uptime, which is why the older `uptime_percent` key there
  is a deprecated alias of the same value (removed in the next API version).
  The location's `avg_response_time_ms`, `min_response_time_ms` and
  `max_response_time_ms` cover its successful checks only, and are `null`
  when none succeeded there in the window.
- Pagination is cursor-based within the `hours` window: when `has_more`
  is `true`, pass `next_cursor` as `?cursor=` with the **same** `hours`
  and `success` values to get the next `limit` rows — newest first, no
  overlap, no gaps. A malformed cursor returns `400`.
- The `success=false` view is what an AI agent (or you) should read
  first when investigating an incident — each failed check carries the
  location and the probe's error text.

## One check in detail

`GET /v1/monitors/{id}/checks/{run_id}` — everything one probe run
recorded: the row from the list plus the stored result, exactly as the
dashboard's probe-result view shows it. Take `run_id` from the list
above.

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

```json
{
  "object": "check_detail",
  "run_id": "154535409e21f640",
  "monitor_id": "0b0f6a41-…",
  "timestamp": "2026-09-05T19:01:36.000Z",
  "location": "eu-frankfurt-1",
  "target": "https://example.com",
  "success": true,
  "duration_ms": 26,
  "error": null,
  "status_code": 200,
  "result": {
    "status_code": 200,
    "headers": [
      { "name": "Content-Type", "value": "text/html; charset=UTF-8" },
      { "name": "Set-Cookie", "value": "•••••• redacted" }
    ],
    "body_snippet": "<!doctype html>…",
    "http_version": "HTTP/2.0"
  },
  "result_bytes": 1842,
  "redacted_count": 1,
  "truncated": false,
  "stored": true
}
```

- `result` is the probe's stored output as a JSON object (a string when
  the probe recorded plain text); its shape depends on the monitor type.
  Sensitive values — passwords, tokens, cookies, `Authorization` headers,
  credentials in URLs, well-known API-key shapes — are redacted on the
  server before the response is built, and `redacted_count` says how
  many were. The raw payload is never returned, to anyone. For the mail
  types (`imap`, `pop3`, `smtp`, `email_roundtrip`) it carries the
  per-stage timings, TLS version and certificate, capabilities and the
  login outcome (`auth.ok`, `auth.error_class`) — the round trip adds
  delivery time, inbox or spam, SPF/DKIM/DMARC verdicts, hops and spam
  score. Passwords and message contents are never recorded.
- `result_bytes` is the size of the stored output before redaction.
  `truncated` is `true` when the probe stopped capturing the body at its
  size cap, so `result` holds the part it kept.
- `stored: false` (with `result: null`) means the check is known but no
  result body is stored for it — the probe recorded no output, or the
  body has since been dropped. The metadata fields are still filled in.
- Failed checks always keep their output. For **successful** `http`,
  `smtp`, `mtr`, `brotli` and `favicon` checks the output is kept as a
  short-lived sample: about one successful check every 10 minutes per
  location, the last ~8 per location, for at most 2 days. A successful
  check of those types that was not sampled returns `stored: false`; other
  types keep their output on every check.
- The type-specific signals are the same as on the list row; `error` is
  `null` on success.
- A `run_id` that is not this monitor's — or whose check is older than
  your plan's check history — returns `404 not_found` (the message names
  the plan's window); a malformed one returns `400`.

## History limits

Every read here follows your plan's
[data retention](/docs/organization/data-retention): the checks list and
check details use the **check history**, the uptime endpoint's check counts
and response times the **charts & reports history**. A window that reaches
further is cut, not refused — a `200` with these headers:

| Header | Meaning |
| --- | --- |
| `X-History-Limit-Days` | The plan's window for this data in days, or `unlimited` |
| `X-History-Earliest` | The oldest timestamp the plan returns (ISO 8601) |
| `X-History-Clamped` | `true` when the requested window was cut |

```bash
curl -sD - -o /dev/null "https://watchfor.io/api/v1/monitors/0b0f6a41-…/checks?hours=720" \
  -H "Authorization: Bearer wf_live_YOUR_KEY" | grep -i '^x-history'
# x-history-limit-days: 30
# x-history-earliest: 2026-09-06T12:00:00Z
```

## Run a check now

`POST /v1/monitors/{id}/check-now` — requires `write` scope. Dispatches
one immediate check without changing the schedule. The call is
asynchronous: it returns `202 Accepted` and the result appears in
[check history](#check-history) within the probe timeout (typically well
under a minute).

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

```json
{
  "object": "check_now",
  "monitor_id": "0b0f6a41-…",
  "status": "dispatched",
  "message": "Check dispatched — the result appears in GET /v1/monitors/{id}/checks shortly"
}
```

The budget is your plan's *Run now* allowance — per monitor, per hour,
shared with the dashboard's "Run check now" action and visible as
`run_now_per_hour` on [`GET /v1/plan`](/docs/api/plan). The check that
follows an [update that changes how the monitor connects](/docs/api/monitors#update-a-monitor)
(`"check_queued": true`) is separate and does not use this budget.

| Situation | Response |
| --- | --- |
| Allowance for this monitor used up | `429 rate_limited` with `Retry-After` and `error.next_allowed_at` (an ISO 8601 time). This is the per-monitor budget, not the API rate limit — the `X-RateLimit-*` headers still describe that one |
| Plan has no on-demand checks | `403 forbidden` |
| Monitor is paused | `409 conflict` — resume it first |
| Monitor not scheduled yet (just created) | `409 conflict` — try again in a moment |
| `heartbeat` monitor | `400 invalid_request` — heartbeats are checked by your own pings |

Send an `Idempotency-Key` header to retry safely: a retry with the same
key (after a timeout, say) replays the original `202` with
`Idempotency-Replay: true` instead of being refused by the hourly budget
the first call used. See [idempotency](/docs/api/operations).

Each dispatch is recorded in the [activity log](/docs/api/activity) as
`monitor.check_now`.

## Also via

MCP tools `get_monitor_uptime`, `get_monitor_checks`, `run_check_now`;
A2A skills `explain-monitor` and `manage-monitor` (`run-check`); CLI
`watchfor checks <monitor-id> --failed`.

---

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