Status monitors
A status space watches things and shows readers whether they are up. This page covers the monitors themselves: what each kind checks, where it is checked from, the fields, and in particular expectStatus — which exists because the default rule calls a healthy endpoint down. How the page those monitors feed looks and behaves (its looks, sub-pages, incidents, subscribers and embeds) is in Status pages.
Kinds of monitor
A monitor is a component on the page. Its kind says where its state comes from, and it is fixed at creation.
| How it decides | Interval |
|---|---|---|
| We poll your URL. | 60 seconds by default, clamped to 30–3600. |
| Your job pings us. | 3600 seconds by default, clamped to 60–604800. |
| A protocol probe of | As |
| A real browser loads | As |
| Nothing probes it. Its state is | — |
| Rolls up another status page of your organization ( | — |
| Mirrors a vendor's public status. See Vendor components. | Read on a schedule. |
A heartbeat is up while its last ping is no older than one and a half times its interval, unknown until the first ping arrives, and down after that. Ping it at GET or POST /status/ping/{monitorId}/{token}, which always answers {"ok": boolean}.
Reporting a run that did not go cleanly
A ping with no body is a clean run — exactly what every ping was before this existed. A job that ran but had part of it fail can say so, instead of going silent (which reads as an outage once 1.5× the interval has passed):
curl -X POST "$PING_URL" -H 'content-type: application/json' \
-d '{"ok": false, "detail": "settle_crypto_invoices failed"}'
# or, from a cron line with no body:
curl "$PING_URL?status=degraded&detail=queue%20lagging"Field | Effect |
|---|---|
|
|
|
|
| Up to 500 characters. Shown to your organization in the console beside the monitor, never on the public page. Dropped on a clean run. |
degradedandpartial_outagestill count as the job being up for the uptime percentage — it ran. They colour the component (and the headline turnsdegraded), and never make it look better than its history does.The next clean ping clears the report. Silence past the window is still an outage, whatever the last report said.
The ping never refuses a body it cannot read: an unparseable body or an unknown
statusis treated as a clean run (or falls back took), because a job whose ping starts failing gets its alerting switched off.Like a missed heartbeat, a reported state does not open an incident on its own.
Reporting a restart
A container that restarts in a few seconds is invisible to a 60-second poll, so the service has to say so itself. Call POST /status/restart/{monitorId}/{token} from your boot or deploy script. It needs no session, because a process that has just started has none; the token is the same secret as the monitor's heartbeat ping URL, so take the monitor's pingUrl and replace /status/ping/ with /status/restart/.
curl -X POST "$RESTART_URL" -H 'content-type: application/json' \
-d '{"reason": "deploy"}'reasonis optional, up to 120 characters, and defaults torestarted.The restart is recorded as a failed check at the database's clock, labelled
restart: <reason>, so it shows as a dip in the uptime history beside the checks.It does not raise an alert or open an incident: a deploy is not an outage.
A restart within 30 seconds of a failed check already recorded on that monitor is folded into it, so a crash loop does not turn the whole bar into dips.
It always answers 200, so a deploy script cannot fail on it:
{"ok": true}for a known monitor and a valid token (a folded restart included), and{"ok": false}for a bad token or an unknown monitor, the same answer for both so that neither can be probed for.
kind cannot be changed after creation. PATCH accepts the field and ignores it. To change a kind, delete the monitor and create it again.
Protocol probes
Not everything worth watching answers HTTP: a database port, a mail server, a DNS record, a certificate about to lapse, a domain about to expire. Each of these kinds takes one object, probe, checked against the kind on create and on PATCH; a missing piece is a 422 that names it.
POST /orgs/{orgId}/spaces/{spaceId}/status/monitors
{
"name": "Primary database",
"kind": "tcp",
"probe": { "host": "db.yourcompany.com", "port": 5432 }
}
| Used by | Default | What it does |
|---|---|---|---|
| every kind but | — | The host name or address. For |
|
|
| The port connected to. |
|
|
|
|
|
|
| The resolver asked. It must be a public address. |
|
| none |
|
|
| none | What is sent, up to 500 characters. |
|
| none | Down when the certificate ( |
|
| — | The page the browser loads. |
|
| none | A CSS selector that must appear on the loaded page. |
Every host must resolve to a public address, exactly as an
httpURL must; a private or loopback target is refused.The monitor's
urlis filled in for you as the probe's address (tcp://db.yourcompany.com:5432,dns://1.1.1.1/yourcompany.com?MX,domain:yourcompany.com), which is what the console shows.A
PATCHwithprobereplaces the stored probe whole, judged against the stored kind.keyword,expectStatusand the other HTTP rules are stored and ignored on a probe, as they are on a heartbeat.
Browser checks: what you need to set up
browser monitors run on a browser runner that Forumulate operates. Until it is configured on Forumulate's side, creating a browser monitor answers 422 with "Browser checks need a browser runner, and this platform has none configured yet", and the console does not offer the kind. Nothing on your side needs setting. GET …/status/regions answers browserChecks: true once the runner is there.
Creating one
POST /orgs/{orgId}/spaces/{spaceId}/status/monitors
{
"name": "API",
"url": "https://api.yourcompany.com/health",
"intervalSeconds": 60
}Field | Type | Default | Notes |
|---|---|---|---|
| string, 1–120 | required | Falls back to the URL's hostname if you send only whitespace. |
| string, ≤2048 |
| For |
|
|
| Changeable with |
|
|
| Immutable after creation. |
| integer, 30–604800 | 60 ( | Clamped per kind, see above — on |
| boolean |
|
|
| string, ≤60, nullable |
| Groups monitors under a heading. |
| string, ≤120, nullable |
| The body must contain this text. |
| integer, 1–365, nullable |
| Fail when the TLS certificate expires within this many days. |
| integer, 100–599, nullable |
| See below. |
| boolean |
| On create and |
| integer, 1–10 |
| On create and |
| integer, 1–10 |
| On create and |
| enum, nullable |
| On create and |
| boolean |
| Turns |
| integer, 1–120000, nullable |
| A response slower than this counts as down. |
| object, nullable |
| Sent with every check. Values are write-only and stored encrypted; reads return the names only. |
| string, ≤4096, nullable |
| Sent with a |
| string, ≤200, nullable |
| A dotted JSON path in the response, and the value it must equal. |
| object | — | A protocol or browser monitor's target. See Protocol probes. |
| string[], ≤10 |
| The probe regions it is checked from. Empty: the primary checker only. See Probing from several regions. |
| integer, 1–10 |
| How many regions must see it failing before it reads down. |
| boolean |
| Stops probing without deleting it. See below. |
|
|
| This component's own day-strip length on the page. |
| boolean |
| Show its uptime figure beside the strip. |
|
|
| Its day bars, only its state, or nothing at all until something is wrong with it (a dependency nobody needs to see while it works). |
| string, ≤500, nullable |
| One sentence about what it is, shown to readers beside its name. |
| string, 1–80 | from the name | Its stable address on the page ( |
| URL, nullable |
| Its name links here (the component's own docs or product). |
| string, ≤60, nullable |
| Where it runs ("EU", "US-East"). The page can group by it ( |
|
|
| The day it started existing. Earlier days draw as "not yet" rather than as no data. |
| object | everyone |
|
| uuid | — | Kind |
| string, ≤200, nullable |
| Vendor kinds: the vendor component's id or exact name. |
Pausing a monitor
paused: true is for a component you are working on, not removing. The row and its history stay on the page, no new checks are taken, no alert is sent, and the component reads unknown (or its declaredState) until you set it back to false. alertsEnabled: false is the other thing: it only mutes the alerts while the checks carry on.
The order on the page
Components are drawn in the order you give them:
PUT /orgs/{orgId}/spaces/{spaceId}/status/monitors/order
{ "ids": ["<api-id>", "<web-id>"] }Components you leave out keep their order after the ones you list, so moving two components can send just those two. An id from another page is a 422.
Update with PATCH /orgs/{orgId}/spaces/{spaceId}/status/monitors/{monitorId}. Send only the fields you are changing; sending a field with the value it already has is a 200 that changes nothing. There is no PUT on a monitor. (PUT …/status/settings is a different thing — the space's status settings.)
expectStatus
What "up" means without it
A monitor is up when both of these hold:
the request connected
and the status code is under 400Both, and in that order. A request that never connected reports a status of 0, which satisfies "under 400" — so the status condition on its own calls a server that cannot be resolved healthy, which is the one thing a status page exists to notice.
What "up" means with it
the request connected
and the status code is exactly the number you setThe under-400 rule is gone. A monitor with expectStatus: 401 is down on a 200.
Why you would want that
An unauthenticated probe of an API that requires a token answers 401 when it is perfectly healthy. Some answer 405 to a GET they only accept as POST. The under-400 rule called both permanently down, which made the monitor useless and the board a liar.
PATCH /orgs/{orgId}/spaces/{spaceId}/status/monitors/{monitorId}
{ "expectStatus": 401 }Clearing it
PATCH …/status/monitors/{monitorId}
{ "expectStatus": null }null is a real value here, and it is the one that restores the default under-400 rule.
Omitting the field does not clear it. An absent key means "leave this alone", so a PATCH that does not mention expectStatus keeps whatever is stored. The two are different on purpose, and they are the thing to get right when a client builds the patch body from a form.
An empty string is a validation error, not a clear. Send null.
What it accepts
A single integer between 100 and 599, or null. Nothing else:
You send | Result |
|---|---|
| Accepted. |
| Accepted. Clears it. |
| 422. It is a number, not a string. |
| 422. There is no range syntax. |
| 422. |
| 422. One code, not a list. |
| 422. Out of range is refused, not clamped. |
| 422. |
| 422. |
Out of range is refused rather than clamped, because clamping 50 up to 100 would silently decide that only a 100 Continue is healthy.
It is http-only
expectStatus on a heartbeat monitor is accepted, stored, and silently ignored — not refused. A heartbeat has no response to read a status from; its verdict is purely the age of the last ping. keyword and certExpiryDays behave the same way for the same reason.
If you set it on a heartbeat and the board does not change, that is why.
Combining conditions
keyword and certExpiryDays are additional conditions, ANDed on top. So a monitor with expectStatus: 200 and keyword: "ok" is up only when it connected, returned exactly 200, and the body contained ok.
Probing from several regions
A failure seen from one place is often that place: a routing problem between one data centre and your host, not your host. So a component can be checked from several regions, and only read as down once enough of them agree.
GET /orgs/{orgId}/spaces/{spaceId}/status/regions
# { "regions": [ { "key": "primary", "label": "Primary", "live": true, "primary": true },
# { "key": "eu", "label": "Europe", "live": true, "primary": false } ],
# "browserChecks": false }
PATCH /orgs/{orgId}/spaces/{spaceId}/status/monitors/{monitorId}
{ "regions": ["primary", "eu"], "confirmRegions": 2 }regionslists the region keys it is probed from. Empty (the default) is the primary checker alone. A list that leaves out the primary's key is checked only from the regions it names.An unknown key is a
422that names it.The rule: the component is down when at least
confirmRegionsof the regions that have reported each see it failingfailureThresholdtimes in a row.confirmRegionsis capped at the number of regions that have reported, so a component probed from one live region is judged by it alone.Alerts are sent once, not once per region.
Every check is stored with the region it ran from.
What readers see: on a page with two or more regions, each component row carries a small dot per region, and the component's own page has a "Checked from" table with each region's latest result (the regions part of theme.status.subpages.component). On a page with one region, neither is drawn.
In the studio: Checks and alerts → Checks, open a check, then Probe from and Regions that must agree it is down.
What you need to set up
Probe regions are run by Forumulate, not by you: each region is a checker host that Forumulate operates and declares on its side (STATUS_PROBE_REGIONS). Until at least one region besides the primary is declared and answering:
GET …/status/regionslists only the primary (or lists a declared region withlive: falsewhile it is not answering yet),the console does not show the region pickers at all,
readers see no region dots and no "Checked from" table,
and every component is checked from the primary checker alone, exactly as before regions existed.
There is nothing to configure on your side beyond picking regions once they are offered.
Vendor components
A page can show the services you depend on next to your own. A vendor component reads the vendor's public status and puts it in your page's words:
|
| What it reads |
|---|---|---|
| The vendor's Statuspage page ( | Its |
| The vendor's Instatus page | Its public status: one component or the whole page. |
| The vendor's Better Stack page | Its public status: one component or the whole page. |
| An RSS or Atom feed (AWS, Azure, Slack) or Google Cloud's | Google Cloud's feed says exactly what is ongoing. An RSS or Atom feed only lists posts, so the newest post, if it is from the last six hours and does not say it is resolved, reads as degraded. |
A vendor's maintenance windows (from the kinds that publish them) can be shown on your page with the partners band. See Status pages.
Opening incidents from outside
Your alerting tool already knows when something breaks. An inbound address lets it open (and resolve) an incident on the page:
POST /orgs/{orgId}/spaces/{spaceId}/status/inbound
{ "source": "grafana", "monitorId": "<component id>", "label": "Grafana prod" }The answer carries the url to paste into the tool as a webhook (or, for email, the address to mail).
| What it understands |
|---|---|
| A mail to the address opens an incident. |
| Each tool's own webhook payload: firing opens, resolved resolves. |
| Any JSON, read through your |
generic takes a mapping of dotted paths into the JSON your tool sends:
| Default | What it is |
|---|---|---|
|
| What ties a later "resolved" to the alert it ends. |
|
| The incident's title. |
|
| Its first update. |
|
| The field that says whether it is resolved… |
|
| …and the value that means it (any case). |
|
| The severity. |
monitorId names the component the incident affects; unset, it names none.
Metrics
A page can chart numbers beside the components: response time from our own checks, and custom metrics you create (PUT …/status/metrics/{key}). A custom metric's points come from one of two places.
You push them, one at a time or up to 500 at once:
POST /orgs/{orgId}/spaces/{spaceId}/status/metrics/api.p95/points
{ "value": 212 }A point more than 5 minutes ahead of the server's clock or more than 30 days behind it is refused and counted in rejected.
We pull them from a provider you already use, every everySeconds (default 300, 60–86400):
PUT /orgs/{orgId}/spaces/{spaceId}/status/metrics/api.p95/source
{
"source": { "provider": "datadog", "query": "avg:trace.http.request.duration{service:api}", "site": "datadoghq.eu" },
"secret": { "apiKey": "…", "appKey": "…" }
}
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
| any request headers |
The secret is stored encrypted and never returned: the answer says only which names are set. "source": null stops pulling and keeps the points. In the studio: Checks and alerts → Metrics and embeds, then Pull from.
What you need to set up
A pulled metric needs your own account with the provider and the keys in the table above, entered in the studio or with the call above. Until they are, the metric is charted from whatever you push, and an empty metric draws an empty chart.
What a metric says about itself (PUT …/status/metrics/{key}): name, unit, description, decimals (0–4), aggregate (avg, sum, max, min, last), visible, sortKey, component (the component whose row chart draws it when it has no measured response time), and the chart's axisMin, axisMax and a dashed threshold line.
What a reader sees
GET /public/orgs/{orgId}/spaces/{spaceId}/statusField | Notes |
|---|---|
|
|
|
|
| Open and resolved incidents. |
uptime is null, not 0, when nothing has been recorded yet. A percentage is published only once there is enough to stand on: at least 20 checks spanning at least 6 hours (window.minSamples / window.minSpanHours; both halves — 25 checks taken in the first hour still read "Not enough data yet"). Until then window.sufficient is false and the page says so, with the bar. history is 90 day-buckets, oldest first, each true, false or null; a day is true only when it had zero failures.
A reader never sees expectStatus. It is on the authenticated monitor routes and on GET …/status/board, and nowhere public. Only visible: true monitors appear at all.
Other public endpoints:
Path | Notes |
|---|---|
| Statuspage-compatible. Cached 60s. |
| An uptime badge. Cached 300s. |
| The incident feed. |
| Double-opt-in email subscription. Rate limited to 5/minute. |
A status space that is not PUBLIC answers all of these with a flat 404 unless the request carries a valid rotating share token.
SMS alerts for readers
What you need to set up
Readers can subscribe to a status page by email, Slack, Discord, Microsoft Teams, Google Chat, Telegram or webhook (see Status pages). SMS is offered as well once your organization connects its own Twilio account. There is no shared sending number: texts are sent from yours and billed by Twilio to you. Until the account is connected, the page offers only the other channels. All three routes need org.manage_integrations.
PUT /orgs/{orgId}/sms
{
"provider": "twilio",
"accountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"authToken": "your-twilio-auth-token",
"fromNumber": "+14155550123"
}accountSidmust start withACand be 34 characters.authTokenis 16 to 128 characters, write-only and stored encrypted. Omit it on an update to keep the stored one.fromNumberis E.164, with the country code.Unknown keys are refused.
GET /orgs/{orgId}/sms answers { configured, provider, accountSid, fromNumber, updatedAt }, never the token. DELETE /orgs/{orgId}/sms disconnects it and the page stops offering SMS.
A reader's number is verified before anything is sent: the page texts a six-digit code and delivers nothing until it is typed back, so nobody can enrol a number that is not theirs. A number that replies STOP is removed from the subscription instead of being retried.
Monitoring Forumulate itself
The Forumulate API has three unauthenticated health endpoints. Point an http monitor (yours or any other tool's) at the one that answers your question.
GET /health/liveanswers{"status": "ok"}as soon as the process accepts connections and never touches the database or Redis. It tells you the process is up, not that it can serve you.GET /health/readychecks that Postgres and Redis both answer and that the process is not saturated (event-loop lag under one second at p99). It answers{"status": "ok", "db": true, "queue": true}with 200, or"degraded"with 503 when any of those fails. This is the one with a status-code contract.GET /health checks only the database and always answers 200, with
{"status": "ok" | "degraded", "db": boolean}. Read thestatusfield; the code stays 200 so a brief blip is not reported as an outage.
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 clients are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP call_api tool. Where a CLI command or MCP tool exists too, the last column names it. Operation names link to the API reference.
Org Sms
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| The organization’s SMS account (never the token) |
| |
| Set the organization’s Twilio account |
| |
| Remove the organization’s SMS account |
|
Public Status
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Subscribe the status |
| |
| The published status board |
| |
| The uptime report as CSV |
| |
| A status badge (SVG) |
| |
| Statuspage-compatible status summary |
| |
| RSS feed of the status |
| |
| Atom feed of the status |
| |
| JSON feed of the status |
| |
| File of maintenance |
|
Public Status Access
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Whether a status page is gated |
| |
| May this reader read a status page? |
|
Public Status Alerts
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Which alert channels a reader can subscribe with |
| |
| Subscribe a Slack hook, webhook or phone to this status page |
| |
| Confirm a reader alert with the code it was sent |
| |
| Stop a reader alert, with the token every delivery carries |
|
Public Status Embeds
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| One component's day, in hourly buckets |
| |
| A status page's response times |
| |
| A status page's custom metric series |
| |
| What is happening on a status page right now |
| |
| Get the widget JS in a space (status) |
|
Public Status Incidents
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| A status page's incident archive |
| |
| One incident on a status page |
| |
| Get the incident |
|
Public Statuspage
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Statuspage-compatible summary (API v2) |
| |
| Statuspage-compatible overall status (API v2) |
| |
| Statuspage-compatible components and groups (API v2) |
| |
| Statuspage-compatible incidents (API v2) |
| |
| Statuspage-compatible unresolved incidents (API v2) |
| |
| Statuspage-compatible scheduled maintenances (API v2) |
| |
| Statuspage-compatible upcoming maintenance (API v2) |
| |
| Statuspage-compatible maintenance under way (API v2) |
|
Public Status Search
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Search a status page's incidents |
|
Public Status Siblings
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| A status page's sibling pages |
|
Status
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Update the incident |
| |
| List channels in a space (status) |
| |
| Create a channel in a space (status) |
| |
| Delete the channel (status) |
| |
| List monitors in a space |
| |
| Add a status monitor |
| |
| Update the monitor |
| |
| Delete the monitor |
| |
| List settings in a space (status) |
| |
| Replace settings in a space |
| |
| List groups in a space |
| |
| Replace groups in a space |
| |
| Get the board in a space |
| |
| Create an incident in a space |
| |
| Create an update in a space |
|
Status Authoring
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| The probe regions |
| |
| Reorder the components |
| |
| Correct a posted incident update |
| |
| Delete a posted incident update |
| |
| Delete an incident |
|
Status Heartbeat
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Record a heartbeat for a monitor |
| |
| Record a heartbeat for a monitor, by POST |
|
Status Integrations
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List readers’ Slack, webhook and SMS subscriptions |
| |
| Remove a reader’s alert subscription |
| |
| List the addresses that open incidents from outside |
| |
| Create an inbound address (email, Datadog or PagerDuty) |
| |
| Revoke an inbound address |
|
Status Metrics
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| A status page's custom metrics, hidden ones included |
| |
| Create a custom metric, or change what it says about itself |
| |
| Delete a custom metric and every point of it |
| |
| Push points to a custom metric |
|
Status Slack
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Slack slash command for a status page |
|
Status Subscribers
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| A status page's email subscribers |
| |
| Add an email subscriber |
| |
| A status page's email subscribers as CSV |
| |
| Import email subscribers from CSV |
| |
| Send a pending subscriber a fresh confirmation link |
| |
| Remove an email subscriber |
|
Status Templates
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List the status page’s incident templates |
| |
| Save an incident template |
| |
| Update the template |
| |
| Delete the template |
| |
| Open an incident from a template |
|