---
title: WatchFor status pages API
description: Create and manage public status pages and their components, and publish, update and close incident announcements from your own tooling.
canonical: https://watchfor.io/docs/api/status-pages
---

# WatchFor status pages API

Create and manage public status pages and their components, and publish, update and close incident announcements from your own tooling.

A **status page** is the public face of your monitoring at
`https://watchfor.io/stats/<slug>` (or your own subdomain). Its
**components** group monitors into the services visitors see;
**announcements** are the manual incident notices you post on it.
Background: [status pages](/docs/status-pages).

| Method | Path | Scope | operationId |
| --- | --- | --- | --- |
| GET | `/v1/status-pages` | read | `listStatusPages` |
| POST | `/v1/status-pages` | write | `createStatusPage` |
| GET | `/v1/status-pages/{id}` | read | `getStatusPage` |
| PATCH | `/v1/status-pages/{id}` | write | `updateStatusPage` |
| DELETE | `/v1/status-pages/{id}` | write | `deleteStatusPage` |
| GET | `/v1/status-pages/{id}/announcements` | read | `listAnnouncements` |
| POST | `/v1/status-pages/{id}/announcements` | write | `createAnnouncement` |
| GET | `/v1/status-pages/{id}/announcements/{announcementId}` | read | `getAnnouncement` |
| PATCH | `/v1/status-pages/{id}/announcements/{announcementId}` | write | `updateAnnouncement` |
| DELETE | `/v1/status-pages/{id}/announcements/{announcementId}` | write | `deleteAnnouncement` |

The status page object:

```json
{
  "id": "9a1d…",
  "object": "status_page",
  "slug": "acme",
  "name": "Acme Cloud Status",
  "description": "Live status of all Acme services",
  "public": true,
  "password_protected": false,
  "auto_publish_incidents": true,
  "dim_untracked_days": false,
  "theme": "auto",
  "url": "https://watchfor.io/stats/acme",
  "component_count": 2,
  "overall_status": "degraded",
  "incident_count": 7,
  "components": [
    { "id": "c0a1…", "object": "status_page_component", "name": "Website", "description": null,
      "position": 0, "display_mode": "rolled_up", "public_cause": "cause", "monitor_ids": [], "monitor_tags": ["prod"],
      "status": "degraded",
      "monitors": [
        { "id": "0b0f6a41-…", "name": "Marketing site", "type": "http" },
        { "id": "3c7e2d10-…", "name": "Checkout", "type": "browser" }
      ],
      "third_party": null },
    { "id": "c0a2…", "object": "status_page_component", "name": "GitHub", "description": null,
      "position": 1, "display_mode": "rolled_up", "public_cause": "hidden", "monitor_ids": [], "monitor_tags": [],
      "status": "up", "monitors": [],
      "third_party": { "service_slug": "github", "components": ["Git Operations"] } }
  ],
  "created_at": "2026-07-01T08:00:00.000Z",
  "updated_at": "2026-08-20T16:45:00.000Z"
}
```

`components` is included on `GET /v1/status-pages/{id}` and on create;
the list endpoint carries only `component_count`. The **live** fields —
`overall_status`, `incident_count` and each component's `status`,
`monitors` and `third_party` — are what the public page shows right now
and come only with `GET /v1/status-pages/{id}`; create and update
responses return the stored page. They are computed by the same render
visitors get (cached per page for up to a minute; edits through the API
refresh it immediately).

| Field | Meaning |
| --- | --- |
| `overall_status` | The page banner: `operational`, `degraded`, `major_outage` or `maintenance`. Driven by the components' rolled-up statuses and by page-wide announcements (a `critical` one reads `major_outage`, a `major` one `degraded`); third-party components never tip it — a provider's outage is not yours. `null` if the live status could not be computed |
| `incident_count` | Announcements on the page — incident and maintenance notices, removed ones excluded |
| `components[].status` | The component's rolled-up status as rendered: `up`, `degraded`, `down`, `maintenance` (a member monitor is under an active maintenance window), `paused`, `unknown` |
| `components[].monitors` | The monitors the component resolves to right now — `monitor_ids` plus every monitor carrying any of `monitor_tags`, alphabetical. Empty for third-party components. A server appears as its host monitor (`type: "host"`) |
| `components[].display_mode` | `rolled_up` (one row), `expanded` (every monitor listed) or `collapsible_group` |
| `components[].public_cause` | How much an auto-published incident explains: `hidden` (generic "service disruption"), `cause` (a plain-words category such as "Disk space running low") or `full` (the detected value). Set in the dashboard — see [incident details on the public page](/docs/status-pages/components#incident-details-on-the-public-page) |
| `components[].third_party` | `{ service_slug, components }` when the component mirrors an external provider's public status page (`components` lists the provider's sub-components shown; empty = the whole service); `null` for your own components |

## List and get pages

`GET /v1/status-pages` — all pages, oldest first (not paginated; a `sort`
parameter returns `400`). `GET /v1/status-pages/{id}` — one page with its
components. `url` is always the canonical `https://watchfor.io/stats/<slug>`
form — the `<slug>.watchfor.io` alias serves the same page.

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

## Create a page

`POST /v1/status-pages` — requires `write` scope.

