Statuspage incident update notifications on your phone.

Get a push notification when a status page you follow (yours or a vendor's) opens, updates or resolves an incident or scheduled maintenance, loudest for critical outages.

How it works

Three steps to your first push

STEP 01

Install the recipe

One click in Studio gives you a personal webhook URL.

STEP 02

Paste your URL into Statuspage

Add it as a webhook for Webhook subscription.

STEP 03

Get a push on your phone

With the text, sound and buttons from the recipe.

Setup

How to set up the Statuspage webhook subscription webhook

  1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL.
  2. Open the status page you want to follow (your own, or a vendor's Statuspage-hosted page) and click Subscribe to updates.
  3. Pick the webhook tab, paste your webhook URL as the Webhook URL, and enter your email address (Statuspage emails it if deliveries keep failing).https://••••••••/•••••••• your personal URL, shown after install
  4. No webhook tab? The page doesn't allow webhook subscribers. On your own page, turn on Webhook under Subscribers > Options > Settings first.
  5. Statuspage webhooks are a kind of **subscriber**, so this works for any Statuspage-hosted status page that allows webhook subscriptions: your own, or a vendor you depend on (for example `githubstatus.com`).
  6. Open the status page and click **Subscribe to updates**, then the **webhook** tab. If there's no webhook tab, the page owner hasn't enabled webhook subscribers. On your own page, turn it on under **Subscribers → Options → Settings → Webhook** and save.
  7. Paste the URL above as the **Webhook URL** and enter your **email address**. Statuspage uses it to email you if your endpoint keeps failing. If a confirmation email arrives, confirm it.
  8. Subscribe. If the page lets you pick components, choose the ones you care about.
  9. One subscription sends **both** incident and component webhooks to the same URL. This recipe only pushes for incidents (and scheduled maintenance, and postmortems); the Component status recipe handles the rest. Install both on the same integration if you want both.
  10. Optional query parameters on the URL above:
  11. `?name=GitHub` puts the page's name in the title (the webhook itself doesn't include it).
  12. `?min=major` only pushes incidents of at least that impact (`minor`, `major` or `critical`).
  13. `?maintenance=0` skips scheduled maintenance.
  14. `?tz=Europe/Amsterdam` adds the start time to "Maintenance scheduled" pushes (otherwise only the date, in UTC).
  15. Combine them with `&`, e.g. `?name=GitHub&min=major`.
  16. Statuspage sends a webhook for every incident update the page owner chooses to notify subscribers about, so one incident usually means several pushes. Requests aren't signed, so keep your endpoint URL private. After 10 failed deliveries in about an hour the subscription is quarantined and you get an email; re-subscribe to reactivate it.
The code

What runs when the webhook arrives

Studio calls handleRequest(request) with the incoming webhook and sends the message it returns. It's yours after install; change anything.

transform.js
1// Statuspage → incident created, updated or resolved (incidents and scheduled maintenance)
2// Fires when a status page you subscribed to by webhook posts or updates an incident or a
3// maintenance, or publishes a postmortem. Works for your own page and for any vendor's
4// Statuspage-hosted page that allows webhook subscribers.
5// Docs: https://support.atlassian.com/statuspage/docs/enable-webhook-notifications/
6
7// Incident statuses (realtime and scheduled), their label, emoji and priority.
8// A null priority means "depends on the incident's impact" (see IMPACTS).
9const STATES = {
10 investigating: { label: "Investigating", emoji: null, priority: null },
11 identified: { label: "Identified", emoji: null, priority: null },
12 monitoring: { label: "Monitoring", emoji: "👀", priority: 0 }, // Normal — a fix is out, keep an eye on it
13 resolved: { label: "Resolved", emoji: "✅", priority: -1 }, // Low — good news
14 scheduled: { label: "Maintenance scheduled", emoji: "🗓️", priority: -1 }, // Low — planned, nothing to do yet
15 in_progress: { label: "Maintenance in progress", emoji: "🛠️", priority: 0 }, // Normal — expect planned disruption now
16 verifying: { label: "Maintenance verifying", emoji: "🛠️", priority: -1 }, // Low — almost done
17 completed: { label: "Maintenance completed", emoji: "✅", priority: -1 }, // Low — good news
18}
19
20// Impact of an open (investigating/identified) incident, lowest to highest.
21const IMPACTS = {
22 none: { rank: 0, emoji: "⚠️", priority: 0 }, // Normal — worth knowing
23 minor: { rank: 1, emoji: "🟡", priority: 0 }, // Normal — degraded, not down
24 major: { rank: 2, emoji: "🟠", priority: 1 }, // High — a partial outage you may need to work around
25 critical: { rank: 3, emoji: "🔴", priority: 2 }, // Highest — a major outage is happening right now
26 maintenance: { rank: 0, emoji: "🛠️", priority: 0 }, // Normal — maintenance posted as an incident
27}
28
29const MAINTENANCE = ["scheduled", "in_progress", "verifying", "completed"]
30
31// Look a key up without tripping over inherited names such as "toString".
32function pick(table, key, fallback) {
33 return typeof key === "string" && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
34}
35
36// Only link to real http(s) URLs.
37function link(url, cta) {
38 return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
39}
40
41// Incident messages may contain HTML; keep plain, single-line, short text.
42function plain(value, max = 300) {
43 if (typeof value !== "string") return null
44 const s = value
45 .replace(/<br\s*\/?>/gi, " ")
46 .replace(/<[^>]*>/g, "")
47 .replace(/&nbsp;/g, " ")
48 .replace(/&amp;/g, "&")
49 .replace(/&lt;/g, "<")
50 .replace(/&gt;/g, ">")
51 .replace(/&quot;/g, "\"")
52 .replace(/&#39;/g, "'")
53 .replace(/\s+/g, " ")
54 .trim()
55 if (!s) return null
56 return s.length > max ? `${s.slice(0, max - 1).trimEnd()}…` : s
57}
58
59// The newest entry in incident_updates (Statuspage lists them newest first, but sort anyway).
60function latestUpdate(updates) {
61 if (!Array.isArray(updates)) return null
62 const time = (u) => Date.parse(u?.display_at ?? u?.created_at ?? "") || 0
63 return updates.filter((u) => u && typeof u === "object").sort((a, b) => time(b) - time(a))[0] ?? null
64}
65
66// "3 Oct 2026", plus "14:00" when ?tz=Europe/Amsterdam is on the webhook URL. Without a
67// time zone the clock time is left out rather than shown in the server's zone.
68function when(timestamp, request) {
69 const date = new Date(typeof timestamp === "string" ? timestamp : NaN)
70 if (Number.isNaN(date.getTime())) return null
71 const tz = typeof request.query?.tz === "string" ? request.query.tz.trim() : ""
72 try {
73 if (tz) {
74 return date.toLocaleString("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", timeZone: tz })
75 }
76 } catch {
77 // not a time zone name this runtime knows; fall through to the date only
78 }
79 return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" })
80}
81
82// The status page itself, from the unsubscribe link's host (e.g. https://www.githubstatus.com).
83function pageUrl(body) {
84 const match = typeof body.meta?.unsubscribe === "string" ? body.meta.unsubscribe.match(/^https?:\/\/[^/?#]+/) : null
85 return match ? match[0] : null
86}
87
88function handleRequest(request) {
89 const body = request.body && typeof request.body === "object" ? request.body : {}
90 const incident = body.incident && typeof body.incident === "object" ? body.incident : null
91 const state = pick(STATES, incident?.status, null)
92
93 // Guard: only incident webhooks. Component webhooks (component_update) and junk stay quiet.
94 if (!incident || !state) {
95 console.log(`Ignoring Statuspage payload — not an incident update (${incident?.status ?? "no incident"})`)
96 return null
97 }
98
99 const query = request.query || {}
100 const impactKey = incident.impact_override ?? incident.impact
101 const impact = pick(IMPACTS, impactKey, IMPACTS.none)
102 const maintenance = MAINTENANCE.includes(incident.status) || impactKey === "maintenance"
103
104 // ?maintenance=0 skips scheduled maintenance entirely.
105 if (maintenance && ["0", "false", "no"].includes(String(query.maintenance ?? "").toLowerCase())) {
106 console.log("Skipping maintenance (?maintenance=0)")
107 return null
108 }
109
110 // ?min=major only pushes incidents of at least that impact (none, minor, major, critical).
111 const min = pick(IMPACTS, String(query.min ?? "").toLowerCase(), null)
112 if (!maintenance && min && impact.rank < min.rank) {
113 console.log(`Skipping ${impactKey ?? "unknown"}-impact incident (?min=${query.min})`)
114 return null
115 }
116
117 const page = typeof query.name === "string" && query.name.trim() ? `${query.name.trim()}: ` : ""
118 const name = plain(incident.name, 120) ?? (maintenance ? "Scheduled maintenance" : "An incident")
119 const buttons = [...link(incident.shortlink, "View incident"), ...link(pageUrl(body), "Status page")].slice(0, 1)
120
121 // A resolved incident that now carries a published postmortem (field presence per the
122 // Incident model; the postmortem webhook's exact payload isn't documented).
123 if (incident.status === "resolved" && incident.postmortem_published_at && plain(incident.postmortem_body)) {
124 return {
125 title: `📝 ${page}Postmortem: ${name}`,
126 message: plain(incident.postmortem_body),
127 topic: "Statuspage",
128 priority: -1, // Low — the incident is over; this is the write-up
129 buttons,
130 }
131 }
132
133 const update = latestUpdate(incident.incident_updates)
134 let label = state.label
135 if (incident.status === "scheduled") {
136 const start = when(incident.scheduled_for, request)
137 if (start) label += ` for ${start}`
138 }
139
140 return {
141 title: `${state.emoji ?? impact.emoji} ${page}${name}`,
142 message: [label, plain(update?.body)].filter(Boolean).join(" — "),
143 topic: "Statuspage",
144 priority: state.priority ?? impact.priority,
145 buttons,
146 }
147}
Payload

The Statuspage incident webhook

This is what Statuspage sends to your URL for webhook subscription. It is a sample, trimmed to the fields recipes use.

incident · sample.json
1{
2 "meta": {
3 "unsubscribe": "https://status.example.com/?unsubscribe=j0vqr9kl3513",
4 "documentation": "https://doers.statuspage.io/customer-notifications/webhooks/"
5 },
6 "page": {
7 "id": "j2mfxwj97wnj",
8 "status_indicator": "critical",
9 "status_description": "Major System Outage"
10 },
11 "incident": {
12 "id": "lbkhbwn21v5q",
13 "name": "Virginia Is Down",
14 "impact": "critical",
15 "status": "monitoring",
16 "shortlink": "https://stspg.io/803310a12",
17 "backfilled": false,
18 "created_at": "2026-09-29T15:08:51-06:00",
19 "updated_at": "2026-09-29T16:30:35-06:00",
20 "resolved_at": null,
21 "monitoring_at": "2026-09-29T16:07:53-06:00",
22 "scheduled_for": null,
23 "impact_override": null,
24 "organization_id": "j2mfxwj97wnj",
25 "postmortem_body": null,
26 "scheduled_until": null,
27 "incident_updates": [
28 {
29 "id": "drfcwbnpxnr6",
30 "body": "A fix has been implemented and we are monitoring the results.",
31 "status": "monitoring",
32 "created_at": "2026-09-29T16:07:53-06:00",
33 "display_at": "2026-09-29T16:07:53-06:00",
34 "updated_at": "2026-09-29T16:09:09-06:00",
35 "incident_id": "lbkhbwn21v5q",
36 "twitter_updated_at": null,
37 "wants_twitter_update": false
38 },
39 {
40 "id": "2rryghr4qgrh",
41 "body": "We are waiting for the cloud to come back online and will update when we have further information",
42 "status": "identified",
43 "created_at": "2026-09-29T15:18:51-06:00",
44 "display_at": "2026-09-29T15:18:51-06:00",
45 "updated_at": "2026-09-29T15:28:51-06:00",
46 "incident_id": "lbkhbwn21v5q",
47 "twitter_updated_at": null,
48 "wants_twitter_update": false
49 },
50 {
51 "id": "qbbsfhy5s9kk",
52 "body": "The cloud, located in Northern Virginia, has once again gone the way of the dodo.",
53 "status": "investigating",
54 "created_at": "2026-09-29T15:08:51-06:00",
55 "display_at": "2026-09-29T15:08:51-06:00",
56 "updated_at": "2026-09-29T15:28:51-06:00",
57 "incident_id": "lbkhbwn21v5q",
58 "twitter_updated_at": null,
59 "wants_twitter_update": false
60 }
61 ],
62 "postmortem_ignored": false,
63 "scheduled_reminded_at": null,
64 "scheduled_remind_prior": false,
65 "postmortem_published_at": null,
66 "scheduled_auto_transition": false,
67 "postmortem_notified_twitter": false,
68 "postmortem_body_last_updated_at": null,
69 "postmortem_notified_subscribers": false
70 }
71}
FAQ

Statuspage incident update notifications: questions

Does this work on iPhone and Android?

Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification.

Do I need to write code?

No. Install the recipe in Studio, paste your webhook URL into Statuspage and you are done. The code is there if you want to change the text, the sound or the buttons.

Can I change what the notification says?

Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save.

What does it cost?

JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.

Ready when you are

Statuspage on your phone in two minutes.

Install the recipe, paste one URL, done. Free for 30 days, no credit card required.