Status pages: upstreams, maintenance from CI, SLA reports and the in-app banner

Embed
Preview of the card
<a href="https://forumulate.com/guides/status-depth"><img src="https://forumulate.com/guides/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/36aa4df0-1e95-4cef-8ae9-795169441345/pages/4a9f4b5b-1864-4653-b373-f901b4517599/social-card.svg?v=3e7ae33eec8b" alt="Status pages: upstreams, maintenance from CI, SLA reports and the in-app banner" width="600" height="315"></a>

A status page says what is wrong with your product. This guide covers the parts that connect it to everything around it: the services you depend on, the pipeline that deploys you, the people who want a number for the month, and the app your users are actually looking at.

Upstream components

Your product rides on other services — a payments provider, an identity provider, a CDN, or another of your own products. Declare them once and the page shows their state beside yours, and their incidents as yours:

PUT /api/orgs/{org}/spaces/{status}/upstreams
[
  { "label": "Payments", "source": { "kind": "statuspage", "url": "https://status.stripe.com" },
    "components": ["API"] },
  { "label": "Sign-in", "source": { "kind": "space", "org": "acme-id", "space": "status" } },
  { "label": "CDN", "source": { "kind": "statuspage", "url": "https://status.example-cdn.com",
    "format": "instatus" } }
]
  • Any provider. kind: "statuspage" reads any Statuspage-compatible summary.json — Atlassian Statuspage, Instatus, Better Stack, a self-hosted page, anyone's. format picks the reader: statuspage (default), instatus, betterstack, or feed for an RSS/Atom status feed (Statuspage's history.rss, ours, AWS's — posts that share a link are one incident, each post one of its updates). Pasting the page's address is enough.

  • Our own pages are a source too: kind: "space" with an org and space (id or slug), in this organization or another. They are read in-process, not over the network — use this rather than the page's URL for a page hosted here, custom domain or not. Another organization's page must be public, or its owner must grant yours (see Suite status page).

  • A platform by name. { "kind": "org", "org": "atlas" } means that organization's public status page here, so a product that signs in with Atlas shows "Upstream: Sign-in degraded" with nothing but { "label": "Sign-in", "source": { "kind": "org", "org": "atlas" } }. atlas, cookiemunch, watchtower and forumulate all publish one. It is stored as the kind: "space" form naming that page, and reads back that way. An organization with no public status page answers 404 saying so (use kind: "statuspage" with its address). One with several asks you to name the page.

  • Components: the source's components by id or exact name. Omit them to follow the source page overall.

  • Each upstream becomes component rows on your page (grouped under its label; each row's name links the source's page). They are read at once, then every minute — a conditional GET (If-None-Match / If-Modified-Since), so an unchanged source answers 304. A source that keeps failing backs off (up to eight minutes) and the page keeps its last word rather than painting their bars red for our network. One of our pages is also re-read the moment it changes, so its incidents reach your page in seconds. display decides what the rows draw (default status).

  • "Upstream: X degraded." While an upstream is not operational the page, widget.json and the in-app banner print that sentence, with "via <source>" linking the source's own page.

  • Incidents are mirrored, with every update. An open incident on the source appears on your page as "Upstream: <label> — <title>"; each update the source posts is posted here once, in order, moving the state as theirs moves; when the source resolves it, it is resolved here with the source's own closing words (from our pages, or a Statuspage's incidents.json). Every mirrored incident carries mirroredFrom — { upstreamId, label, source, url }, the source's name and its page for the incident — and the reader shows "Mirrored from <source>". Your subscribers are told through their one subscription to your page — subscribe once, hear about every dependency. Turn it off per upstream with mirrorIncidents: false.

  • GET …/upstreams lists them with their live reading, the source's name and page as last read (sourceName, sourceUrl) and why the last read failed (error; denied when the source no longer lets your page read it). An upstream left out of the next PUT is removed with its rows; its mirrored incidents are resolved.

  • A page that already reads yours (directly or through others) cannot be made your upstream: mirroring it back would echo each page's incidents into the other (422). Our pages never serve their own mirrored incidents to a mirror, so a chain (Cookie Munch → Referalli → the suite) does not repeat itself.

