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” / “Scheduled maintenance in progress”, plus the average uptime over 90 days.
- Service list — one row per monitor or heartbeat: name, type, a colored status mark, uptime over 90 days, average latency over 30 days and a bar of 90 daily cells (a cell’s tooltip shows the date and percentage). A service in a maintenance window is marked “Scheduled maintenance”. The 30-day uptime is available in the JSON snapshot.
- Incident feed — the last 20 auto-incidents with start time and duration (“Incident” — still ongoing, “Resolved” — closed).
- Subscription form — a visitor leaves an email and gets a message on every incident and recovery.
- Footer — the update time and the “powered by Notifly” badge (a link to
notifly.ru with UTM tags
utm_source=statuspage&utm_medium=badge&utm_campaign=<slug>); the badge can be hidden on the Pro and Business plans.
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 |
maintenance | the service is in an active maintenance window — shown as “Scheduled maintenance” |
The overall page status is aggregated across all services:
- at least one
down→down(banner “Major outage”); - otherwise a
degraded→degraded; - otherwise a service in maintenance →
maintenance(banner “Scheduled maintenance in progress”); - 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, maintenance, nodata), next to it are the 90-day uptime and the average
latency over 30 days (the 30-day uptime is in the JSON). An up cell means all
checks of the day succeeded, down — none succeeded, degraded — some failed. If
a day has no data, the cell is grey and the percentage is shown as —. Failed checks inside a maintenance window are
excluded from uptime; a day whose failures all fell into a window is shown as
maintenance.
Incidents
Section titled “Incidents”Incidents are created automatically; you can’t create an incident or post a message on the page manually.
- a monitor goes down (a heartbeat goes to
alerting) → an incident is opened on every status page of the channel (status: "open") with the monitor name as its title; - the monitor recovers (the heartbeat gets a ping) → 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. The check error text is stored with the incident but never reaches the public page or subscriber emails.
A failure inside an active maintenance window opens no incident. If the source is still down after the window, the incident opens together with the deferred alert.
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 (your own channels only) |
| 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 page list shows the status (“Published” / “Draft”) and the reach — the number
of public page views over 30 days (only HTML page opens are counted, /summary
polling is not).
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: <api-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"}Required fields are appid, slug and title. 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. - Without JavaScript the page reloads itself every 2 minutes.
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", "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, "title": "API", "status": "resolved", "startedAt": "2026-06-21T14:02:00Z", "resolvedAt": "2026-06-21T14:19:00Z" } ], "brandingHidden": false, "generatedAt": "2026-06-23T10:00:00Z"}days holds 90 cells, oldest first. Uptime percentages are 0..100; -1 means
“no data” (for avgLatencyMs too). A service in a maintenance window has
status: "maintenance" and maintenance: true; overall takes the values ok,
degraded, down, maintenance, none. The endpoint is public:
no authentication is needed, while a disabled page (enabled: false) and an
unknown slug both return 404.
The snapshot is deliberately narrow: internal identifiers (channel, monitor, the page itself) and the check error text are never exposed publicly — they are visible only to the owner in the admin UI.
Plan limits
Section titled “Plan limits”| Capability | Free | Pro | Business |
|---|---|---|---|
| Number of status pages | 1 | 5 | Unlimited |
| Hide the “powered by Notifly” badge | — | ✅ | ✅ |
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 an API token (X-Notifly-Key) and
channel ownership; modifying requests need a token with write access. The public
endpoints work without authentication.
| Method | Path | Authentication | Description |
|---|---|---|---|
GET | /status-page | API token | List your status pages (with the views30d reach) |
POST | /status-page | API token (write) | Create a page. Body: appid, slug, title, description, enabled, lang, brandingHidden |
PUT | /status-page/{id} | API token (write) | Update a page (same payload) |
DELETE | /status-page/{id} | API 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 — a missing required field, invalid slug/language or
someone else’s channel, 403 — plan limit or a premium field not available on the
plan, 404 — on update, the page does not exist or is not yours, 409 — the slug
is taken. Deletion responds 204.
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.