Skip to content
Docs/Status pages

Status pages

Status pages

A status page is the public face of your monitoring. Components show live status from monitors, pushes or manifests, and incidents and maintenance appear on it automatically.

Create a page

curl -X POST https://upbutler.com/api/v1/pages \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Status",
    "slug": "acme",
    "theme": { "preset": "midnight", "accent": "#22c55e" },
    "branding": { "logoText": "Acme", "websiteUrl": "https://acme.com", "supportUrl": "https://acme.com/support" }
  }'

The page is live at https://upbutler.com/s/acme. Wherever the API takes a pageId, you can pass either the id (pg_…) or the slug. Your plan sets how many pages you can have: 1 on Free, 1 on Starter, 5 on Pro, 20 on Business.

Slugs

Lowercase letters, digits and dashes, 3–48 characters, globally unique. If you leave it out, it is derived from the name (and gets -status appended if the result is reserved or too short). Reserved: app, api, admin, www, status, docs, login, new, pricing, blog, help, support, mail. A taken slug returns 409 conflict.

Page fields

Accepted by POST /pages and PATCH /pages/:pageId. Updates merge: nested objects (theme, branding, settings, seo) only change the keys you send. In branding, sending null removes a value.

FieldTypeDescription
namerequiredstringmax length 100
slugstringPublic URL slug (/s/<slug>). 3–48 characters, or omit to derive it from name.pattern ^[a-z0-9](?:[a-z0-9-]{1,46}[a-z0-9])?$
descriptionstringShown under the page title.max length 500
customDomainstring | nullStarter and up (rolling out). A hostname such as status.example.com. null removes it.pattern ^(?=.{4,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$ · nullable
visibilitystringpublic (default) or private.publicprivate
themeobject
theme.presetstringOne of: midnight, grid, daylight, paper.max length 30
theme.modestringdark or light. Set by the preset.darklight
theme.accentstringHex color such as #22c55e.pattern ^#[0-9a-fA-F]{6}$
theme.tokensmap<string, string>Override individual color tokens. See Themes.
theme.fontSansstringUI font: Geist, Inter, Manrope, IBM Plex Sans, Space Grotesk, System.max length 40
theme.fontMonostringMono font: Geist Mono, IBM Plex Mono, JetBrains Mono, System Mono.max length 40
theme.radiusintegerCorner radius in px.min 0 · max 24
theme.customCssstringPro and up. Raw CSS appended to the page.max length 50,000
brandingobject
branding.logoUrlstring | nullmax length 1,000 · nullable
branding.logoDarkUrlstring | nullmax length 1,000 · nullable
branding.logoTextstring | nullmax length 60 · nullable
branding.faviconUrlstring | nullmax length 1,000 · nullable
branding.websiteUrlstring (url) | nullmax length 500 · nullable
branding.supportUrlstring (url) | nullmax length 500 · nullable
branding.headerLinksobject[]max 8 items
branding.headerLinks[].labelrequiredstringmax length 60
branding.headerLinks[].urlrequiredstring (url)max length 500
branding.footerLinksobject[]max 20 items
branding.footerLinks[].labelrequiredstringmax length 60
branding.footerLinks[].urlrequiredstring (url)max length 500
branding.footerTextstring | nullmax length 300 · nullable
branding.hidePoweredBybooleanStarter and up. Removes the “Powered by UpButler” footer (also in subscriber emails).
settingsobject
settings.uptimeDaysintegerDays of uptime history bars (default 90).min 7 · max 90
settings.showUptimePercentbooleanShow uptime % next to components (default true).
settings.showResponseTimesbooleanAllow public response-time charts (components opt in with showResponseTimes)
settings.languagestringPublic page + subscriber email language; auto = visitor Accept-Languageautoendefresitptnlelpltrja
settings.autoIncidentsbooleanMonitor and push failures open public incidents automatically (default true).
settings.incidentMinStatusstringLowest component status that opens an automatic incidentdegradedpartial_outagemajor_outage
settings.aiPublicUpdatesbooleanUse AI-written public summaries for automatic incidents (default true).
settings.aiGuidelinesstringRules for AI-written public text, e.g. "Never mention internal infrastructure"max length 2,000
settings.aiAvoidTermsstring[]AI public text containing any of these is replaced by a neutral templatemax 50 items
settings.subscriptionsobjectWhich subscription types visitors may use (all default true).
settings.subscriptions.emailboolean
settings.subscriptions.webhookboolean
settings.subscriptions.slackboolean
settings.subscriptions.rssboolean
settings.timezonestringIANA zone for dates on the page (default UTC).max length 60
seoobject
seo.titlestringmax length 120
seo.descriptionstringmax length 300
seo.noindexbooleanAsk search engines not to index the page.

Themes

Pick a preset, then adjust the accent, individual tokens, fonts and radius. Switching presets resets the token overrides and applies that preset's accent, fonts and radius, unless you send them in the same request.

PresetLookModeAccentFontsRadius
midnightDark, quiet and precise. Hairline borders, mono labels.dark#22c55eInter / JetBrains Mono12px
gridDeveloper-tool dark with a masked line grid and soft glow behind the header.dark#6366f1Manrope / IBM Plex Mono12px
daylightClean light theme for consumer-facing brands.light#16a34aInter / JetBrains Mono12px
paperWarm off-white with an editorial feel.light#c2410cIBM Plex Sans / IBM Plex Mono6px

Design tokens

theme.tokens overrides the CSS custom properties (--ub-<token>) used by the page renderer: bg, surface, surface-2, border, border-strong, text, muted, faint, ok, warn, partial, bad, maint, unknown, accent, accent-fg. The Grid preset also uses glow and grid.

Token names must match [a-z0-9-] (max 30 characters). Values may contain only letters, digits, spaces and #(),.%- (max 80 characters), so #0b0c0e and rgba(255,255,255,0.08) work. Anything else is silently dropped.

PATCH /pages/acme
{
  "theme": {
    "preset": "paper",
    "accent": "#1d4ed8",
    "radius": 4,
    "tokens": { "bg": "#fbf8f1", "ok": "#15803d" }
  }
}

Custom CSS (theme.customCss, up to 50,000 characters; Pro and up) is appended to the page. <style> and <script> tags are stripped. Sending custom CSS on a plan without it returns 402 plan_limit. Hiding the “Powered by UpButler” footer (branding.hidePoweredBy) needs Starter or up.

Groups

Groups are the sections of a page. They are created on the fly when you add a component with a new group name. To reorder, rename, describe or collapse them, replace the whole list:

curl -X PUT https://upbutler.com/api/v1/pages/acme/groups \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "groups": [
      { "id": "grp_0n3q8kz4a1b2c3d4e5f", "name": "APIs" },
      { "name": "Dashboard", "description": "app.acme.com" },
      { "name": "Integrations", "collapsed": true }
    ]
  }'

