Skip to content

Public status pages

A status page is a public address of the form /status/<slug> where anyone (a customer, a colleague, a subscriber) can see whether your services are up. The data comes from monitors and heartbeats you have already configured on the selected channel: nothing extra to set up or send.

The page is served as ready-made HTML — no authentication, no JS framework and no calls to the admin UI.

  • Overall banner — “All systems operational” / “Partial degradation” / “Major outage”, plus the average uptime over 90 days.
  • Service list — one row per monitor: name, type, current status, uptime over 30 and 90 days, average latency over 30 days and a bar of 90 daily cells.
  • Incident feed — the last 20 auto-incidents with duration and a “Resolved” mark.
  • Subscription form — a visitor leaves an email and gets a message on every incident and recovery.
visitor ──GET /status/<slug>────▶ server-rendered HTML (no authentication)
GET /status/<slug>/summary ──▶ the same snapshot as JSON (for auto-refresh)
channel monitors ──up↔down transition──▶ auto-incident ──▶ email to subscribers

A page is bound to one channel. It automatically shows every event source of that channel that keeps a check history:

SourceType on the page (kind)
Active monitor (ICMP / TCP / HTTP, …)the monitor’s own kind: icmp, tcp, http, …
HTTP monitorhttp
Content monitorcontent
Port monitorport
Workflow monitorworkflow
Browser workflowbrowser_workflow
Heartbeatheartbeat

Every service status is mapped to a single vocabulary:

StatusMeaning
upchecks are passing
degradedpartial degradation
downchecks are failing (for a heartbeat — a missed ping)
pendingno checks yet
pausedthe monitor is paused

The overall page status is aggregated across all services:

  • at least one downdown (banner “Major outage”);
  • otherwise a degradeddegraded;
  • otherwise, if there are services → ok;
  • no services at all → none.

Uptime is computed from the same daily check buckets as the monitor history: the bar has 90 daily cells (up, degraded, down, nodata), next to it are the percentages for 30 and 90 days and the average latency over 30 days. If a day has no data, the cell is grey and the percentage is shown as .

Incidents are created automatically, no manual bookkeeping required:

  • a monitor goes down → an incident is opened on every status page of the channel (status: "open") with the monitor title and the text of the last error;
  • the monitor recovers → the incident is closed (status: "resolved", with resolvedAt set).

The operation is idempotent: another failure while an incident is already open creates nothing, and a recovery without an open incident closes nothing. The feed shows the last 20 incidents.

The Status pages section → Create status page. Dialog fields:

FieldWhat it sets
Titlethe <title> and the page header
Slugthe address: /status/<slug>
Channelwhose monitors land on the page
Descriptionsubtitle under the header
Subscriber notification languageru or en — the language of subscriber emails and of the confirmation/unsubscribe pages
Page publishedoff = draft, the public address returns 404
Hide the “powered by Notifly” badgePro and Business plans only

The slug is normalised by the server: lower case, only a-z, 0-9 and hyphens; spaces and underscores become hyphens, repeats are collapsed. For example, My Status Pagemy-status-page. Slugs are globally unique: a taken one returns 409.

Terminal window
curl -X POST "$NOTIFLY_URL/status-page" \
-H "Content-Type: application/json" \
-H "X-Notifly-Key: <client-token>" \
-d '{
"appid": 12345,
"slug": "acme-status",
"title": "ACME service status",
"description": "Current state of the API and the website",
"enabled": true,
"lang": "en",
"brandingHidden": false
}'

The response is the created page object:

{
"id": 987,
"appid": 12345,
"slug": "acme-status",
"title": "ACME service status",
"description": "Current state of the API and the website",
"enabled": true,
"brandingHidden": false,
"lang": "en",
"created": "2026-06-23T10:00:00Z",
"updated": "2026-06-23T10:00:00Z"
}

The page is immediately available at $NOTIFLY_URL/status/acme-status (for example, https://api.notifly.ru/status/acme-status).

At the bottom of the page there is an email subscription form. It works as double opt-in:

  1. the visitor enters an email → POST /status/<slug>/subscribe;
  2. a confirmation email is sent (GET /status-subscribe/verify?token=…);
  3. after confirming, the subscriber receives an email when each incident is opened and when it is closed;
  4. every email contains an unsubscribe link (GET /status-subscribe/unsub?token=…).

Subscribing again with the same address does not create a duplicate: a confirmed subscriber gets nothing, an unconfirmed one gets the confirmation email again. The mailing is done by the same per-minute delivery worker as notification escalation, so the email arrives within a minute of the incident.

Subscriber emails and the confirmation/unsubscribe pages use the page language (the lang field). The public page UI itself is localised by the visitor’s Accept-Language header (ru/en).

  • The page snapshot is cached for 30 seconds — a traffic spike does not turn into a storm of database queries.
  • An open page polls /status/<slug>/summary every 45 seconds. If the overall status changed, the page reloads; otherwise only the “Updated” timestamp is refreshed.

GET /status/<slug>/summary returns the same snapshot as JSON — handy for a widget on your own site, a dashboard or a bot:

Terminal window
curl "$NOTIFLY_URL/status/acme-status/summary"
{
"title": "ACME service status",
"description": "Current state of the API and the website",
"overall": "ok",
"overallUptime90d": 99.94,
"services": [
{
"kind": "http",
"id": 555,
"name": "API",
"status": "up",
"uptime30d": 99.98,
"uptime90d": 99.94,
"avgLatencyMs": 148,
"days": [{"date": "2026-03-26", "state": "up", "uptimePct": 100}],
"lastCheckAt": "2026-06-23T09:59:12Z"
}
],
"incidents": [
{
"id": 4242,
"statusPageId": 987,
"appid": 12345,
"entityKind": "http_monitor",
"entityId": 555,
"title": "API",
"status": "resolved",
"startedAt": "2026-06-21T14:02:00Z",
"resolvedAt": "2026-06-21T14:19:00Z",
"lastEventAt": "2026-06-21T14:19:00Z",
"lastError": "HTTP 502 from https://api.example.com/health"
}
],
"brandingHidden": false,
"generatedAt": "2026-06-23T10:00:00Z"
}

Uptime percentages are 0..100; -1 means “no data”. The endpoint is public: no authentication is needed, while a disabled page (enabled: false) and an unknown slug both return 404.

CapabilityFreeProBusiness
Number of status pages15Unlimited
Hide the “powered by Notifly” badge
Custom domain (customDomain)

Exceeding the page limit returns 403 on creation; enabling a premium field on an unsuitable plan also returns 403 with an explanation in errorDescription. Current limit values are on the Quotas & pricing page.

Managing pages requires a client (or MCP) token and channel ownership; the public endpoints work without authentication.

MethodPathAuthenticationDescription
GET/status-pageclient-tokenList your status pages
POST/status-pageclient-token (write)Create a page. Body: appid, slug, title, description, enabled, lang, brandingHidden, customDomain
PUT/status-page/{id}client-token (write)Update a page (same payload)
DELETE/status-page/{id}client-token (write)Delete a page
GET/status/{slug}Public HTML page
GET/status/{slug}/summaryThe same snapshot as JSON
POST/status/{slug}/subscribeEmail subscription: { "email": "user@example.com" }
GET/status-subscribe/verify?token=…Confirm the subscription (link from the email)
GET/status-subscribe/unsub?token=…Unsubscribe (link from the email)

CRUD response codes: 400 — invalid slug/language or someone else’s channel, 403 — plan limit or a premium field not available on the plan, 404 — the page does not exist or is not yours, 409 — the slug is taken.