Suite status page

A suite page shows several products at once — each product's own status space, composed on one page — with every product's incidents on it as they happen. It is an ordinary status space whose upstreams are the products, with relation: "product": on a suite page a product is not upstream of anything, it is part of the page, so its notice reads "Cookie Munch degraded" and its mirrored incidents "Cookie Munch: Elevated consent errors".

PUT /api/orgs/{suite-org}/spaces/{suite-status}/upstreams
[
  { "label": "Cookie Munch", "relation": "product",
    "source": { "kind": "space", "org": "cookiemunch", "space": "status" } },
  { "label": "Referalli", "relation": "product",
    "source": { "kind": "space", "org": "referalli", "space": "status" } }
]

A product page can list another product as a dependency in the same way — Referalli's page showing "Consent (Cookie Munch)":

PUT /api/orgs/referalli/spaces/status/upstreams
[
  { "label": "Consent (Cookie Munch)",
    "source": { "kind": "space", "org": "cookiemunch", "space": "status" } }
]

A Cookie Munch incident is then on the suite page and on Referalli's page within seconds (pushed), and within a few minutes at worst (the minute poll is the backstop). Each product page stays the place its incidents are written; the suite and the dependants only mirror them.

Who may compose whom. A product page that is public (and not behind a reader sign-in or a network allow-list) may be mirrored by anyone — its summary.json is public anyway. A page that is private or gated may be mirrored only by its own organization's pages and by the organizations its owner names:

PUT /api/orgs/cookiemunch/spaces/status/status/mirror-grants
{ "organizations": ["acme-suite"] }

GET …/status/mirror-grants lists them, says whether the page is public, and counts how many pages in how many organizations mirror it now (counts only: which organizations read a page is theirs to say). Taking an organization off the list stops its mirror at once, and its mirrored incidents are closed with "no longer shares its status". A mirror sees what an anonymous reader would: components and incidents limited to an audience, drafts and unpublished post-mortems stay out. In the studio: Upstream services (the list, with "This page treats it as: a product of this suite") and Mirroring consent. CLI: forumulate status upstreams set --data @suite.json, forumulate status mirror-grants set --data '{"organizations":["acme-suite"]}'.

A product hosted elsewhere joins the same way with kind: "statuspage" and its address — any Statuspage-compatible page, Instatus, Better Stack, or a feed — read by polling instead of push.

Maintenance windows from CI

forumulate status maintenance --org acme --space status \
  --from 2026-11-02T22:00:00Z --to 2026-11-02T23:00:00Z \
  --components api,dashboard --title "Database upgrade" --key "$CI_PIPELINE_ID"

POST /api/orgs/{org}/spaces/{status}/status/maintenance takes the window the way a pipeline has it: from, to, components by id, slug or name, and an optional key. The same key again updates that window instead of announcing a second one, so a retried job is harmless (201 created, 200 updated). The window starts and completes itself at its times; subscribers are told as for any maintenance (notifySubscribers: false to skip).

Incident templates

Saved incidents live at …/status/templates; forumulate status templates list and forumulate status templates use <id> open one in a step.

The SLA report

GET …/status/sla-report?days=30&target=99.9 measures every component over the period against the target: uptime, met or breached, the downtime budget and what was used, and the incidents in the period. sections is the same report laid out for a PDF (headings, columns, rows). …/sla-report.csv gives the rows as CSV. The target comes from the request, else the page's slaTarget setting, else the organization's status.sla_target, else 99.9. CLI: forumulate status sla, forumulate status sla csv --out sla.csv.

The in-app banner

<script src="https://status.acme.com/api/public/orgs/acme/spaces/status/status/banner.js" async></script>

