Skip to content
  • There are no suggestions because the search field is empty.

Status Hub Pages, Incidents and Maintenance with the AlertOps SDKs

Build status pages from services and service groups, change pages with status_hub.pages.patch (PUT replaces the whole record), and post incidents and maintenance with the SDKs' quiet defaults so subscribers are only notified when you say so.

Overview

Relevant for Developers publishing service status to customers or staff

Status Hub publishes the health of your services. You keep a catalog of services (optionally in service groups), lay them out on status pages, and post incidents and scheduled maintenance that the pages show. Subscribers can follow a page by email and other channels. The SDKs cover the management API (status_hub, v3) and the viewer API that renders pages (status_viewer).

Key capabilities:

  • Whole-record safety — status_hub.pages.patch and status_hub.maintenance.patch read the record, overlay your changes and write it back.
  • Quiet defaults — incidents, updates and maintenance are created with notifySubscribers: false unless you opt in.
  • Layout helper — status_hub.pages.services.set_layout builds the grouped-services layout correctly.
  • Viewer access — status_viewer.public reads published pages with no credentials. status_viewer.internal reads internal pages with the API key.

Reading the operation names

Relevant for Developers using any of the five AlertOps SDKs

This guide applies to every AlertOps SDK. It names each operation by its resource path and operation name, such as groups.members.add_users, lists positional arguments in order, and shows request bodies and options as JSON. Where a task needs real code, the example is given in all five languages. Each SDK spells the same call in its own style:

SDKThe same callOptions and request fields
Pythonao.groups.members.add_users(group_id, [101, 102], roles=["Primary"])Keyword arguments, such as search="Net"
TypeScriptawait ao.groups.members.addUsers(groupId, [101, 102], ["Primary"])One trailing options object keyed by the names in this guide, such as { search: "Net" }
Goao.Groups.Members.AddUsers(ctx, groupID, []any{101, 102}, []any{"Primary"})&alertops.Request{Fields: alertops.M{"search": "Net"}} for endpoints, alertops.M{…} for helper options
Javaao.groups().members().addUsers(groupId, List.of(101, 102), List.of("Primary"))Request.of("search", "Net") for endpoints, a Map keyed by the names in this guide for helper options
.NETawait ao.Groups.Members.AddUsersAsync(groupId, new object?[] { 101, 102 }, new object?[] { "Primary" })new Kwargs { ["search"] = "Net" }
  • Names follow each language's style: update_basic is updateBasic in TypeScript and Java, UpdateBasic in Go and UpdateBasicAsync in .NET. Go keeps initialisms upper case (create_api is CreateAPI, post_to_url is PostToURL).
  • Paginated operations also have variants that collect every page or iterate lazily (…All / …Iter; in Python, .all() and .iter() on the operation).
  • Helpers that look something up return null (None in Python, nil in Go) when nothing matches. **ensure helpers** return the record together with a flag that says whether it was created.
  • Errors are the same classes in every SDK, such as NotFoundError and BadRequestError.
  • Code examples come in the order Python, TypeScript, Go, Java, .NET. Each assumes a client named ao (and, in Go, a ctx and a function that returns error), created as in that SDK's Getting Started article.

For one language's details, see Calling the API from Python, TypeScript, Go, Java or .NET.

Before you start

Relevant for Developers setting up the client

Every operation in this guide hangs off status_hub (pages, services, service groups, incidents, maintenance, templates, notifications) or status_viewer on the client. Status Hub uses numeric codes:

FieldValues
Incident status1 Investigating, 2 Identified, 3 Monitoring, 4 Resolved
Incident impact1 None, 2 Minor, 3 Major, 4 Critical
Service currentStatus1 Operational, 3 Partial Outage
Maintenance status (writes)1 Scheduled, 2 In Progress
Access permissionId1 Owner, 2 Edit, 3 View

Services, group memberships, incident and maintenance history, and subscribers are cursor-paged (pageSize plus after). The paginated variants (…All / …Iter) follow the cursor for you. Pages and service groups are bare lists.

Status pages are capped per account
On the tested Enterprise Trial plan, the account allows 3 status pages in total. A fourth status_hub.pages.create fails with 402 StatusPage.MaxTotalPages (PaymentRequiredError), even though the account limits read -99 (unlimited). Reuse pages in tests instead of creating new ones.

Create a status page

Relevant for Developers creating and publishing a page

status_hub.pages.create always creates a draft and returns the page with its statusPageId. timezoneId is the numeric id of the zone in lookups.time_zones() (for example, the entry whose zone is America/Chicago).

