1. Documentation
  2. How it works
  3. Notification delivery
ReadmenpmGitHub
  • Introduction
  • Quickstart
  • Configuration file
  • Monitors
  • Checks and scheduling
  • Notifications
  • Incidents and maintenance
  • Subscribers
  • Metrics and badges
  • Identity and appearance
  • Telemetry
  • The check run
  • Uptime and history
  • Automatic incidents
  • Notification delivery
  • Public subscriptions
  • StatusPage component
  • Themes
  • UI primitives
  • Colour mode
  • React Router
  • Next.js
  • Astro
  • SQLite
  • Cloudflare D1
  • PostgreSQL
  • Schema and migrations
  • Cloudflare Workers
  • Vercel
  • Self-hosting
  • API overview
  • Status endpoints
  • Incident and maintenance endpoints
  • Subscriber endpoints
  • Badges, feeds and metrics
  • Admin endpoints
  • CLI commands
  • Package exports
  • Security
  • Troubleshooting

Notification delivery

How events are enriched, filtered, delivered and logged.

Events are produced in two places — the check run and the admin API — and delivered through one dispatcher. The dispatcher's contract is simple: it must never let a channel failure break monitoring, and it must record what it tried.

Producing monitor events

collectMonitorNotificationEvents() runs before a batch of results is written, so the failure counts it reads are the ones that precede the batch.

For each monitor not covered by an in-progress maintenance window:

ConditionEvent
Failing, and priorFailures + 1 === failureThresholdmonitor.down or ssl.expiring
Succeeding, and priorFailures >= failureThresholdmonitor.recovered
Anything elseno event

The strict equality is deliberate: a monitor that stays down does not re-page every five minutes, and a monitor that was never considered down does not announce a recovery.

ssl.expiring is chosen from the expiring flag the SSL checker sets, never by matching the message text — rewording a check must not silently turn an expiry warning into an outage alert.

Enriching

Every event passes through enrichStatusEvent(), which attaches the page identity and, for monitor events, configuration detail:

ts
{  type: "monitor.down",  occurredAt: Date,  monitor: {    name: "API",    type: "http",    group: "API",                       // from the monitor configuration    target: "https://api.acme.com/health", // url, host:port, host, or components    status: false,    message: "HTTP 503, expected 200",    latencyMs: 142,    consecutiveFailures: 3,  },  page: { title: "Acme Status", siteUrl: "https://status.acme.com" },}

target is derived per type: the URL for http, host:port for ssl and tcp, the host for dns, and the component list for composite.

Filtering and delivery

ts
for (const event of events) {  for (const channel of config.notifications.channels) {    if (channel.events && !channel.events.includes(event.type)) continue;    await channel.send(event);  }}

Wildcard selectors were already expanded when the channel was built, so the dispatcher only compares exact names. Channels are tried in configuration order, and each delivery is awaited so ordering is predictable.

Isolation

Every send() is wrapped. A thrown error becomes a failed delivery record with the message; it is never rethrown. This is what lets a status page keep monitoring while a Slack webhook has been revoked.

Writing the log is best-effort for the same reason — a logging failure is swallowed rather than escalated.

The delivery log

Each attempt becomes a row:

ts
interface NotificationLog {  id: string;  channelId: string;  channelType: "slack" | "discord" | "webhook" | "email";  eventType: StatusEventType;  status: "sent" | "failed";  attempts: number;  error: string | null;  monitorName: string | null;  incidentId: string | null;  maintenanceId: string | null;  createdAt: Date;}

Note what is not stored: no payload, no webhook URL, no credentials, no recipient. The log is for debugging delivery, not for replaying it.

bash
curl -H "authorization: Bearer $HEYO_STATUS_API_TOKEN" \  "https://status.acme.com/api/heyo-status/admin/notifications/log?status=failed&limit=20"

Filters: channelId, eventType (comma-separated), status, limit, offset.

Retries

Only the generic webhook channel retries, with exponential backoff: retries total attempts (default 3) starting at retryDelayMs (default 250 ms), doubling each time. The attempt count reaches the log either from the successful result or from the thrown delivery error.

Slack, Discord and email report their first failure. Wrap them in a custom channel if you need provider-specific retry semantics.

Public subscribers

After configured channels, incident and maintenance events are also delivered to confirmed public subscribers, if subscriptions are enabled. Monitor-level events never are. See Public subscriptions.

Dispatching your own event

ts
import { dispatchNotifications } from "@heyo-sh/heyo-status";await dispatchNotifications(statusPage, {  type: "incident.updated",  occurredAt: new Date(),  incident,});

The return value is one NotificationDispatchResult per channel per event, with channelId, eventType, status, attempts and error — useful in tests, where it is the only way to assert delivery without mocking the database.

Related

  • Configuring channels
  • The check run
  • Admin endpoints
Automatic incidents< PreviousPublic subscriptionsNext >

Powered by heyo

On this page

Producing monitor eventsEnrichingFiltering and deliveryIsolationThe delivery logRetriesPublic subscribersDispatching your own event