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.
What the page shows
Section titled “What the page shows”- 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 subscribersWhat lands on the page
Section titled “What lands on the page”A page is bound to one channel. It automatically shows every event source of that channel that keeps a check history:
| Source | Type on the page (kind) |
|---|---|
| Active monitor (ICMP / TCP / HTTP, …) | the monitor’s own kind: icmp, tcp, http, … |
| HTTP monitor | http |
| Content monitor | content |
| Port monitor | port |
| Workflow monitor | workflow |
| Browser workflow | browser_workflow |
| Heartbeat | heartbeat |
Statuses and uptime
Section titled “Statuses and uptime”Every service status is mapped to a single vocabulary:
| Status | Meaning |
|---|---|
up | checks are passing |
degraded | partial degradation |
down | checks are failing (for a heartbeat — a missed ping) |
pending | no checks yet |
paused | the monitor is paused |
The overall page status is aggregated across all services:
- at least one
down→down(banner “Major outage”); - otherwise a
degraded→degraded; - 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
Section titled “Incidents”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", withresolvedAtset).
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.
Creating a page
Section titled “Creating a page”From the admin UI
Section titled “From the admin UI”The Status pages section → Create status page. Dialog fields:
| Field | What it sets |
|---|---|
| Title | the <title> and the page header |
| Slug | the address: /status/<slug> |
| Channel | whose monitors land on the page |
| Description | subtitle under the header |
| Subscriber notification language | ru or en — the language of subscriber emails and of the confirmation/unsubscribe pages |
| Page published | off = draft, the public address returns 404 |
| Hide the “powered by Notifly” badge | Pro 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 Page → my-status-page. Slugs are globally unique: a taken one
returns 409.
Via the REST API
Section titled “Via the REST API”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).
Subscribing to updates
Section titled “Subscribing to updates”At the bottom of the page there is an email subscription form. It works as double opt-in:
- the visitor enters an email →
POST /status/<slug>/subscribe; - a confirmation email is sent (
GET /status-subscribe/verify?token=…); - after confirming, the subscriber receives an email when each incident is opened and when it is closed;
- 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).
Cache and auto-refresh
Section titled “Cache and auto-refresh”- 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>/summaryevery 45 seconds. If the overall status changed, the page reloads; otherwise only the “Updated” timestamp is refreshed.
JSON for external integrations
Section titled “JSON for external integrations”GET /status/<slug>/summary returns the same snapshot as JSON — handy for a
widget on your own site, a dashboard or a bot:
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.
Plan limits
Section titled “Plan limits”| Capability | Free | Pro | Business |
|---|---|---|---|
| Number of status pages | 1 | 5 | Unlimited |
| 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.
REST API
Section titled “REST API”Managing pages requires a client (or MCP) token and channel ownership; the public endpoints work without authentication.
| Method | Path | Authentication | Description |
|---|---|---|---|
GET | /status-page | client-token | List your status pages |
POST | /status-page | client-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}/summary | — | The same snapshot as JSON |
POST | /status/{slug}/subscribe | — | Email 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.
See also
Section titled “See also”- Active monitors — the main data source for the page.
- Heartbeat — “silent” jobs also appear on the page.
- Delivery & escalation — how the same outage reaches your team rather than the public.
- Quotas & pricing — limits on the number of pages.