# Status pages

Source: https://www.pingtower.com/docs/service/status-pages/

A page that tells your customers what is down, fed by the alerts that page you. An outage reaches it once someone acknowledges it, backdated to when it began; a blip that clears first never does.

A status page belongs to the team, not to a project. It lists components, and each component is bound to checks and rules from any of the team's projects. A component's state comes from three places: the alerts of what it is bound to, the [incidents](/docs/service/incidents/) published to it, and the [maintenance windows](/docs/service/maintenance/) shown on it. The page is server-rendered HTML with inline CSS and SVG. It has no JavaScript, and a public page sets no cookies. Light and dark follow the reader's system setting.

In the [web app](https://app.pingtower.com) it is **Status pages**, where each page has a **Components** tab and a **Settings** tab. The iOS app shows the pages, their components' states and their uptime under Account → Status pages, and leaves editing to the web app. Every member can read a page's setup. Owners and admins, or any role with `status_pages:write`, change it. A member [limited to some projects](/docs/getting-started/concepts/#per-project-roles) gets `403` `{"error": "not available to project-limited members"}` on creating, changing or deleting a page, its logo, custom domain, components or phases, whatever their role.

## A page

| Setting | |
| --- | --- |
| Title | Required, up to 120 bytes. |
| Address | The page's slug. See [Address](#address). |
| Description | Optional, up to 1000 bytes. Shown under the title as plain text. |
| Who can see it | **Anyone** (public, the default) or **Team members only** (private). See [Members only](#members-only). |
| Time zone | An IANA name such as `Europe/Oslo`, default `UTC`. The daily bars and every time on the page use it. |
| Accent colour | Optional lower-case hex such as `#0b7285`. The default is `#7c3aed`, a violet. It colours the dot on the **Get help** button, links when they are pointed at, the focus ring on a group, and the tile behind the title's first letter on a page without a logo. The state colours never change. |
| Support link | Optional `https` URL, shown as a **Get help** button in the page's header. |
| Logo | Optional PNG, JPEG or WebP, at most 128 KiB, shown up to 32 px high in the page's header, beside the title. Without one, the header shows the title's first letter on a tile in the accent colour. The type is read from the file's bytes, and SVG is refused. |
| Acknowledgement gate | On by default, with a 15-minute fallback. See [When an outage reaches the page](#when-an-outage-reaches-the-page). |

A team holds at most 20 pages.

## Address

The slug is 3 to 32 lower-case letters, digits and single hyphens. It starts and ends with a letter or a digit. It is unique across every team. Names that a domain is expected to answer on are reserved: `account`, `admin`, `api`, `app`, `auth`, `autodiscover`, `billing`, `blog`, `cdn`, `dashboard`, `docs`, `help`, `id`, `login`, `mail`, `mta-sts`, `ns1`, `ns2`, `oauth`, `pay`, `portal`, `security`, `signup`, `status`, `support`, `verify` and `www`. So is any slug containing `pingtower` or `ptwr`, so that no team's page can pass for Pingtower's own. Changing the slug frees the old address at once, with no redirect.

On pingtower.com a page lives at `https://{slug}.ptwr.net`. `ptwr.net` is a separate domain, so a page your team writes is never on the same site as pingtower.com. A host serves its one page and nothing else:

| Path | |
| --- | --- |
| `/` | Current status. |
| `/history` | Incidents and completed maintenance from the last 90 days. |
| `/incidents/{id}` | One incident's permalink. |
| `/badge.svg`, `/uptime.svg`, `/embed.json`, `/embed.js` | The [embeds](#embeds). |

On a self-hosted box a page lives at `/status/{slug}` on the box's own address, with the same paths below it. To give each page its own host, set `--status-host` (or `PINGTOWER_STATUS_HOST`). It is a host template with exactly one `{slug}` in its first label, such as `{slug}.status.example.com` or `status-{slug}.example.com`, and it must not match the portal's host. With it set, a matching host serves only its page, and everything else on it answers 404, `/v1` included. `/status/{slug}` on the box's other hosts then redirects there with a 308. DNS and certificates for those names are yours to provide; [Behind a proxy](/docs/self-hosting/proxy/) covers the reverse-proxy side.

## Members only

A private page is for the signed-in members of the page's team, whatever their role. It has no page password, no SSO, no email codes and no IP allowlist.

On `ptwr.net`, a visitor without a pass is sent to app.pingtower.com, where their session is. A member comes back with a single-use pass that is good for 60 seconds. The page trades it for a `pt_status_pass` cookie on that page's host, which is `HttpOnly`, `Secure` and `SameSite=Lax` and lasts 12 hours. Someone who is signed out is sent to sign in first. Every view checks the account again. A person removed from the team, suspended, signed out, or whose session was revoked is locked out on their next request, not when the cookie expires. Anyone who is not a member gets the same 404 as a slug that does not exist.

A private page answers with `Cache-Control: private, no-store` and `X-Robots-Tag: noindex`. It has no embeds and cannot take a custom domain.

On a self-hosted box, a private page at `/status/{slug}` is read on the portal's own host, where the browser already sends the portal session. A signed-out visitor gets a sign-in page (`401`). With `--status-host` set, the box uses the same pass handoff as pingtower.com. A box started with `--portal=false` has nobody who can sign in, so it refuses `private` with a `422` that says why.

## Components

| Field | |
| --- | --- |
| Name | Required, up to 120 bytes. |
| Description | Optional, up to 500 bytes. Shown under the name. |
| Group | Optional, up to 80 bytes. Groups appear in the order of their first component, and components keep their order within a group. |
| Bindings | Up to 20. Each names a project, exactly one check or rule in it, and an impact. |
| Response times | **Show response times on the page** (`show_latency`), off by default. Shows a chart of the response times of the checks the component binds. See [Response times](#response-times). |

A page holds at most 100 components.

A binding's **impact** is what the component shows while one of its alerts is on the page: `degraded`, `partial` or `major`. When two bindings point at the same rule, the worse impact wins. A component cannot bind the same target twice.

- **A check** is bound through its alerting rule, so the page and the pager share one definition of down. HTTP, TCP, ping, DNS, SSH and TLS certificate checks can be bound. The check must alert (`alert_if` set). To use another kind of check, bind the rule that watches it. A TLS certificate check is always bound as `degraded`: an expiring certificate is a notice, never downtime.
- **A rule** can be any rule in any of the team's projects.

A binding whose check or rule is later deleted, or whose check stops alerting, stays on the component. It is marked missing and moves nothing until the target is back.

A member [limited to some projects](/docs/getting-started/concepts/#per-project-roles) sees every component, but not a binding to a project outside their grants. They also don't see the timeline phases or component uptime that come from that binding. The component's current state is shown as it is.

## When an outage reaches the page

The page is brought up to date every 30 seconds and before it renders. For each binding it reads the rule's alerts:

1. **An alert that is firing or acknowledged** reaches the page once it passes the acknowledgement gate. It opens a phase on the component at the binding's impact.
2. **The phase is backdated** to when the alert opened, so the bars count the outage from when it began, not from when someone noticed.
3. **The phase ends** when the alert resolves, at the time it resolved. If the alert has already been pruned, or the binding was removed, it ends at that evaluation.

The gate is what keeps blips off the page:

- **With the gate on (the default)**, an alert passes once someone acknowledges it, or once it has fired for the fallback time without anyone doing so. The fallback is 15 minutes by default, can be set from 1 to 1440 minutes, or `0` for never. The web app offers 5, 15, 30 or 60 minutes, or never.
- **An alert that resolves before it passes never shows** and never counts against uptime. A blip that clears in a minute stays between you and your pager.
- **With the gate off**, every alert reaches the page as it opens.

A few alerts never reach the page on their own. A **muted rule's** alerts stay off it. If the alert is still firing when the mute ends, it reaches the page then, backdated to when it opened. An alert a [maintenance window](/docs/service/maintenance/) held counts from the window's end. While an admin has put a component in maintenance by hand, it takes no new phases from its alerts.

A component's state is its worst open phase that is not hidden, in this order: major outage, partial outage, degraded performance, under maintenance, operational. The page's banner is the worst state among its components.

A rendered page is reused for 30 seconds. A change made in the web app, such as an edit, a published incident or a corrected phase, shows on the next view. A change that comes from an alert can take up to those 30 seconds.

## Correcting the timeline

Each component keeps its timeline: the phases its state is made of, with where each came from (an alert, an incident, a maintenance window, or an admin). In the web app, open a component on the **Components** tab. An owner or admin can:

- **set a state now**, which opens a phase that stays until someone ends it;
- **backfill** a closed phase in the past, for an outage Pingtower did not see;
- **edit** any phase's state and times, including one an alert wrote;
- **hide** any phase.

Hiding is how a false positive is erased. The evaluator still finds the hidden phase for that alert, so it does not write it again, and a hidden phase counts for nothing. Removing a phase someone set by hand deletes it. Removing one an alert or an incident wrote hides it. A phase starts no earlier than 400 days ago and neither starts nor ends in the future. Closed phases are kept for 400 days, which is what [uptime reports](/docs/service/uptime-reports/) read a component's uptime from.

## Uptime

Uptime is time spent in each state, per component, per day, in the page's time zone:

- **Downtime** is the time in major outage plus 0.3 of the time in partial outage. Degraded performance is not downtime.
- **Where phases overlap**, the worst state wins for the overlap, so an hour that is both partial and major counts once, as major.
- **Maintenance** time is left out of the measure, neither up nor down. An outage during maintenance is worse than maintenance, so it still counts. Time covered by a maintenance window that is not shown on the page is left out as well.
- **A day's uptime** is 1 − downtime ÷ observed time. Observed time starts when the component was created and stops at now, so a day before the component existed has no data.

The page's figure is the average of every component's days that have data, over 90 days, to two decimals.

| Bar | The day |
| --- | --- |
| Green | No outage, no degradation. |
| Yellow | Degraded only. |
| Orange | Under 30 minutes of downtime. |
| Red | 30 minutes of downtime or more. |
| Blue | Maintenance only. |
| Grey | No data. |

Each bar's tooltip gives the date, the day's uptime, minutes down and the worst state it touched. It is an SVG title, so it needs no script.

## What the public sees

Every page has a header that stays at the top while the reader scrolls. It holds the logo or the title's first letter, the title, the description (hidden on narrow screens), a link to the history or back to the current status, and the **Get help** button when the page has a support link. Below it, the front page shows, from top to bottom:

- the overall state, such as **All systems operational** or **Partial outage**, in its colour. Below it are the components that are not operational, each with its state and since when it has been in that state. When every component is operational, a line says so instead. Beside it is [Checks by region](#checks-by-region);
- current incidents, each with [its progress](#an-incidents-progress) and its latest public update;
- maintenance in progress, and maintenance scheduled in the next 7 days;
- the components by group, each with its state and 90 daily bars, and the page's 90-day uptime, with a legend for the bars' colours;
- incidents resolved in the last 14 days, each with its worst impact, when it ran, the components it named and its latest public update, and a link to the full history.

The history page lists incidents from the last 90 days and the maintenance completed in that time. Each maintenance list shows at most 20 entries. An incident's permalink shows its title, when it started and how long it has run or ran, the affected components with their impact, its progress, and up to its 20 newest public updates, each with its stage if it has one. The footer gives the time of the last update and "Powered by pingtower".

The page shows no tenant, project or account ids. It is served with a sandboxing content security policy, so nothing on it can run script, and everything on it works without script. Every route of a page, the embeds included, is limited to 10 requests a second per client address, with a burst of 40.

A self-hosted box shows this design from 1.1.0; a box on an earlier release shows the earlier design.

### Checks by region

Beside the overall state, **Checks by region** shows how each region sees the checks the page's components bind. Only check bindings count; a bound rule adds nothing here. Only sources named like a region appear: two letters for the continent, a hyphen and three letters. On pingtower.com those are Pingtower's probe fleet. An [agent](/docs/addons/) your team named anything else never appears, so a host name used as a source name stays private. A region of Pingtower's fleet shows its city, and any other its name alone.

A region counts a check when its last run judged the target, up, degraded or down, within three of the check's intervals. Each region says what it sees now, such as **All 4 passing**, **1 of 4 failing** or **Degraded**, and draws its runs over the last 2 hours in 5-minute cells. Each cell's tooltip gives its time, how many checks ran, and how many failed or were degraded. The section is left out when no region has a recent run of a bound check.

### Groups

A group of components folds into one row: its name, how many components it has, its worst state, and one bar of 90 days that shows each day the colour of its worst component. A day with downtime, degradation or maintenance names, in its tooltip, the component that caused it. A group whose state is worse than operational is open when the page loads, so the reader sees which component it is. An operational group is closed. The reader opens and closes a group with a click, without script. Components with no group are listed on their own.

### Response times

A component with **Show response times on the page** turned on shows a chart under its daily bars:

- **The last 24 hours** as 48 bars, each the average response time over half an hour, from every source that runs the component's checks. A bar more than 1.5 times the day's average is marked, so a slow spell stands out.
- **Now**, the average over the last 15 minutes, and the 24-hour average.
- **By region**, over the last 15 minutes: the share of its runs that failed when any did, otherwise its average response time, otherwise **passing**. As in [Checks by region](#checks-by-region), only regions appear, never your agents' names.

Only check bindings feed it. The chart is left out when none of the component's checks timed a run in the last 24 hours.

A page whose components bind more than 50 distinct checks shows neither Checks by region nor response times, rather than figures that cover only some of its checks.

### An incident's progress

Each current incident on the front page, and every incident's permalink, shows its progress as five steps: Started, Investigating, Identified, Monitoring, Resolved. The steps it has passed are filled, and the one it is in is marked in the colour of its worst impact. Each step gives the time of the first public update at that stage. On the front page only the latest update is read, so earlier stages show no time there; the permalink reads up to 20 updates. A resolved incident's last step is when it resolved. On a narrow screen only the current step is labelled.

## Incidents on the page

An incident shows on a page when someone publishes it there and names at least one of the page's components, each with an impact (`degraded`, `partial` or `major`). It must have at least one public update. It cannot be in triage or carry a view password. Declined and duplicate incidents never show. While it is open, each named component holds a phase at that impact, from when the incident was opened until it is resolved.

Only public updates leave the team. The page never shows internal notes, the description, the cause and fix, the attached alerts or who worked on it. The stage shown is the one on the newest public update that has a stage. Publishing is for members and above. [Incidents](/docs/service/incidents/#publishing) covers how.

## Maintenance on the page

A maintenance window shows on a page only when its author turns that on and picks the components it affects, at most 50, from any of the team's pages, each bound to the window's project. While an occurrence runs, each picked component is under maintenance, in blue, unless something worse is happening. An outage during maintenance shows as the outage. The front page lists maintenance in progress and occurrences starting in the next 7 days. The history page lists completed ones. A window that is not shown never appears on the page, but the time it covered is still left out of uptime. Setting windows up is in [Maintenance windows](/docs/service/maintenance/).

## Custom domains

On pingtower.com, a public page can also live on a name of your own, such as `status.example.com`, instead of `{slug}.ptwr.net`. A self-hosted box already serves its own domain through `--status-host`.

Add the domain on the page's **Settings** tab. Then create two records with your DNS provider:

| Type | Name | Value |
| --- | --- | --- |
| TXT | `_pingtower-verify.status.example.com` | `pingtower-verify=` followed by the page's token, as the web app shows it |
| CNAME | `status.example.com` | the target the web app shows |

Then press **Verify**. Both records are looked up, and the domain is verified only when both hold. If you fix a record and try again, verification allows three attempts in a row, then one every 10 seconds. Once a domain is verified, it becomes the page's address. A Let's Encrypt certificate is issued for it on the first visit, and only verified domains get one.

- **Subdomains only.** A CNAME cannot sit at a zone's apex, so `example.com` itself is refused. Use a name such as `status.example.com`.
- **Public pages only.** A page made private keeps its domain, but the domain answers 404 until the page is public again. Member sign-in never happens on your domain.
- **One domain per page.** A claim that is still pending holds nothing. A verified domain stays with its page while the TXT record still carries that page's token.
- **Keep the TXT record.** Leave it published after verifying. A record that lapses changes nothing on its own: the domain keeps serving your page. But another team can add your domain, and its claim stays pending until it presses **Verify**. If its own TXT and CNAME records hold and your token is no longer in the TXT record at that moment, the domain moves to its page. While your token is still in the record, the other team is refused. A domain taken this way goes back to pending on your page, and the **Settings** tab says why. To win it back, publish your record again, remove the other team's token from the TXT record, and press **Verify**.
- **Not Pingtower's names.** `pingtower.com`, `ptwr.net` and every name under them are refused, and so are IP addresses.

Changing the page's slug keeps its domain. A verified domain only ever serves its own page. Once that page is deleted, the domain is released and answers 404. It never falls through to another page on its own, and serves another page only after a team adds it and verifies it as described above.

## Embeds

A public page can be shown on other sites in four ways. Each one answers like an unknown slug for a private page. None reads or sets a cookie, and each counts against the page's rate limit. The **Settings** tab's embed section builds the URLs, including one component's badge.

**Status badge.** The page's worst current state, such as "status: Operational". Add `?component=` followed by a component's id for that component alone, labelled with its name. Add `theme=dark` for a dark label.

```html
<img src="https://acme.ptwr.net/badge.svg" alt="Status">
<img src="https://acme.ptwr.net/badge.svg?theme=dark" alt="Status">
```

**Uptime badge.** The page's uptime as its bars compute it, over 90 days by default or 30 with `?days=30`. Any other value means 90. The badge is green from 99.9%, yellow from 99%, orange from 95%, red below that, and grey with "no data".

```markdown
![Uptime](https://acme.ptwr.net/uptime.svg?days=30)
```

**Incident banner.** One script tag. While a published incident is unresolved, it draws a banner where the tag sits, with one line per incident linking to its permalink and showing its stage, coloured by the worst impact. When nothing is unresolved, it draws nothing. A tag in `<head>` puts the banner at the top of `<body>`. Add `data-theme="dark"` for a dark banner. It lives in a closed shadow root, so your page's styles and scripts leave it alone, and it writes the page's text only as text, never as markup. It does not show component outages that have no published incident, and it does not show maintenance.

```html
<script src="https://acme.ptwr.net/embed.js" async></script>
<script src="https://acme.ptwr.net/embed.js" data-theme="dark" async></script>
```

**`embed.json`** is what the banner reads, and anything else may read it too. It is open to every origin (`Access-Control-Allow-Origin: *`) and never takes credentials. URLs in it are relative to the host it came from.

```json
{
  "title": "Acme status",
  "url": "/",
  "status": "partial",
  "status_label": "Partial outage",
  "incidents": [
    {
      "title": "Checkout errors",
      "url": "/incidents/0b6d…",
      "stage": "identified",
      "stage_label": "Identified",
      "impact": "partial",
      "started_at": "2026-10-02T03:12:00Z"
    }
  ],
  "updated_at": "2026-10-02T03:40:12Z"
}
```

`status` is `operational`, `degraded`, `partial`, `major` or `maintenance`. `incidents` lists the published incidents that are still open, and is empty otherwise. On a self-hosted box the same paths sit under `/status/{slug}/`, or on its status host.

## Limits

| | |
| --- | --- |
| Pages per team | 20 |
| Components per page | 100 |
| Bindings per component | 20 |
| Components per incident | 50 |
| Components per maintenance window | 50 |
| Logo | 128 KiB, PNG, JPEG or WebP |
| Acknowledgement fallback | 0 (never) to 1440 minutes |
| Phases kept after they end | 400 days |
| Requests per client address | 10 a second, burst 40 |
| Checks a page reads for regions and response times | 50 |

Not built yet: subscribers who get email or other notifications, RSS or Atom feeds, and a `status.json` API. `embed.json` is the one machine-readable feed.