While an incident, maintenance window or upstream problem is active, a one-line bar names it and links to your page. It makes one request (to widget.json beside it), loads nothing from anyone else, and stores nothing — dismissing it lasts the page view — so it needs no cookie consent. By default it overlays the top edge (no layout shift); to place it in the flow, reserve an element and name it: data-target="#status-slot". data-position="bottom", data-show, data-accent, data-scheme and data-locale work as on the floating widget.

Private events

Machines can tell staff things the public should not see: POST …/status/events with an events key (forumulate status event-keys create "CI", status:write on that page only) or a partner link carrying status:write.

{ "id": "evt_123", "type": "webhook_endpoint.failing", "subject": "https://hooks.acme.com/fm",
  "state": "failing", "title": "Webhook endpoint is failing", "detail": "HTTP 503 for 1h" }

The same id twice is stored once. Staff read them at …/status/events (?open=true for unacknowledged) and acknowledge them. A page can also receive our own webhook-health events by setting eventFeeds.webhookHealth: true in its settings. Events never reach the public page or its subscribers.

In the API

Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go SDKs are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP agent server — its api_* family tools, or call_api. Where a CLI command or a hand-made MCP tool exists too, the last column names it. Operation names link to the API reference.

Status Depth

Operation

Request

What it does

Shortcuts

StatusDepth_listUpstreams

GET /orgs/{orgId}/spaces/{spaceId}/upstreams

List the services this status page depends on

forumulate status upstreams list<br>client.status.depth.listUpstreams()

StatusDepth_replaceUpstreams

PUT /orgs/{orgId}/spaces/{spaceId}/upstreams

Replace the services this status page depends on

forumulate status upstreams set<br>client.status.depth.replaceUpstreams()

StatusDepth_patchUpstreams

PATCH /orgs/{orgId}/spaces/{spaceId}/upstreams

Change the services this status page depends on

client.status.depth.patchUpstreams()

StatusDepth_listMirrorGrants

GET /orgs/{orgId}/spaces/{spaceId}/status/mirror-grants

Who may mirror this status page

forumulate status mirror-grants list<br>client.status.depth.listMirrorGrants()

StatusDepth_replaceMirrorGrants

PUT /orgs/{orgId}/spaces/{spaceId}/status/mirror-grants

Replace who may mirror this status page

forumulate status mirror-grants set<br>client.status.depth.replaceMirrorGrants()

StatusDepth_maintenance

POST /orgs/{orgId}/spaces/{spaceId}/status/maintenance

Schedule a maintenance window (from CI)

forumulate status maintenance<br>client.status.depth.maintenance()

StatusDepth_slaReport

GET /orgs/{orgId}/spaces/{spaceId}/status/sla-report

The uptime SLA report

forumulate status sla<br>client.status.depth.slaReport()

StatusDepth_slaReportCsv

GET /orgs/{orgId}/spaces/{spaceId}/status/sla-report.csv

The uptime SLA report as CSV

forumulate status sla csv<br>client.status.depth.slaReportCsv()

StatusDepth_listEvents

GET /orgs/{orgId}/spaces/{spaceId}/status/events

The page’s private events feed

forumulate status events list<br>client.status.depth.listEvents()

StatusDepth_acknowledge

POST /orgs/{orgId}/spaces/{spaceId}/status/events/{eventId}/acknowledge

Acknowledge a private event

client.status.depth.acknowledge()

StatusDepth_listKeys

GET /orgs/{orgId}/spaces/{spaceId}/status/event-keys

List the page’s events keys

forumulate status event-keys list<br>client.status.depth.listKeys()

StatusDepth_mintKey

POST /orgs/{orgId}/spaces/{spaceId}/status/event-keys

Mint an events key

forumulate status event-keys create<br>client.status.depth.mintKey()

StatusDepth_revokeKey

DELETE /orgs/{orgId}/spaces/{spaceId}/status/event-keys/{keyId}

Revoke an events key

forumulate status event-keys revoke<br>client.status.depth.revokeKey()

The Forumulate teamUpdated