# Build reports and summaries

This page is for turning monitoring data into an answer: "how did we do this week", "what went down yesterday",
"did we meet the SLA", "is the page getting slower". It lists the calls that answer each question, the limits on
each, and four recipes you can run by hand, from a script or as an AI assistant. All calls need the `monitor:read`
scope unless noted; all times are Unix seconds.

For how the figures are calculated, see [Uptime percentage and SLA](/incidents/uptime-sla/) and
[Reading a monitor's results and incidents](/incidents/reading-results/).

## Which call answers which question

| Question | API | MCP | Notes |
|---|---|---|---|
| Uptime %, downtime and SLA of chosen monitors over a window | `GET /monitor/result/summary?monitor=ID1,ID2&from=F&to=T` | `get_uptime_summary` | One row per monitor, ordered by monitor id - there is no `sort` (see [Sort a summary](#sort-a-summary-worst-first)). `monitor=` is required here (up to 500 ids). |
| One uptime figure for the whole account | `GET /monitor/result/summary?groupBy=account&from=F&to=T` | `get_uptime_summary` with `monitor=""` and `groupBy="account"` (the tool requires `monitor`; empty means the whole account) | One row, summed over every monitor (or over `monitor=` if you send it). Window up to 30 days, up to 2,000 monitors. |
| Uptime of every monitor, as a list | `GET /monitor?expand=uptime&from=F&to=T` | - | Adds `uptime` (percent, `null` when there is no data) to each monitor row. Page through with `limit` up to 500. |
| A daily, weekly or monthly series | add `bucket=hour\|day\|week\|month` to the summary | `get_uptime_summary` with `bucket` | One row per monitor per bucket. Buckets are aligned to UTC; weeks start on Monday. |
| SLA met or missed, error budget left | add `sla=99.9` to the summary, or set the monitor's `slaTarget` | `get_uptime_summary` with `sla` | Adds `slaTarget`, `slaMet`, `errorBudgetSecRemaining`. |
| Response time and its phases | add `metrics=responseTime,dns,connect,tls,ttfb,transfer` to the summary | `get_uptime_summary` with `metrics` | Each point is `{t, value, p95, samples}`, in milliseconds. |
| How many outages opened and closed | add `expand=incidentCounts` to the summary | `api_request` with `GET /monitor/result/summary` and `expand=incidentCounts` (`get_uptime_summary` has no `expand`) | Adds `incidents{opened, restored}` per row: episodes that opened / closed inside the window or bucket. |
| The outages themselves, with cause and duration | `GET /monitor/incident?from=F&to=T` (all monitors) or `GET /monitor/{id}/incident` | `list_incidents` | Any incident that overlaps the window, newest first. No window cap. |
| One outage in detail | `GET /monitor/incident/{id}` (`expand=recheck`) | `get_incident` | Adds the transitions that opened and closed it and which locations confirmed it. |
| The failing checks inside one outage | `GET /monitor/incident/{id}/check` | - | Plan feature (full event log); otherwise `403 package_limit`. |
| A monitor's up/down timeline | `GET /monitor/{id}/span?from=F&to=T` | `get_monitor` with `expand="spans"` | Every continuous up or down period; down spans name their incident. |
| The check log (raw results) | `GET /monitor/{id}/result?from=F&to=T` | `list_monitor_results` | Newest first; `expand=metrics` adds measurements, `expand=recheck` what each location saw. |
| Raw results across monitors | `GET /monitor/result?monitor=ID1,ID2&from=F&to=T` | - | Without `monitor=` the window is capped at 24 hours. |
| What is down right now | `GET /monitor?state=down&expand=lastIncident` | `list_monitors` with `state="down"` | |
| Who was alerted, and when | `GET /contact/notification` | - | Scope `contact:read`. |
| Planned work during the window | `GET /maintenance?from=F&to=T` | `list_maintenance` | |
| A formatted document to send someone | `POST /monitor/report` (a job) | `generate_report` | PDF, CSV, XML or HTML. Scope `monitor:write`. |

## Limits to plan around

- **Default window.** On every windowed read, an omitted `to` is now and an omitted `from` is 30 days before `to`.
  Send both to be explicit.
- **Raw results: 30 days per request.** A wider `from`/`to` on `GET /monitor/{id}/result` or `GET /monitor/result`
  is refused with `422 invalid_range`; read longer periods one window at a time. Without `monitor=`, the account-wide
  `GET /monitor/result` accepts at most 24 hours and defaults to the last 24 hours.
- **Raw results are grouped.** Consecutive identical results are stored as one row with a `checkCount`, so a row
  is not always one check. Uptime and incident figures are computed from state changes and are exact regardless.
- **Summary limits.** At most 1,000 buckets per request and at most 50,000 (monitors x buckets) cells; a request
  over either is refused (`422`), never silently truncated - narrow the window or the monitor set, or widen the
  bucket. `groupBy=account` covers at most 30 days and 2,000 monitors.
- **Incidents are not clipped.** An incident that started before `from` or ended after `to` is returned whole, with
  its full `start`, `end` and `durationSec`. To count downtime inside the window, use the summary's `downSec`
  (which is clipped) or clip each incident yourself: `min(end, to) - max(start, from)`, with `end` = now for an open
  incident.
- **No server-side ranking.** The summary has no `sort`; rank rows yourself - see
  [Sort a summary, worst first](#sort-a-summary-worst-first).
- **Paging.** Lists return `data`, `nextCursor` and `hasMore`. Keep calling with `cursor=<nextCursor>` while
  `hasMore` is true. The API takes `limit` 1-500 (default 50); the MCP tools take 1-50 (default 20). A cursor only
  works with the same query that produced it. For long id lists, send the query as a JSON body to `POST <path>/q`.
- **Generated reports.** Up to 500 monitors per report. Defaults: format `pdf`, the last 30 days, sections `stats`.
  The report id stays downloadable for 7 days.

## Reading a summary row

`GET /monitor/result/summary` returns one row per monitor (per bucket):

| Member | Meaning |
|---|---|
| `monitorId`, `from`, `to` | Which monitor and which window or bucket. Absent `monitorId` on an account roll-up row. |
| `upSec`, `downSec` | Seconds up and down inside the window, maintenance included. |
| `totalSec` | Seconds with a known state (`upSec + downSec`). Shorter than the window when the monitor did not exist yet or was paused. |
| `maintenance.upSec`, `maintenance.downSec` | The parts that fell inside maintenance windows. `maintenance.downSec` is the excused downtime. |
| `checks` | `total`, `up`, `down`, `maintenanceUp`, `maintenanceDown` - checks recorded. |
| `downSpans` | Down periods that started in the window. |
| `uptimePercent` | The result: `upSec / (totalSec - maintenance.downSec)`, `null` when nothing was measured. **Quote it as given**; on the whole-window figure it comes from the stored daily statistics the rest of the product shows. |
| `slaTarget`, `slaMet`, `errorBudgetSecRemaining` | When a target applies. A negative budget means the target was missed by that many seconds. |
| `incidents` | With `expand=incidentCounts`: `opened`, `restored`. |
| `metrics` | With `metrics=`: one series per metric, points `{t, value, p95, samples}`. |
| `monitors`, `scope`, `sampled` | Account roll-up only: how many monitors, `account` or `selection`, and whether the timing figures were sampled. |

An incident row carries `id`, `monitorId`, `start`, `end`, `durationSec`, `state` (`open`, `resolved`), `severity`
(`minor` under 5 minutes, `major` 5 to 60 minutes, `critical` 1 hour or more), `cause` (the error, with a
`codename`), `underMaintenance`, `comment` and `checkCount`. For an open incident, `end` is the last moment the
monitor was seen down.

## Sort a summary, worst first

`GET /monitor/result/summary` has **no `sort` parameter**, and neither does `get_uptime_summary`. It returns rows
ordered by monitor id and then by bucket start - a stable order for paging, not a ranking. (`GET /monitor` takes
`sort=` over `name`, `state`, `type`, `interval`, `lastChange`, `url`, `tags` or `created`, but not over uptime.)
To rank monitors, fetch every row and sort on your side:

1. **Collect the ids.** `GET /monitor?limit=500` (follow `nextCursor`), or MCP `list_monitors` (50 per page).
2. **Fetch the rows.** `GET /monitor/result/summary?monitor=ID1,...&from=F&to=T` in batches of up to 500 ids (add
   `expand=incidentCounts` for outage counts). Follow `nextCursor` until `hasMore` is false - a page holds at most
   500 rows through the API, 50 through MCP. Keep the window and ids identical while paging; a cursor from a
   different query is refused.
3. **Sort.** Ascending by `uptimePercent` puts the worst first. Tie-break by `downSec` descending, then by
   `incidents.opened` descending. Rows **without** `uptimePercent` (nothing was measured in the window - a paused
   or brand-new monitor) have no figure at all: put them last and label them "no data", never treat them as 0 %.
4. **Report** `uptimePercent` exactly as returned; do not recompute it from the seconds.

For "worst by outages" instead, sort by `incidents.opened` descending. The same client-side sort applies to
`GET /monitor?expand=uptime` rows (by `uptime`, `null` = no data).

## Recipe 1: a weekly uptime summary for all monitors

Goal: last week's uptime per monitor, the account-wide figure, and the outages, for a weekly status email.

1. **Pick the window in the user's time zone.** Read the zone from `GET /account` (`timezone`). Last week =
   Monday 00:00 to the next Monday 00:00 in that zone, converted to Unix seconds: `F` and `T`.
2. **Per-monitor uptime:**

   ```http
   GET /monitor?expand=uptime&from=F&to=T&limit=500
   ```

   Follow `nextCursor` until `hasMore` is false. Each row carries `id`, `name`, `url`, `state` and `uptime`.
3. **The account-wide figure and outage counts:**

   ```http
   GET /monitor/result/summary?groupBy=account&from=F&to=T&expand=incidentCounts
   ```

   One row: `uptimePercent`, `downSec`, `monitors`, `incidents.opened`.
4. **The outages:**

   ```http
   GET /monitor/incident?from=F&to=T&sort=monitor&expand=monitor&limit=500
   ```

   `sort=monitor` groups them by monitor name; `expand=monitor` adds each monitor's name and url.
5. **Write it up.** Lead with the account figure, then the monitors below their SLA or below 100%, lowest first,
   then one line per outage (monitor, start in the user's zone, duration, cause). Say which outages fell inside
   maintenance (`underMaintenance`), and that maintenance downtime is not counted against uptime.

With MCP: `get_uptime_summary(monitor="", groupBy="account", from=F, to=T)` for the account figure (the tool
requires `monitor`; an empty value with `groupBy="account"` means the whole account);
`get_uptime_summary(monitor="ID1,ID2,...", from=F, to=T)` in batches of monitor ids from `list_monitors` for
per-monitor rows, sorted by you (see [Sort a summary](#sort-a-summary-worst-first)); `list_incidents(from=F, to=T)`
for the outages. For outage counts per monitor use
`api_request(method="GET", path="/monitor/result/summary", query="monitor=ID1,ID2&from=F&to=T&expand=incidentCounts")`.

## Recipe 2: what went down yesterday, and for how long

1. **Window:** yesterday 00:00 to today 00:00 in the user's zone, as Unix seconds `F`, `T`.
2. **List the incidents that touched that day:**

   ```http
   GET /monitor/incident?from=F&to=T&expand=monitor&limit=500
   ```

   MCP: `list_incidents(from=F, to=T)`.
3. **For each incident** report the monitor, `start` and `end` in the user's zone, `durationSec` in human units
   (for example "12 min 30 s"), `severity` and `cause.codename`. Mark `state: "open"` ones as still down. An incident
   that began the day before or ended today still appears whole; say so, and clip its duration to yesterday if the
   user asked "how long yesterday":

   `downInWindow = min(end, T) - max(start, F)`

   using `end` = now for an open incident (its `end` is only the last moment it was seen down). No field carries the
   clipped value. For a per-monitor total that is already clipped to the window, read `downSec` from
   `GET /monitor/result/summary?monitor=ID&from=F&to=T` (downtime inside maintenance windows included;
   `maintenance.downSec` is that part).
4. **Explain the long ones:** `GET /monitor/incident/{id}?expand=recheck` (MCP `get_incident`) shows which locations
   confirmed the outage and the error each saw.
5. **If the user wants a record,** add their note with `POST /monitor/incident/{id}/comment`
   `{"comment": "Database failover"}` (MCP `comment_incident`, scope `monitor:write`). A new comment replaces the
   previous one.

Only confirmed outages are incidents. A single failed check that the recheck did not confirm is not one - see
[How down detection works](/monitors/down-detection/).

## Recipe 3: a monthly SLA report for one monitor

1. **Window:** the first day of the month 00:00 to the first day of the next month 00:00, in the user's zone.
2. **The SLA figures:**

   ```http
   GET /monitor/result/summary?monitor=ID&from=F&to=T&sla=99.9&expand=incidentCounts
   ```

   Read `uptimePercent`, `slaMet`, `errorBudgetSecRemaining`, `downSec`, `maintenance.downSec` and
   `incidents.opened`. Leave out `sla=` to measure against the monitor's own `slaTarget`. MCP:
   `get_uptime_summary(monitor="ID", from=F, to=T, sla=99.9)`.
3. **A day-by-day breakdown** (UTC days): the same call with `bucket=day`.
4. **The outages behind the number:** `GET /monitor/ID/incident?from=F&to=T`.
5. **A document to send:**

   ```http
   POST /monitor/report
   Idempotency-Key: 6f1c1d2e-monthly-sla-2026-09
   {
     "monitorIds": ["ID"],
     "from": F,
     "to": T,
     "format": "pdf",
     "sections": ["stats", "outages", "incidents"],
     "timezone": "Europe/Berlin"
   }
   ```

   The answer is `202` with a `jobId` and a `Retry-After`. Poll `GET /job/{jobId}` until the state is `succeeded`;
   the finished job names the report and its download url. `GET /monitor/report/{id}` describes it and
   `GET /monitor/report/{id}/content` returns the file. Sections: `state`, `stats`, `outages`, `incidents`, `log`.
   MCP: `generate_report(monitorIds="ID", from=F, to=T, format="pdf", sections="stats,outages,incidents")`, then
   `wait_for_job`; hand the user the download url rather than trying to read the file.

For a report that arrives on its own every month, subscribe an email contact instead:
`PUT /monitor/ID/report/CONTACT_ID` `{"frequencies": ["monthly"]}` (MCP `subscribe_contact` with
`frequencies="monthly"`). See [Schedule report subscriptions](/reports/scheduling/).

## Recipe 4: the response-time trend of a page

1. **Pick the monitor** that checks the page - a Website/HTTPS or API monitor records every timing phase.
2. **Daily averages and the slow tail over the last 30 days:**

   ```http
   GET /monitor/result/summary?monitor=ID&from=F&to=T&bucket=day&metrics=responseTime,ttfb
   ```

   Each row's `metrics.responseTime` is a list of points: `t` (the bucket's midpoint), `value` (mean, milliseconds),
   `p95` (95th percentile) and `samples` (how many checks). MCP:
   `get_uptime_summary(monitor="ID", from=F, to=T, bucket="day", metrics="responseTime,ttfb")`.
3. **Choose the bucket for the window:** `hour` for a few days (the 1,000-bucket limit allows about 41 days),
   `day` for weeks to months, `week` or `month` for longer.
4. **Read it carefully:** compare `p95` with `value` - a rising p95 with a flat mean means occasional slow checks.
   A bucket with few `samples` is weak evidence. A `null` value means nothing was measured in that bucket.
5. **Find the slow phase:** `metrics=dns,connect,tls,ttfb,transfer` splits the time into DNS lookup, connection,
   TLS handshake, waiting for the first byte and download.
6. **Drill into one slow check:** `GET /monitor/ID/result?from=...&to=...&expand=metrics` (MCP
   `list_monitor_results` with `expand="metrics"`).

Other monitor types record fewer phases or none; their series come back empty (`samples: 0`). For a full page
load in a real browser, use a [Page speed monitor](/monitors/types/page-speed/) and read its per-check results.

## Presenting the numbers

- Quote `uptimePercent` exactly as returned; do not recompute it from the seconds.
- Show times in the user's time zone (`GET /account` -> `timezone`) and durations in human units.
- Say which window each figure covers - daily, weekly and monthly uptime are different numbers.
- Mention maintenance: downtime inside a window that suppresses statistics is excused, and incidents that began in
  one are flagged `underMaintenance`.
- For "why", link the user to the monitor's statistics page (`/sites/stats/{id}`) or the
  [Uptime reports](/reports/uptime-reports/) page in the app.

## Related

- [Uptime percentage and SLA](/incidents/uptime-sla/)
- [Reading a monitor's results and incidents](/incidents/reading-results/)
- [The account-wide uptime report](/reports/uptime-reports/)
- [Do anything in HostTracker: task map](/reference/operator-task-map/)