| Field | Required | Notes |
| --- | --- | --- |
| `slug` | yes | 3–50 lowercase letters, digits and dashes; globally unique across WatchFor. Reserved words (`status`, brand, system and abuse-prone names) are refused |
| `name` | yes | 1–120 characters |
| `description` | no | Up to 2000 characters |
| `isPublic` | no | Default `true` |
| `autoPublishIncidents` | no | Default `true` — confirmed incidents on component monitors appear automatically |
| `dimUntrackedDays` | no | Default `true` — days before a component's monitor existed show as no data |
| `theme` | no | `light`, `dark` or `auto` (default — follows each visitor's system setting) |
| `components` | no | Up to 50 of `{ name (1–120), description? (≤500), monitorIds? (≤500), monitorTags? (≤20) }`, positioned in array order. To put a server on a component, pass the host's `monitor_id` from [`GET /v1/hosts`](/docs/api/hosts) in `monitorIds` |

```bash
curl -X POST https://watchfor.io/api/v1/status-pages \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "acme",
    "name": "Acme Cloud Status",
    "components": [
      { "name": "Website", "monitorTags": ["prod"] },
      { "name": "API", "monitorIds": ["0b0f6a41-…"] }
    ]
  }'
```

Returns `201`. A slug that is already taken, or that is a reserved word,
returns `409 conflict` (the message says which: "The slug "status" is
reserved and can't be used for a customer status page — choose another
one"); a malformed slug (too short, too long, invalid characters) is
`400`. The plan's status-page limit returns `403`. Monitor ids outside your organization
are dropped from components rather than rejected. Password protection,
logo upload and custom subdomains are configured in the dashboard.
Supports `Idempotency-Key`.

## Update and delete a page

`PATCH /v1/status-pages/{id}` — partial: `slug`, `name`, `description`
(`null` clears), `isPublic`, `autoPublishIncidents`, `dimUntrackedDays`,
`theme`. A slug change re-checks uniqueness and reserved words (`409`)
and the old URL stops serving immediately. Component editing is dashboard-only for now.

`DELETE /v1/status-pages/{id}` returns `200` with
`{ "object": "deleted", "id": "…", "deleted": true }`.

Both require `write` scope; the public page cache refreshes immediately.

## Announcements

Manual incident notices on the page — the classic use is announcing a
problem, posting progress updates and the all-clear straight from your
incident tooling. Maintenance-type notices are created by
[maintenance windows](/docs/api/maintenance-windows) (dashboard
publishing), not here.

The announcement object:

```json
{
  "id": "e7b2…",
  "object": "announcement",
  "type": "incident",
  "title": "Degraded API performance",
  "body": "We are investigating elevated error rates.",
  "status": "investigating",
  "severity": "major",
  "affected_component_ids": ["c0a1…"],
  "affected_monitor_ids": [],
  "scheduled_start": null,
  "scheduled_end": null,
  "source_incident_id": null,
  "resolved_at": null,
  "created_at": "2026-08-24T10:20:00.000Z",
  "updated_at": "2026-08-24T10:20:00.000Z",
  "updates": [
    { "id": "1f9c…", "body": "We are investigating elevated error rates.",
      "status": "investigating", "created_at": "2026-08-24T10:20:00.000Z" }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `updates` | The public timeline, oldest first — one entry per posted update (the opening body is the first). Each carries the status it moved the announcement to |
| `affected_monitor_ids` | Narrows the impact to specific monitors inside the affected components; empty = every monitor of those components. Auto-published notices name the triggering monitor |
| `scheduled_start` / `scheduled_end` | The planned window — set only on `type: "maintenance"` notices, which [maintenance windows](/docs/api/maintenance-windows) create; `null` on incidents |
| `source_incident_id` | The [incident](/docs/api/incidents) an auto-published notice came from; `null` for notices posted by hand or through this API |

### List announcements

`GET /v1/status-pages/{id}/announcements` — newest first; `limit` 1–100
(default 50), `has_more` indicates more exist. Removed announcements are
excluded. Every row carries its `updates` timeline.

### Create an announcement

`POST /v1/status-pages/{id}/announcements` — requires `write` scope.

| Field | Required | Notes |
| --- | --- | --- |
| `title` | yes | 1–200 characters |
| `body` | yes | 1–5000 characters — becomes the first timeline entry |
| `status` | no | `investigating` (default), `identified`, `monitoring`, `resolved` |
| `severity` | no | `minor` (default), `major`, `critical` |
| `affectedComponentIds` | no | Component ids of **this** page; any other id returns `400` |

```bash
curl -X POST https://watchfor.io/api/v1/status-pages/9a1d…/announcements \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Degraded API performance",
    "body": "We are investigating elevated error rates.",
    "severity": "major",
    "affectedComponentIds": ["c0a1…"]
  }'
```

Returns `201`. Supports `Idempotency-Key`.

### Post an update

`PATCH /v1/status-pages/{id}/announcements/{announcementId}` — requires
`write` scope. Body: `body` (1–5000) **and** `status`, both required.
Appends an entry to the public timeline and moves the announcement to
that status; `status: "resolved"` closes it and sets `resolved_at`.
Editing the title or severity is dashboard-only.

```bash
curl -X PATCH https://watchfor.io/api/v1/status-pages/9a1d…/announcements/e7b2… \
  -H "Authorization: Bearer wf_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "body": "A fix has been deployed; error rates are back to normal.", "status": "resolved" }'
```

### Get and remove

`GET …/announcements/{announcementId}` returns one announcement.
`DELETE …/announcements/{announcementId}` removes it from the public
page (soft delete) and returns `200` with the deletion confirmation.

All announcement actions are recorded in the
[activity log](/docs/api/activity) (`status_page.incident_created`,
`status_page.incident_updated`, `status_page.incident_deleted`).

## Related

- Public, keyless machine endpoints for a page (status badge and JSON) are
  described in [status page embeds](/docs/status-pages/embed).
- The [WatchFor status page](https://status.watchfor.io) itself is
  built with these features.

---

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