Skip to main content

No matching sections.

Status pages

A status page is the public face of your monitoring: a permanent slug, an ordered set of components, and the incidents you declare on it. Everything below sits under /statuspage and reads statuspage:read / statuspage:write - its own scope family, so a token can manage the page without touching monitors or contacts.

Two kinds of "incident" live on this surface, and they are not the same thing. A monitor episode (Results, /monitor/incident) is derived - the checking engine opened it because a monitor went down. A status-page incident is declared - it is what the owner says is happening, in the owner's words, and it exists whether or not a check failed.

Pages

OperationScope
GET /statuspagestatuspage:read
GET /statuspage/{id}statuspage:read
POST /statuspagestatuspage:write
PATCH /statuspage/{id}statuspage:write
DELETE /statuspage/{id}statuspage:write

The list is small and closed - it answers the standard envelope but never pages - and it sorts by created, title or slug (see the query surface for the spelling).

GET /statuspage/{id}  ·  200 OK
{
  "id": "…",
  "slug": "acme",
  "title": "Acme status",
  "componentCount": 4,
  "unresolvedIncidents": 0,
  "hasPassword": false,
  "created": 1785712608,
  "customDomain": null,
  "settings": {
    "homepageUrl": "https://acme.example.com",
    "theme": "…", "themeColor": "…",
    "density": "wide",            // wide | compact
    "logoAlignment": "left",      // left | center
    "showGroups": true,
    "robotsIndex": true,
    "hideBranding": false,
    "autoAddMonitors": false,
    "features": [ /* named tokens - never a bitmask */ ],
    "slaTarget": 99.9
  },
  "components": [ /* ordered - see below */ ]
}
The item read is a strict superset of the list row. It carries everything the list carries plus created, componentCount and unresolvedIncidents - so following a list row to its detail never loses a member. (It published fewer members than its own list row until 2026-08-17, which is the sort of thing you only discover after writing the client.)

POST /statuspage takes {slug, title} - both required - plus optional settings and components, and answers 201 + Location; the plan's page cap is enforced. The slug is permanent: it is the page's public address, so pick it deliberately. PATCH changes the title and settings members - only what you send changes, and an explicit null clears a member rather than being ignored. DELETE answers the standard receipt and frees the slug:

DELETE /statuspage/{id}  ·  200 OK
{
  "id": "…", "deleted": true, "type": "statusPage",
  "slug": "acme", "title": "Acme status",
  "cascaded": { "components": 4, "incidents": 12, "subscribers": 38, "templates": 3 }
}

Components

PUT /statuspage/{id}/component replaces the whole component set. It is a PUT rather than a PATCH on purpose: the array's order is the display order, and there is no coherent way to express "insert here" in a partial update.

Carry each surviving component's id in the new array. A component you send without an id is a NEW component - and per-component subscriptions belong to the id, so dropping it silently unsubscribes everyone who was watching that component.

An item is {id?, monitorId? | thirdParty, name?, group?, manualState?}:

MemberMeaning
idPresent = keep this component (and its subscriptions). Absent = create one.
monitorIdThe monitor whose state drives it. Must be one of your own monitors.
thirdPartyThe alternative to monitorId: a component nothing checks, whose state you pin by hand.
nameThe display name. It is name, not label - a component is an entity, and every entity on this surface carries a name.
groupOptional grouping heading.
manualStateoperational | degraded | down, and third-party components only - a monitored component's state comes from its monitor. Spelled manualStatus until 2026-08-17.

The plan's component cap is enforced on the whole set, and Idempotency-Key is honoured.

Declared incidents

OperationNotes
GET /statuspage/{id}/incidentNewest first, each with its full timeline[].
GET /statuspage/{id}/incident/{incidentId}One incident, timeline and postmortem.
POST /statuspage/{id}/incidentDeclare one. Idempotency-Key REQUIRED
PATCH /statuspage/{id}/incident/{incidentId}Fix title / kind / components; set or clear the postmortem. The timeline is untouched.
POST /statuspage/{id}/incident/{incidentId}/timelineAppend an entry. Idempotency-Key REQUIRED
DELETE /statuspage/{id}/incident/{incidentId}Delete it. No subscriber notice, matching the dashboard.
Declaring an incident, and appending to its timeline, FAN OUT to the page's confirmed subscribers. That is why those two are the only status-page operations that refuse without an Idempotency-Key: a keyless retry after a timeout announces the incident twice, to everybody. Mint one key per logical declaration and reuse it for that declaration's retries only - see Jobs & idempotency.

