---
title: WatchFor reports API
description: GET /v1/reports — the full organization report for a period versus the previous one — and the endpoints that manage the weekly and monthly report emails.
canonical: https://watchfor.io/docs/api/reports
---

# WatchFor reports API

GET /v1/reports — the full organization report for a period versus the previous one — and the endpoints that manage the weekly and monthly report emails.

| Method | Path | Scope | operationId |
| --- | --- | --- | --- |
| GET | `/v1/reports` | read | `getReport` |
| GET | `/v1/reports/schedules` | read | `listReportSchedules` |
| PATCH | `/v1/reports/schedules/{frequency}` | write | `updateReportSchedule` |

The same engine renders the dashboard [Reports](/docs/reports) page and
the scheduled report emails, so the API returns exactly the numbers a
human sees.

## Get a report

`GET /v1/reports` — the organization report for a period, always
compared with the previous period of equal length: uptime, incidents,
MTTR/MTTA/MTBF, downtime, response-time trend, per-monitor rows (worst
uptime first), type-aware sections and plain-language insights.

```bash
curl "https://watchfor.io/api/v1/reports?period=7d" \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

### Query parameters

| Query | Meaning |
| --- | --- |
| `period` | `7d` (default) or `30d`, trailing from now. Ignored when `from`/`to` are given |
| `from`, `to` | Custom window — ISO 8601 or Unix epoch (seconds or milliseconds); both required together; at most 92 days; `to` is clamped to now. **Advanced-reports plans** |
| `monitor_ids` | Comma-separated monitor ids to scope the report to (first 200 used). **Advanced** |
| `tags` | Comma-separated monitor tags (first 50 used); combined with `monitor_ids` as a union. **Advanced** |
| `sections` | Comma-separated subset of the type-aware sections to generate: `locations`, `ssl`, `domains`, `cwv`, `heartbeat`, `blacklist`, `mcp`. Omit for all. **Advanced** |
| `response_time`, `monitors_table`, `insights` | Set to `false` to drop that block. **Advanced** |
| `incident_status` | Comma-separated subset of `acknowledged`, `resolved`, `open` (or `all`). Filters incident aggregates — counts, MTTR/MTTA, downtime, per-monitor incident column — to incidents in those states; `uptimePct` (every confirmed critical incident) and `checkSuccessPct` are unaffected. **Advanced** |

On plans without advanced reports, any scoping or selection parameter
returns `403 forbidden` with an upgrade message; `period=7d|30d` always
works. An invalid `sections` or `incident_status` token returns
`400 invalid_request` listing the allowed values.

### Response shape

```json
{
  "object": "report",
  "meta": {
    "organizationId": "6f0a…",
    "organizationName": "Acme Cloud",
    "generatedAtMs": 1756710000000,
    "period": { "fromMs": 1756105200000, "toMs": 1756710000000, "label": "Aug 25 – Sep 1, 2026" },
    "previousPeriod": { "fromMs": 1755500400000, "toMs": 1756105200000, "label": "Aug 18 – Aug 25, 2026" },
    "depth": "advanced",
    "typesPresent": ["http", "ssl", "heartbeat"]
  },
  "summary": {
    "uptimePct": { "current": 99.94, "previous": 99.99, "delta": -0.05 },
    "checkSuccessPct": { "current": 99.91, "previous": 99.98, "delta": -0.07 },
    "incidents": { "current": 2, "previous": 1, "delta": 1 },
    "criticalIncidents": { "current": 1, "previous": 0, "delta": 1 },
    "downtimeMs": { "current": 2520000, "previous": 0, "delta": 2520000 },
    "mttrMs": { "current": 1260000, "previous": 900000, "delta": 360000 },
    "mttaMs": { "current": 300000, "previous": 420000, "delta": -120000 },
    "mtbfMs": { "current": 302400000, "previous": 604800000, "delta": -302400000 },
    "longestIncident": { "monitorName": "api.acme.dev", "durationMs": 2520000, "startedAtMs": 1756350000000 },
    "totalChecks": 60480,
    "monitorsCovered": 3
  },
  "responseTime": {
    "avgMs": { "current": 342, "previous": 318, "delta": 24 },
    "minMs": 87, "maxMs": 4210,
    "slowChecks": { "current": 112, "previous": 74, "delta": 38 },
    "slowThresholdMs": 1000
  },
  "monitors": [
    { "monitorId": "…", "name": "api.acme.dev", "type": "http", "target": "https://api.acme.dev/health",
      "uptimePct": { "current": 99.82, "previous": 99.97, "delta": -0.15 },
      "downtimeMs": 2520000,
      "checkSuccessPct": 99.76,
      "avgMs": 512, "p95Ms": 1240, "incidents": 2, "hadMaintenance": false }
  ],
  "totalMonitors": 3,
  "sections": {
    "ssl": [ { "monitorId": "…", "name": "acme.dev certificate", "target": "acme.dev", "daysUntilExpiry": 27, "issuer": "R11", "tlsVersion": "TLS 1.3", "tlsLegacy": false } ],
    "heartbeat": [ { "monitorId": "…", "name": "nightly-backup", "missedCount": 1, "totalPings": 7, "incidents": 0 } ]
  },
  "gatedSections": [],
  "insights": [
    { "tone": "warning", "text": "Certificate for acme.dev certificate expires in 27 days.", "monitorId": "…" }
  ],
  "gatedInsightCount": 0
}
```

- Every headline number in `summary` (and `responseTime.avgMs`,
  `slowChecks`, per-monitor `uptimePct`) is a **compared value**
  `{ current, previous, delta }`; `previous` and `delta` are `null` when
  there is no baseline data. `mttrMs`, `mttaMs` and `mtbfMs` are `null`
  when nothing resolved / was acknowledged / occurred.
- `responseTime` is `null` when the organization has no latency-style
  monitors in the period.
- `sections` only contains keys for monitor types the organization
  actually has, and only at `advanced` depth. Section keys: `locations`,
  `ssl` (certificate expiry, issuer, TLS version, legacy-TLS flag),
  `domains` (domain expiry), `cwv` (Core Web Vitals), `heartbeat`
  (check-ins), `blacklist` (DNS blocklists), `mcp` (tool drift).
- `gatedSections` lists sections that exist for your monitors but are
  locked by the plan; `gatedInsightCount` counts insights withheld for
  the same reason. On `basic` depth the per-monitor table is truncated.
- `insights` are ordered most severe first; `tone` is `good`, `warning`,
  `critical` or `neutral`.
- Field names in the report follow the dashboard's camelCase document
  rather than the snake_case used by the rest of the API.

Try it without a key: [`GET /api/v1/sandbox/reports`](https://watchfor.io/api/v1/sandbox/reports).

## Scheduled report emails

Every organization has exactly two report-email cadences — **weekly**
(sent on Mondays) and **monthly** (sent on the 1st) — each with up to 5
recipients. Recipients who unsubscribed themselves stay on the list but
are skipped at send time and reported in `suppressed_recipients`.

The schedule object:

```json
{
  "object": "report_schedule",
  "frequency": "weekly",
  "enabled": true,
  "recipients": ["ops@example.com", "cto@example.com"],
  "suppressed_recipients": []
}
```

### List schedules

`GET /v1/reports/schedules` — both cadences, weekly first
(`has_more` is always `false`).

```bash
curl https://watchfor.io/api/v1/reports/schedules \
  -H "Authorization: Bearer wf_live_YOUR_KEY"
```

### Update a schedule

`PATCH /v1/reports/schedules/{frequency}` — `frequency` is `weekly` or
`monthly`; requires `write` scope. Body: `enabled` (boolean) and/or
`recipients` (full replacement list, max 5 valid email addresses) — at
least one of the two.

```bash
curl -X PATCH https://watchfor.io/api/v1/reports/schedules/monthly \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true, "recipients": ["ops@example.com"] }'
```

Semantics match the dashboard:

- enabling needs at least one recipient (`400` otherwise);
- sending an empty `recipients` list disables the cadence;
- adding the first recipient to an empty, disabled cadence enables it
  automatically.

The change is recorded in the [activity log](/docs/api/activity) as
`report_schedule.updated`.

## Also via

MCP tools `get_report`, `get_report_schedules`, `set_report_schedule`;
A2A skills `org-report` and `manage-report-emails`; CLI
`watchfor report --period 30d`. Background on how the numbers are
calculated: [Reports](/docs/reports).

---

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