Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249---category: Referencetitle: Subscriber Referencedescription: Technical specification for managing status page subscribers and their notifications.---
A subscriber in openstatus is an entity (typically a user or an integration) that opts to receive real-time notifications and updates regarding incidents and status changes on a specific status page. Subscribers play a crucial role in maintaining transparent communication during service disruptions.
**Key functions of subscribers:**
- Receive automated alerts when monitor statuses change or incidents are updated.- Stay informed about service health without actively monitoring the status page.- Choose preferred notification channels for receiving updates.
## Subscription process
Users typically subscribe to a status page's updates through a dedicated interface provided on the status page itself. The process involves:
1. **Entering an email address** โ public self-subscription is email-only.2. **Opt-in confirmation** โ a verification email is sent, and the subscription only activates once the recipient confirms it.3. **Scope selection (optional)** โ where enabled, choosing which page components to be notified about instead of the whole page.
Webhook and Slack subscriptions cannot be created from the public page. An operator adds those from the dashboard (see below).
## Notification types received
Subscribers receive notifications for key events affecting the monitored services linked to the status page:
- **Incident creation** โ when a new incident is detected and published.- **Incident updates** โ when status reports are published for an ongoing incident (e.g., status changes from `investigating` to `identified`, `monitoring`, or `resolved`).- **Scheduled maintenance** โ when a maintenance window is created on the page, subscribers can be notified that it has been scheduled (see the [maintenance reference](/docs/reference/maintenance)).
Raw monitor status changes are *not* sent to status page subscribers. Those go to your configured [notification channels](/docs/reference/notification). Subscribers only receive status report updates and scheduled maintenance.
## Subscriber management
Status page administrators can manage their subscriber lists, including:
- **Viewing subscribers** โ accessing a list of all active subscribers for a status page.- **Adding/removing subscribers** โ manually adding or removing subscribers.- **Communication** โ sending ad-hoc notifications to the subscriber list (if supported by the platform).
## Adding subscribers from the dashboard
Beyond public self-subscription, administrators can add subscribers directly from the dashboard. This is useful for onboarding partners, internal teams, or automation that should receive updates without going through the public opt-in flow. Each manually added subscriber uses one of three channels:
- **Email** โ delivers updates to a contact address. By adding an email here you confirm the contact has consented to receive status updates; no confirmation email is sent.- **Webhook** โ POSTs each update to a URL. Slack and Discord URLs receive channel-native messages; any other URL receives a generic JSON payload.- **Slack** โ posts each update straight into a Slack channel through the openstatus Slack app. The bot token is resolved from the workspace integration at send time and is never stored on the subscriber.
For every channel, you can optionally:
- **Set a display label** โ shown in place of the raw destination in the dashboard (e.g. `Supabase #incidents`).- **Scope to page components** โ leave empty to notify for the entire page, or select specific components to only notify on matching reports.