Order in the array is display order. Send an existing group's id to keep it (and its components). Entries without a known id become new groups. Components in groups you leave out become ungrouped.

Components

A component is one row on the page. POST /pages/:pageId/components creates it, PATCH /components/:id updates it and DELETE /components/:id removes it. Your plan sets how many components a page can have: 10 on Free, 30 on Starter, 100 on Pro, 500 on Business. One more returns 402 plan_limit. After a downgrade, components over the cap are hidden from the bottom of the page up and come back on their own when you upgrade (see Downgrading).

FieldTypeDescription
namerequiredstringmax length 100
keystringStable machine key used by pushes and manifests (default: slug of name)pattern ^[a-z0-9][a-z0-9_.-]{0,62}$
descriptionstringmax length 500
groupstringGroup name or id; created if it does not existmax length 80
monitorIdsstring[]Status follows these monitorsmax 20 items
aggregatestringworstmajority
pushbooleanStatus is pushed via the component push URL
pushTtlSecinteger | nullBecome "unknown" when no push arrives for this long (null clears it)min 60 · max 604,800 · nullable
manualbooleanSwitch the component to manual status (set via overrides/incidents)
manifestobjectStatus read from a manifest monitor key
manifest.monitorIdrequiredstring
manifest.keyrequiredstringmax length 120
showUptimeboolean
showResponseTimesbooleanPublic response-time chart (monitor-backed components only; page setting showResponseTimes must be on)
hiddenboolean
orderintegermin 0 · max 10,000
confirmationsintegerPush/manifest: consecutive fresh bad reports before a worse status shows (default 2; 1 = immediate)min 1 · max 10

