1. Documentation
  2. How it works
  3. Public subscriptions
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

Public subscriptions

Double opt-in, idempotency, unsubscribing and the protections around visitor-supplied destinations.

A subscription endpoint is an unauthenticated write that causes outbound traffic. Everything on this page exists because of that one sentence.

Creating a subscription

bash
curl -X POST https://status.acme.com/api/heyo-status/subscribers \  -H "content-type: application/json" \  -d '{"type":"email","target":"person@example.com"}'
  1. 1

    Validate

    The method must be enabled in subscribers.methods. Email targets must look like an address; webhook targets must be HTTP(S), publicly routable and free of credentials.

  2. 2

    Deduplicate

    A subscription is identified by its destination. If one already exists and is confirmed — or is pending and younger than 15 minutes — the request returns the same public response without sending anything. Repeating the request is therefore harmless.

  3. 3

    Issue a token

    When confirmation is required, a 32-byte random token is generated with crypto.getRandomValues and stored with the pending row.

  4. 4

    Send

    Confirmation mail goes out through the first configured email channel. If sending fails, the pending row is deleted again so the address is not left stranded, and the API answers 503.

Responses never reveal whether a destination was already subscribed:

CaseResponse
Confirmation required202 { "status": "pending" }
Confirmation not required201 { "status": "subscribed" }
Already subscribed (either state)the same response as above

Anyone can submit somebody else's address, so the subscription id — which is an unsubscribe capability — and the confirmation state are never disclosed.

Confirming

text
GET /api/heyo-status/subscribers/confirm?token=<token>

The token is single use. A valid one returns the subscriber record; an invalid or already-consumed one returns 404. The response carries Cache-Control: no-store and Referrer-Policy: no-referrer so the token is not leaked to a cache or a third-party page.

Unsubscribing

Two routes, because a mail client cannot issue a DELETE:

text
GET  /api/heyo-status/subscribers/:id/unsubscribe   → a confirm pagePOST /api/heyo-status/subscribers/:id/unsubscribe   → performs the removalDELETE /api/heyo-status/subscribers/:id             → programmatic removal

The link in an email opens a small, self-contained HTML page with a confirm button. A link that unsubscribed on GET would fire whenever a mail scanner prefetched it, so the destructive step is always a POST. The page is noindex, no-store, and depends on none of the application's assets — it still works if the rest of the deployment is broken, which is exactly when somebody is trying to leave.

Concurrency

Two simultaneous requests for the same destination both pass the duplicate check. The database's unique index settles it: the losing insert is caught, the winner is looked up, and the loser returns the winner's public response rather than an error the visitor cannot act on.

Protecting against request forgery

Webhook targets go through isPubliclyRoutableHost() before they are ever stored. Rejected:

CategoryExamples
Loopback127.0.0.1, ::1, localhost, *.localhost
Private ranges10/8, 172.16/12, 192.168/16, fc00::/7
Link-local and metadata169.254.0.0/16 (incl. 169.254.169.254), fe80::/10
Carrier-grade NAT100.64.0.0/10
Multicast and reserved224.0.0.0/4, ff00::/8, 0.0.0.0
IPv4-mapped IPv6::ffff:127.0.0.1 in either notation
Reserved suffixes.local, .internal, .home.arpa, .localdomain
URL credentialshttps://user:pass@host
Literal filtering is not a sandbox

A hostname that resolves to a private address today can resolve somewhere else tomorrow; URL validation cannot see through DNS or prevent rebinding. If you enable visitor-supplied webhooks, restrict outbound network access at the platform level too.

What subscribers receive

Only incident and maintenance events. Monitor-level signals stay internal:

EventDelivered to subscribers
incident.*yes
maintenance.*yes
monitor.downno
monitor.recoveredno
ssl.expiringno

A subscription may filter by monitor. monitors: null or an empty list means everything; otherwise only events naming one of those monitors are sent, and global events — an incident with no affected monitors — always go to everyone.

Email bodies include a View status page link and an unsubscribe link, both built from siteUrl. Without siteUrl configured there is no absolute base to build them from, so neither link is included — which is one more reason to set it before enabling subscriptions. Webhook deliveries use the same JSON payload shape as configured webhook channels.

Failure isolation

  • A failure to list subscribers is swallowed: an incident update must not fail because the subscriber table is unavailable.
  • Each destination is delivered independently; one failure does not stop the rest.
  • Every delivery, successful or not, is written to the notification log.

Related

  • Enabling subscriptions
  • Subscriber endpoints
  • Security checklist
Notification delivery< PreviousStatusPage componentNext >

Powered by heyo

On this page

Creating a subscriptionConfirmingUnsubscribingConcurrencyProtecting against request forgeryWhat subscribers receiveFailure isolation