The write body is {title, state, message, componentIds?, kind?, scheduledStart?, scheduledEnd?, impact?}. The initial state seeds the timeline together with message - so one call both declares the incident and posts its first update.

POST /statuspage/{id}/incident  ·  201 + Location
curl -X POST 'https://api2.host-tracker.com/statuspage/PAGE_ID/incident' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: incident-2026-08-17-checkout' \
  -d '{
    "title": "Checkout is failing for some customers",
    "state": "investigating",
    "message": "We are looking into elevated errors on checkout.",
    "componentIds": ["…"],
    "impact": "major"
  }'
the incident that comes back
{
  "id": "…",
  "title": "Checkout is failing for some customers",
  "state": "investigating",      // investigating | identified | monitoring | resolved
  "kind": "incident",             // incident | maintenance
  "impact": "major",              // minor | major
  "created": 1785712608,
  "resolvedAt": null,
  "scheduledStart": null, "scheduledEnd": null,
  "componentIds": ["…"],
  "componentNames": ["Checkout"],
  "postmortem": null,
  "timeline": [
    { "state": "investigating", "message": "We are looking into elevated errors on checkout.", "at": 1785712608 }
  ]
}
The lifecycle member is state, on the row, in every timeline entry and in both write bodies. It was status until 2026-08-17, when this family joined the surface-wide rule that state means "where something is in its lifecycle" - the same word a monitor, a job, a maintenance window and an instant check use. There is no alias: the old spelling binds nothing and the old member is simply absent. The affected components likewise ride as componentIds + componentNames (was componentLabels).

A maintenance is the same resource with kind: "maintenance" and a Unix-seconds window in scheduledStart/scheduledEnd.

The timeline

An incident is a story, not a flag, and the timeline is where it is told. Each entry is {state, message, at}, appended with:

POST /statuspage/{id}/incident/{incidentId}/timeline
curl -X POST 'https://api2.host-tracker.com/statuspage/PAGE_ID/incident/INCIDENT_ID/timeline' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: incident-…-update-3' \
  -d '{ "state": "resolved", "message": "Checkout is fully recovered." }'

Posting resolved stamps the incident's resolvedAt - once; a later entry does not move it.

The path was …/update until 2026-08-17, and the operation was called addStatusPageIncidentUpdate - a verb in a url whose every other segment is a resource, and one that read as "update the incident" beside the incident's own PATCH door. The old path answers 405 with allowed[], never a silent accept.

Incident templates

Presets for the incidents you declare repeatedly: GET and POST /statuspage/{id}/template ({title, message, defaultImpact?}, 201 + Location), and DELETE /statuspage/{id}/template/{templateId}. There is deliberately no update: a template is cheap to re-create, and an editable preset is a preset two people can disagree about.

Membership of THIS page is checked on delete, so a 200 always means the template really was here. Its receipt carries no cascaded block at all - a template cascades to nothing, and a block of zeroes would suggest it might.

Subscribers

OperationNotes
GET /statuspage/{id}/subscriberEmails (with their double-opt-in state) and push channels.
POST /statuspage/{id}/subscriberAdd a push channel: {kind: webhook|slack|teams, url, componentId?}. Plan subscriber cap enforced.
DELETE /statuspage/{id}/subscriber/{subscriberId}Remove one of any kind. 404 when it is not this page's.
An email subscriber cannot be added through the API, by design. Email subscription is double-opt-in: the address has to confirm for itself, from the page. The API can read email subscribers (and their confirmedAt) and remove them; it cannot add one, because an API that could would be a way to sign somebody else up.

componentId scopes a subscriber to one component, so a channel can follow just the part it cares about. A row reads {id, kind, email?, url?, componentId?, confirmedAt?, created}.

What stays in the dashboard

This family covers configuration, components, incidents, templates and subscribers. Three things are deliberately not here, and are managed in the web dashboard:

  • The public rendering - the page itself, its status.json and its badges. Those are the public surface, not this API.
  • The page password.
  • The custom-domain lifecycle - customDomain is readable here, but claiming and verifying one is a dashboard flow.
Declaring through the API notifies exactly as declaring from the dashboard does. Same email with per-recipient unsubscribe links, same Webhook/Slack/Teams channel posts, same component-scoped delivery, same feature flag gating the email channel. Delivery is best-effort: a failing subscriber never fails your write.