key is the component's stable machine name. Pushes, manifests and subscriber filters use it, it is unique per page, and it defaults to the slugified name (“Search API” → search-api). group takes a group name or id. On update, "" removes the component from its group.

Sources

A component gets its status from exactly one source. If you send several, monitorIds wins over manifest, which wins over push. Sending none creates a manual component.

Examples
# Follows two monitors (e.g. two regions)
curl -X POST https://upbutler.com/api/v1/pages/acme/components \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Search API", "group": "APIs", "monitorIds": ["mon_0n3q8kz1m4hx7c2v9rt", "mon_0n3q8kz9z8y7x6w5v4u"]}'

# Fed by pushes; "unknown" if silent for 10 minutes
curl -X POST https://upbutler.com/api/v1/pages/acme/components \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Data pipeline", "key": "pipeline", "push": true, "pushTtlSec": 600}'

# Read from a manifest monitor
curl -X POST https://upbutler.com/api/v1/pages/acme/components \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"name": "Reddit", "group": "Platforms", "manifest": {"monitorId": "mon_0n3q8kzm4n1f3s7t2qa", "key": "reddit"}}'
SourceSet withBehavior
monitormonitorIds (up to 20), aggregateRecomputed whenever a linked monitor changes state. Starts from the monitors' current state.
pushpush: true, pushTtlSecYou POST statuses to source.pushUrl (returned in the response). Starts as unknown. See Push & manifest.
manifestmanifest: {monitorId, key}Reads key from a manifest monitor on every check. Starts as unknown.
manual(default)Starts operational. Change it with overrides or incidents.

Combining several monitors

Paused and pending monitors are ignored. aggregate decides how the remaining monitors combine:

Monitorsworst (default)majority
Any downmajor_outageMore than half down: major_outage. Otherwise: partial_outage
None down, any degradeddegradeddegraded
All upoperationaloperational
All paused or pendingunknownunknown

Use majority for redundant setups, such as the same service checked from several endpoints or replicas, where one failing instance is a partial outage rather than a full one.

Overrides

An override pins a component's displayed status above whatever its source says. Incidents set overrides for the components they list, and maintenance sets maintenance for its window. Both clear them on resolution. You can also set one by hand:

curl -X POST https://upbutler.com/api/v1/components/cmp_0n3q8kz2p7wd4yx0s3a/override \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"status": "degraded", "reason": "Upstream provider throttling"}'

# Clear it
curl -X POST https://upbutler.com/api/v1/components/cmp_0n3q8kz2p7wd4yx0s3a/override \
  -H "Authorization: Bearer $UPBUTLER_API_KEY" -H "Content-Type: application/json" \
  -d '{"status": null}'

API responses show both status (from the source) and effectiveStatus (what visitors see). For real outages, prefer an incident: it also notifies subscribers.

Custom domains

Custom domains are included from Starter: 1 on Starter, 5 on Pro, 20 on Business. They are still rolling out (see below); until then, every page on every plan is live at https://<slug>.upbutler.com. Free pages can't use one (402 plan_limit), and the error names the plan that can.

Serve the page from your own hostname:

  1. PATCH /pages/acme with {"customDomain": "status.acme.com"}.
  2. Coming soon. Until bring-your-own domains roll out, every page is live at https://<slug>.upbutler.com (and /s/<slug>). Setting customDomain returns 503 unavailable with your page address in the hint.
  3. The domain is marked verified on the first request that reaches us through it. From then on, page links, emails and webhook payloads use https://status.acme.com.

A hostname can belong to only one page (409 conflict). Send "customDomain": null to remove it. After a downgrade, domains beyond the new plan's count are removed (the oldest pages keep theirs) and those pages stay online at their upbutler.com address.