## Webhook subscribers
A webhook subscriber receives a POST request for every status report and scheduled maintenance update. openstatus inspects the destination URL and emits a payload tailored to it.
### Slack
A URL starting with `https://hooks.slack.com/services/` is treated as a Slack incoming webhook. The payload uses Slack [Block Kit](https://api.slack.com/block-kit) attachments โ a colored header (red/yellow/green/blue by status), status and page fields, the update message, affected components, and footer links to view, manage, and unsubscribe.
### Discord
A URL starting with `https://discord.com/api/webhooks/` is treated as a Discord webhook. The payload uses a Discord embed with a status-colored sidebar, a title linking to the event, status and page fields, the update message, affected components, and manage/unsubscribe links.
### Generic
Any other URL receives a versioned, channel-agnostic JSON payload. Use this to forward updates into your own systems. The body is shaped as:
```json{ "version": "1", "type": "status_report", "data": { "status_report": { "id": 123, "title": "Database connectivity issues", "url": "https://acme.openstatus.dev/events/report/123", "update": { "id": 456, "status": "investigating", "message": "We are investigating elevated error rates.", "occurred_at": "2026-06-26T10:00:00.000Z" }, "page": { "id": 1, "name": "Acme Status", "slug": "acme", "url": "https://acme.openstatus.dev" }, "components": [ { "id": 7, "name": "API", "impact": "major_outage" } ] } }, "subscription": { "manage_url": "https://acme.openstatus.dev/manage/<token>", "unsubscribe_url": "https://acme.openstatus.dev/unsubscribe/<token>" }}```
`page.url` is the canonical status page origin, while `status_report.url` (and `maintenance.url` below) is the deep link to that specific event. Each entry in `components` carries an `impact`. The schema permits `null` for forward compatibility, but the current sender always emits a concrete impact, falling back to `operational` for reports created before component impacts existed.
Scheduled maintenance is delivered with `type: "maintenance"` and a `data.maintenance` object carrying `starts_at` / `ends_at` in place of the `update` block. The `version` field is bumped on any breaking change, so pin to it when consuming the payload.
The **Send test** action emits the same envelope with `type: "test"` and a `data.test` object โ use the `type` discriminator to ignore it (it carries no real event):
```json{ "version": "1", "type": "test", "data": { "test": { "message": "Your openstatus webhook is configured correctly.", "timestamp": "2026-06-26T10:38:00.132Z" } }}```
You can attach **custom request headers** to every webhook request โ for example, a shared secret or an authorization token your receiver verifies.
#### Timeout and retries
Each webhook request is given **5 seconds** to complete. If your endpoint has not responded within that window, the request is aborted and treated as a failed attempt.
Failed attempts are retried with **jittered exponential backoff** (โ200 ms base delay), up to **3 retries** (4 attempts total). Only transient failures are retried:
- **Network errors and timeouts** (no HTTP response).- **`5xx` responses** โ a server-side error on your end that may clear.- **`429 Too Many Requests`** โ your endpoint asked us to back off.
Any other `4xx` response (e.g. `400`, `401`, `404`) is treated as a permanent client error and is **not** retried, since replaying the same request would fail again. Make your receiver **idempotent** โ a retried delivery is identical to the original, so deduplicate on the update `id` if you act on each request.
#### Example: post updates to X / Bluesky
[`statuspage-socials-notifier`](https://github.com/openstatusHQ/statuspage-socials-notifier) is a reference receiver that consumes the generic payload and cross-posts each update to your X (Twitter) and Bluesky accounts. Point a generic webhook subscriber at its endpoint to mirror your status page on social media โ it doubles as a worked example of validating and handling the payload.
#### Validating the payload
The payload shape is defined in [`packages/subscriptions/src/payload.ts`](https://github.com/openstatusHQ/openstatus/blob/main/packages/subscriptions/src/payload.ts). Copy this zod schema to validate incoming requests in your own receiver:
```tsimport { z } from "zod";
export const WEBHOOK_PAYLOAD_VERSION = "1" as const;
const impactSchema = z.enum([ "operational", "degraded_performance", "partial_outage", "major_outage",]);
const statusSchema = z.enum([ "investigating", "identified", "monitoring", "resolved",]);
const componentSchema = z.object({ id: z.number().int(), name: z.string(), impact: impactSchema.nullish(),});
const pageSchema = z.object({ id: z.number().int(), name: z.string(), slug: z.string(), url: z.url(),});
const subscriptionSchema = z.object({ manage_url: z.string().nullish(), unsubscribe_url: z.string().nullish(),});
const statusReportWebhookSchema = z.object({ version: z.literal(WEBHOOK_PAYLOAD_VERSION), type: z.literal("status_report"), data: z.object({ status_report: z.object({ id: z.number().int(), title: z.string(), url: z.url(), update: z.object({ id: z.number().int(), status: statusSchema, message: z.string(), occurred_at: z.string(), }), page: pageSchema, components: z.array(componentSchema), }), }), subscription: subscriptionSchema,});
const maintenanceWebhookSchema = z.object({ version: z.literal(WEBHOOK_PAYLOAD_VERSION), type: z.literal("maintenance"), data: z.object({ maintenance: z.object({ id: z.number().int(), title: z.string(), url: z.url(), message: z.string(), starts_at: z.string().optional(), ends_at: z.string().optional(), page: pageSchema, components: z.array(componentSchema), }), }), subscription: subscriptionSchema,});
const testWebhookSchema = z.object({ version: z.literal(WEBHOOK_PAYLOAD_VERSION), type: z.literal("test"), data: z.object({ test: z.object({ message: z.string(), timestamp: z.string(), }), }),});
export const webhookPayloadSchema = z.discriminatedUnion("type", [ statusReportWebhookSchema, maintenanceWebhookSchema, testWebhookSchema,]);
export type WebhookPayload = z.infer<typeof webhookPayloadSchema>;```
## Related resources
- **[Status page reference](/docs/reference/status-page)** โ detailed information on managing and configuring status pages.- **[Notification channels reference](/docs/reference/notification)** โ technical specifications for the various notification delivery methods.- **[Incident reference](/docs/reference/incident)** โ information about incident creation and management.