{
"pageSlug": "acme-status", "displayName": "Acme Service Status",
"description": "Live status of Acme services",
"visibilityType": "public", "timezoneId": 11,
"customDomain": {"domain": null, "subdomainAlias": "acme-status"},
"allowSubscriptions": true, "enabledChannels": ["Email"]
}
visibilityTypeWho can see the page
publicAnyone, at the subdomain alias (or a custom domain)
private_ssoVisitors who sign in through the page's SAML identity provider
internalUsers of your AlertOps account, through the internal viewer. Create it with customDomain: null, allowSubscriptions: false and enabledChannels: []

The SDKs fill in isPublished: false, showUptimeHistory: true and a default branding (primary colour #0ea5e9) when you leave them out.

Change a page

pages.update replaces the whole page
PUT /status_pages/{id} is a whole-record replace and requires pageSlug and branding (400 without them). Use status_hub.pages.patch(pid, changes), which reads the page, keeps every writable field and overlays yours.

OperationEffect
status_hub.pages.patch(pid, {displayName: "Acme Status", branding: {…, primaryColor: "#112233"}})Changes the name and colour, keeps everything else. Send the page's whole branding object with your change
status_hub.pages.publish(pid)Returns nothing; the page is now served publicly
status_hub.pages.unpublish(pid)Returns nothing

Branding, custom domains and SSO

  • Logo and favicon — status_hub.pages.assets.upload_logo(pid, file) uploads a multipart file part (for example logo.png, image/png) and sets branding.logoUrl itself. status_hub.pages.assets.delete(pid, "logo") clears it.
  • Custom domain — patch the page's customDomain.domain first. status_hub.pages.custom_domains.create(pid, {domain}) is 400 until the page carries the domain. It returns a Pending verification with dnsRecordName and dnsRecordValue for your DNS. To clear the domain, patch domain: "". null keeps the old value.
  • SSL — status_hub.pages.ssl.warmup(pid) is 400 "Custom domain is not verified" until DNS verifies. status_hub.pages.ssl.refresh(pid) works regardless.
  • SSO — status_hub.pages.sso.update(pid, {samlMetadataUrl, samlSignOnUrl, samlSignOutUrl, samlCertificate}) switches the page to private_sso, and status_hub.pages.sso.delete(pid) switches it back to public. samlCertificate must be a parseable X.509 certificate (base64 DER), or the viewer's SSO login fails with 400 "ASN1 corrupted data". status_hub.pages.sso.get is 404 until a config exists.

Services, groups and page layout

Relevant for Developers maintaining the service catalog

Services and service groups are account-wide. A page shows the ones in its layout.

StepOperationResult
1status_hub.services.create({name: "Checkout API", description: "Payments endpoint"})Service with serviceId (currentStatus 1)
2status_hub.services.create({name: "Storefront Web"})A second service
3status_hub.service_groups.create({name: "Commerce", description: "Customer-facing"})Group with serviceGroupId
4status_hub.service_groups.services.add(gid, {services: [{serviceId: 201, displayOrder: 0}, {serviceId: 202, displayOrder: 1}]})Both services in the group
5status_hub.pages.services.set_layout(pid, {groups: {103: [201, 202]}})The page shows the group with its services

status_hub.pages.services.set_layout(pid, {service_ids: [201], groups: {103: [202, 203]}}) replaces the page layout: grouped services first, then ungrouped ones, all visible.

A group item alone attaches no services
In status_hub.pages.services.add and update, an item {"type": "group", "groupId": ...} only carries the group's display options. Each grouped service must be its own item {"type": "service", "serviceId": ..., "groupId": ...}. set_layout sends both kinds for you.

Delete in order: empty groups first
Deleting a service that belongs to a service group, or a group that still has services, fails with 409 ConflictError ("referenced by ..."). Remove memberships with status_hub.service_groups.services.remove(gid, {serviceIds: [202, 203]}) first, then delete the group, then the services.

Incidents, updates and postmortems

Relevant for Incident commanders communicating an outage

Open an incident, post an update, then resolve it:

Python

inc = ao.status_hub.incidents.create(title="Checkout errors", description="<p>Some payments fail.</p>",
status=1, impact=3, affectedServiceIds=[api_id])
ao.status_hub.incidents.updates.create(inc["incidentId"], status=2, message="Cause identified: a bad deploy.")
ao.status_hub.incidents.resolve(inc["incidentId"], "Rolled back. All payments succeed.")

TypeScript

const inc = await ao.statusHub.incidents.create({
title: "Checkout errors", description: "<p>Some payments fail.</p>",
status: 1, impact: 3, affectedServiceIds: [apiId],
});
await ao.statusHub.incidents.updates.create(inc.incidentId, { status: 2, message: "Cause identified: a bad deploy." });
await ao.statusHub.incidents.resolve(inc.incidentId, "Rolled back. All payments succeed.");

Go

inc, err := ao.StatusHub.Incidents.Create(ctx, &alertops.Request{Fields: alertops.M{
"title": "Checkout errors", "description": "<p>Some payments fail.</p>",
"status": 1, "impact": 3, "affectedServiceIds": []any{apiID},
}})
if err != nil {
return err
}
iid := alertops.Get(inc, "incidentId")
if _, err = ao.StatusHub.Incidents.Updates.Create(ctx, iid,
&alertops.Request{Fields: alertops.M{"status": 2, "message": "Cause identified: a bad deploy."}}); err != nil {
return err
}
_, err = ao.StatusHub.Incidents.Resolve(ctx, iid, "Rolled back. All payments succeed.", false)

Java

JsonNode inc = ao.statusHub().incidents().create(Request.of(
"title", "Checkout errors", "description", "<p>Some payments fail.</p>",
"status", 1, "impact", 3, "affectedServiceIds", List.of(apiId)));
JsonNode iid = inc.get("incidentId");
ao.statusHub().incidents().updates().create(iid, Request.of("status", 2, "message", "Cause identified: a bad deploy."));
ao.statusHub().incidents().resolve(iid, "Rolled back. All payments succeed.", false);

.NET

var inc = await ao.StatusHub.Incidents.CreateAsync(new Kwargs
{
["title"] = "Checkout errors", ["description"] = "<p>Some payments fail.</p>",
["status"] = 1, ["impact"] = 3, ["affectedServiceIds"] = new[] { apiId },
});
var iid = inc!["incidentId"]!;
await ao.StatusHub.Incidents.Updates.CreateAsync(iid, new Kwargs { ["status"] = 2, ["message"] = "Cause identified: a bad deploy." });
await ao.StatusHub.Incidents.ResolveAsync(iid, "Rolled back. All payments succeed.");

resolve(incident_id, message, notify_subscribers) posts a status-4 update and returns the incident (status 4, resolvedAt set). incidents.updates.list returns the newest update first. status_hub.incidents.update(iid, {title, impact}) changes the incident itself.

Subscribers are only notified when you ask
The SDKs send notifySubscribers: false on incidents.create and incidents.updates.create unless you pass notifySubscribers: true (or pass true as resolve's notify_subscribers argument). The API itself has no such safety.

Postmortems hang off the incident. Their fields are HTML strings, and only a published postmortem appears on the public page. Create one with status_hub.incidents.postmortem.create(iid, body), then publish it with status_hub.incidents.postmortem.publish(iid), which returns the postmortem with isPublished: true.

{
"summary": "<p>Deploy 418 broke card tokenisation.</p>",
"rootCause": "<p>Config typo.</p>",
"impact": "<p>12% of payments failed for 25 minutes.</p>",
"resolution": "<p>Rollback.</p>",
"actionItems": "<p>Add a config check to CI.</p>"
}

status_hub.incidents.postmortem.get is 404 until a postmortem exists.

Scheduled maintenance

Relevant for Ops teams announcing planned work

status_hub.maintenance.create(body) takes ISO 8601 UTC date-times without fractional seconds and returns the window with its maintenanceWindowId:

{
"title": "Database upgrade", "description": "<p>Read-only for 1 hour.</p>",
"scheduledStartDate": "2026-11-15T02:00:00Z", "scheduledEndDate": "2026-11-15T03:00:00Z",
"affectedServiceIds": [201]
}

The SDKs create maintenance quietly: notifySubscribers, sendReminderAtStart, sendReminderAtEnd, autoStartEnabled and autoCompleteEnabled are false, and reminderHoursBeforeStart is 1 (the API requires at least 1 even with reminders off).

A maintenance PUT turns notifications back on
PUT /status_pages/maintenance_windows/{id} resets notifySubscribers, sendReminderAtStart and sendReminderAtEnd to true when they are omitted, so a plain update can notify subscribers. Use status_hub.maintenance.patch(mid, changes), which reads the window and sends every flag back.

To finish a window, move it to In Progress, then complete it. complete works only on status 2 (In Progress) and is 400 otherwise.

  1. status_hub.maintenance.patch(mid, {status: 2})
  2. status_hub.maintenance.complete(mid, {actualEndDate: "2026-11-15T02:55:00Z"})

Create and update responses show affectedServices: [] even though the services are attached; status_hub.maintenance.upcoming({daysAhead: 30}) and maintenance.list show them. Maintenance updates (maintenance.updates.*) are dead routes (404).

Templates, subscribers and access

Relevant for App Admins standardising messages and controlling who manages pages

Templates

status_hub.templates.incident.create(body) creates an incident template:

{
"name": "Payment outage", "titleTemplate": "Payment processing issues",
"descriptionTemplate": "<p>We are investigating.</p>",
"defaultImpact": 3, "defaultStatus": 1, "defaultServiceStatus": 3, "defaultServiceIds": [201],
"updateTemplates": [{"message": "Investigating", "status": 1, "notifySubscribers": false}]
}

status_hub.templates.maintenance.create({name: "DB upgrade", titleTemplate: "Database upgrade", descriptionTemplate: "<p>Read-only mode.</p>"}) creates a maintenance template. status_hub.templates.list_all() returns both kinds, each tagged with type incident or maintenance. Template lists default to active templates; pass activeOnly: false for all.

Subscribers

The subscriber list hides unverified subscribers
status_hub.pages.subscribers.list(pid) returns verified subscribers only. Pass status: "all" (or "unverified") to see pending ones, for example with the paginated variant of status_hub.pages.subscribers.list.

subscriberType is 1 Email, 2 SMS, 3 Webhook, 4 Slack, 5 Teams. status_hub.pages.subscribers.delete(pid, subscriber_id) removes one.

Access

Pages, services and service groups share one access API: grant, revoke, update, restrict, unrestrict, permission and transfer_ownership.

OperationResult
status_hub.pages.access.grant(pid, {user_ids: [101], permission: "view"})Returns the access list
status_hub.pages.access.restrict(pid)Custom (restricted) access on
status_hub.pages.access.permission(pid)isRestricted is now true

unrestrict drops the custom grants. transfer_ownership(eid, {newOwnerUserId}) is one-way for the caller: afterwards a transfer back fails with 400 "Current owner and new owner must be different users". An unrestricted object stays editable and deletable by an admin key.

The public and internal viewer

Relevant for Developers embedding or checking what visitors see

status_viewer.public calls the anonymous {v3}/public/* API. It sends no credentials, and only published pages are served (404 otherwise). Resolve the organization and page slugs from the subdomain alias first.

OperationReturns
status_viewer.public.resolve("acme-status")The organization and page slugs (from by_alias(alias))
status_viewer.public.page(org, slug)name, overallStatus, requiresSsoAuthentication …
status_viewer.public.services(org, slug, {grouped: true}){groups[], ungroupedServices[]}
status_viewer.public.active_incidents(org, slug)Open incidents
status_viewer.public.incidents(org, slug, {startDate: "2026-11-01", endDate: "2026-11-05"})Incident history

status_viewer.public.subscribe(org, slug, {address: "ops@example.com"}) returns {success, requiresVerification} and emails a verification token. That token is never returned by any API, so verify_subscription, subscriber_preferences and unsubscribe_token only work with a token taken from the email.

status_viewer.internal reads pages by pageSlug with the API key (the organization comes from the key): status_viewer.internal.page("acme-status"). status_viewer.internal.subscribe(slug) subscribes the calling user's own contact. It works only on internal pages (400 "only available for internal status pages" otherwise) and is 403 for keys of Basic-role users.

status_viewer.sso_login(pid, {returnUrl}) is 404 for pages without SSO. On an SSO page it follows the redirect to the identity provider and returns the final page as text.

Status Hub operations

OperationHTTPWhat it does
status_hub.pages.create / get / list / find(slug)/status_pagesCreate a draft page, read one, list all, find by pageSlug
status_hub.pages.patch(pid, changes)GET + PUT /status_pages/{id}Change fields safely
status_hub.pages.publish(pid) / unpublish(pid)POST .../publish, .../unpublishPublish or unpublish (returns nothing)
status_hub.pages.services.set_layout(pid, {service_ids, groups})PUT .../services/groupedReplace the page layout
status_hub.pages.subscribers.list(pid, {status: "all"})GET .../subscribersSubscribers, verified only by default
status_hub.services.create / update / delete / list/status_pages/servicesService catalog (cursor-paged list)
status_hub.service_groups.create / services.add / services.remove/status_pages/service_groupsGroups and their memberships
status_hub.incidents.create / updates.create / resolve/status_pages/incidentsIncidents and their updates
status_hub.incidents.postmortem.create / publish.../incidents/{id}/postmortemPostmortems
status_hub.maintenance.create / patch / complete / upcoming/status_pages/maintenance_windowsScheduled maintenance
status_hub.templates.incident.* / maintenance.* / list_all()/status_pages/*_templatesIncident and maintenance templates
status_hub.dashboard() / notifications.usage()GETHub totals, notification quota per channel
status_viewer.public.resolve(alias) / page / services / incidentsGET {v3}/public/...Anonymous viewer reads
status_viewer.internal.page(slug) / subscribe(slug){v3}/status_pages/internal/{slug}Internal viewer with the API key

The legacy pre-status_pages family (status_hub.legacy.*) and pages.access.set_status are dead routes (404). Each SDK's method reference lists every other operation.

Best Practices

Relevant for Anyone running status pages in production

Changing pages and maintenance

  • Change pages with pages.patch and maintenance with maintenance.patch. Never PUT a partial record.
  • Build the layout with set_layout, so grouped services are attached.

Notifying subscribers

  • Keep notifySubscribers: false while you test, and opt in explicitly for real communications.
  • Check pages.subscribers.list(pid, {status: "all"}) before announcing, so you know who will be notified.

Capacity and cleanup

  • Count your pages before creating one; the plan cap is account-wide.
  • Tear down in order: memberships, groups, services, then the page.
  • Transfer ownership only when you mean it. You can't take it back with the same key.

Configuration Examples

Relevant for Starting templates for common setups

Example 1 — Minimal: an internal page with two services

This is the shape the SDK end-to-end scenario built and verified.

StepOperation
1status_hub.pages.create({pageSlug: "acme-internal", displayName: "Acme Internal Status", visibilityType: "internal", timezoneId: <id>, customDomain: null, allowSubscriptions: false, enabledChannels: []})
2status_hub.services.create({name: "Checkout API", description: "Checkout API"}) and status_hub.services.create({name: "Storefront Web", description: "Storefront Web"})
3status_hub.service_groups.create({name: "Commerce", description: "Customer-facing"})
4status_hub.service_groups.services.add(gid, {services: [{serviceId: <api>, displayOrder: 1}, {serviceId: <web>, displayOrder: 2}]})
5status_hub.pages.services.set_layout(statusPageId, {groups: {<gid>: [<api>, <web>]}})

Example 2 — Typical: publish, then run an incident to resolution

StepOperation
1status_hub.pages.publish(pid)
2status_hub.incidents.create({title: "Elevated checkout latency", description: "<p>Investigating.</p>", status: 1, impact: 2, affectedServiceIds: [<api>], notifySubscribers: true})
3status_hub.incidents.updates.create(iid, {status: 3, message: "Fix deployed, monitoring.", notifySubscribers: true})
4status_hub.incidents.resolve(iid, "Latency is back to normal.", true)

Use with caution: this notifies every verified subscriber three times.

Example 3 — Read what visitors see

status_viewer.public.resolve("acme-status") returns the organization and page slugs; status_viewer.public.page(org, slug) then returns the page's name and overallStatus. The viewer calls need no API key, and the async clients expose the same operations.

Troubleshooting

Relevant for Anyone debugging Status Hub calls

402 StatusPage.MaxTotalPages when creating a page

  • The account already has its maximum number of pages (3 on the tested trial plan). Delete or reuse a page.

400 when updating a page

  • pages.update was called without pageSlug or branding. Use pages.patch.

Services don't appear under their group on the page

  • The layout had a group item but no {"type": "service", "groupId": ...} items. Rebuild it with set_layout.

409 "referenced by ..." when deleting

  • The service is still in a group, or the group still has services. Empty the group with service_groups.services.remove first.

Subscribers were notified after a maintenance edit

  • A raw maintenance.update omitted the notify and reminder flags, which reset to true. Use maintenance.patch.

complete fails with 400

  • The window isn't In Progress. Run maintenance.patch(mid, {status: 2}) first.

A new subscriber is missing from the list

  • Unverified subscribers are hidden by default. List with status: "all".

The public viewer returns 404

  • The page isn't published. Run pages.publish(pid).
  • public.page_by_id is a dead route. Use resolve(alias) and page(org, slug).

"Custom domain is not verified" from ssl.warmup

  • DNS hasn't verified yet. Add the CNAME from custom_domains.create and re-check with custom_domains.check(pid, verification_id).

SSO login fails with "ASN1 corrupted data"

  • samlCertificate isn't a valid base64 DER X.509 certificate. Paste the identity provider's signing certificate without PEM headers.

Related Articles

Need help?

Email support@alertops.com or contact your AlertOps representative.