Settings

  • autoIncidents: when a linked monitor goes down, or a push or manifest component reaches incidentMinStatus, a public incident opens on this page and resolves on recovery. Turn it off to keep automatic incidents internal: your team is still alerted.
  • incidentMinStatus: the lowest component status that opens an automatic incident: degraded, partial_outage (default) or major_outage. It applies to push- and manifest-fed components. With major_outage, a partial outage still colors the component but doesn't open an incident or notify subscribers. A monitor that goes down always opens one. Maintenance never does.
  • aiPublicUpdates: automatic incidents get an AI-written title and customer-safe summary, and recovery posts get an AI summary. With it off, a neutral template is used. If an incident spans several pages, every page must have this on. See AI policy.
  • subscriptions: email, webhook, slack (covers Slack and Discord) and rss. See Subscribers.
  • uptimeDays (7–90) and showUptimePercent control the uptime bars. timezone sets the zone for server-rendered dates. Visitors' browsers re-render times in their own zone.
  • language: the language of the public page and subscriber emails: en, de, fr, es, it, pt, nl, el, pl, tr, ja, or auto to follow each visitor's browser (Accept-Language).
  • showResponseTimes: allows public response-time charts. Each component also opts in with its own showResponseTimes, which only works for monitor-backed components. See Response times.
  • visibility: "private" hides the page from the public API and subscriptions.

AI policy

Two settings control what AI may write on your page. They apply to automatic incident titles and summaries, recovery summaries, polish: true rewrites and published postmortems:

  • aiGuidelines (up to 2,000 characters): rules the AI must follow for all public text, such as “Never mention internal infrastructure or third-party vendors. Refer to the API as ‘the Acme API’.”
  • aiAvoidTerms (up to 50 terms, 2–60 characters each): words that must never appear publicly, such as internal codenames, vendor names or kubernetes. They're passed to the AI, and every AI result is also checked case-insensitively. If the text contains a term, it isn't used. Automatic incidents and recoveries fall back to the neutral template, a polished update falls back to your original text, and publishing a postmortem is rejected with the reason.
PATCH /pages/acme
{
  "settings": {
    "aiGuidelines": "Be brief. Never name our cloud provider or internal services.",
    "aiAvoidTerms": ["hetzner", "postgres", "project-falcon"]
  }
}

When an incident spans several pages, the guidelines of all pages are combined, and so are their avoided terms. The team-facing AI analysis isn't restricted by these settings.

Response times

With settings.showResponseTimes on and a monitor-backed component's showResponseTimes on, the page shows a response-time chart for that component. The same data is public JSON:

curl "https://upbutler.com/api/v1/public/pages/acme/components/search-api/response-times?range=7d"

range is 24h (default, 30-minute buckets), 7d (3-hour buckets) or 30d (daily). 24h and 7d give p50 and p95 in ms from raw checks. 30d gives the daily average in p50, with p95 null (kind: "daily_average"). Components with several monitors average them. Components that haven't opted in return 404.

Public JSON API

Every public page has a JSON view that needs no auth and is safe to poll every 30 seconds:

curl https://upbutler.com/api/v1/public/pages/acme
200 OK (abridged)
{
  "page": { "id": "pg_0n3q8kz0b2fd81mka5e", "slug": "acme", "name": "Acme Status", "description": null, "url": "https://status.acme.com", "websiteUrl": "https://acme.com" },
  "status": "degraded",
  "statusText": "Degraded performance",
  "groups": [
    {
      "id": "grp_0n3q8kz4a1b2c3d4e5f", "name": "APIs", "description": null, "collapsed": false, "status": "degraded",
      "components": [
        {
          "id": "cmp_0n3q8kz2p7wd4yx0s3a", "key": "search-api", "name": "Search API", "description": null,
          "group": "grp_0n3q8kz4a1b2c3d4e5f", "status": "degraded", "statusMessage": "Slow response: 2140ms (threshold 1500ms)",
          "updatedAt": "2026-10-09T08:41:02.118Z", "uptime": 99.982,
          "days": [{ "date": "2026-10-09", "status": "degraded", "uptime": 100, "downSec": 0, "degradedSec": 1260, "incidents": [] }]
        }
      ]
    }
  ],
  "activeIncidents": [],
  "maintenance": [],
  "recentIncidents": [],
  "subscribe": { "url": "https://upbutler.com/api/v1/public/pages/acme/subscribers", "types": ["email", "webhook", "slack", "rss"] },
  "generatedAt": "2026-10-09T08:42:10.004Z"
}

Pass ?history=false to drop the per-day bars. History lives at GET /public/pages/:slug/incidents (with limit and before) and GET /public/pages/:slug/incidents/:id. Only public updates are included, never internal notes or AI engineering analysis. To be notified instead of polling, subscribe.