# 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. - Company: Statuspage (https://www.justpush.io/recipes/statuspage) - Event: `incident` (Webhook subscription) - Tags: Monitoring & observability, Issues & tickets, Uptime & downtime - Install: https://studio.justpush.io/recipes/statuspage/incident-update - Web page: https://www.justpush.io/recipes/statuspage/incident-update ## Setup 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 (shown in Studio after install) as the Webhook URL, and enter your email address (Statuspage emails it if deliveries keep failing). 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. ## Code Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing. ```js // Statuspage → incident created, updated or resolved (incidents and scheduled maintenance) // Fires when a status page you subscribed to by webhook posts or updates an incident or a // maintenance, or publishes a postmortem. Works for your own page and for any vendor's // Statuspage-hosted page that allows webhook subscribers. // Docs: https://support.atlassian.com/statuspage/docs/enable-webhook-notifications/ // Incident statuses (realtime and scheduled), their label, emoji and priority. // A null priority means "depends on the incident's impact" (see IMPACTS). const STATES = { investigating: { label: "Investigating", emoji: null, priority: null }, identified: { label: "Identified", emoji: null, priority: null }, monitoring: { label: "Monitoring", emoji: "👀", priority: 0 }, // Normal — a fix is out, keep an eye on it resolved: { label: "Resolved", emoji: "✅", priority: -1 }, // Low — good news scheduled: { label: "Maintenance scheduled", emoji: "🗓️", priority: -1 }, // Low — planned, nothing to do yet in_progress: { label: "Maintenance in progress", emoji: "🛠️", priority: 0 }, // Normal — expect planned disruption now verifying: { label: "Maintenance verifying", emoji: "🛠️", priority: -1 }, // Low — almost done completed: { label: "Maintenance completed", emoji: "✅", priority: -1 }, // Low — good news } // Impact of an open (investigating/identified) incident, lowest to highest. const IMPACTS = { none: { rank: 0, emoji: "⚠️", priority: 0 }, // Normal — worth knowing minor: { rank: 1, emoji: "🟡", priority: 0 }, // Normal — degraded, not down major: { rank: 2, emoji: "🟠", priority: 1 }, // High — a partial outage you may need to work around critical: { rank: 3, emoji: "🔴", priority: 2 }, // Highest — a major outage is happening right now maintenance: { rank: 0, emoji: "🛠️", priority: 0 }, // Normal — maintenance posted as an incident } const MAINTENANCE = ["scheduled", "in_progress", "verifying", "completed"] // Look a key up without tripping over inherited names such as "toString". function pick(table, key, fallback) { return typeof key === "string" && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback } // Only link to real http(s) URLs. function link(url, cta) { return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : [] } // Incident messages may contain HTML; keep plain, single-line, short text. function plain(value, max = 300) { if (typeof value !== "string") return null const s = value .replace(//gi, " ") .replace(/<[^>]*>/g, "") .replace(/ /g, " ") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, "\"") .replace(/'/g, "'") .replace(/\s+/g, " ") .trim() if (!s) return null return s.length > max ? `${s.slice(0, max - 1).trimEnd()}…` : s } // The newest entry in incident_updates (Statuspage lists them newest first, but sort anyway). function latestUpdate(updates) { if (!Array.isArray(updates)) return null const time = (u) => Date.parse(u?.display_at ?? u?.created_at ?? "") || 0 return updates.filter((u) => u && typeof u === "object").sort((a, b) => time(b) - time(a))[0] ?? null } // "3 Oct 2026", plus "14:00" when ?tz=Europe/Amsterdam is on the webhook URL. Without a // time zone the clock time is left out rather than shown in the server's zone. function when(timestamp, request) { const date = new Date(typeof timestamp === "string" ? timestamp : NaN) if (Number.isNaN(date.getTime())) return null const tz = typeof request.query?.tz === "string" ? request.query.tz.trim() : "" try { if (tz) { return date.toLocaleString("en-GB", { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", timeZone: tz }) } } catch { // not a time zone name this runtime knows; fall through to the date only } return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }) } // The status page itself, from the unsubscribe link's host (e.g. https://www.githubstatus.com). function pageUrl(body) { const match = typeof body.meta?.unsubscribe === "string" ? body.meta.unsubscribe.match(/^https?:\/\/[^/?#]+/) : null return match ? match[0] : null } function handleRequest(request) { const body = request.body && typeof request.body === "object" ? request.body : {} const incident = body.incident && typeof body.incident === "object" ? body.incident : null const state = pick(STATES, incident?.status, null) // Guard: only incident webhooks. Component webhooks (component_update) and junk stay quiet. if (!incident || !state) { console.log(`Ignoring Statuspage payload — not an incident update (${incident?.status ?? "no incident"})`) return null } const query = request.query || {} const impactKey = incident.impact_override ?? incident.impact const impact = pick(IMPACTS, impactKey, IMPACTS.none) const maintenance = MAINTENANCE.includes(incident.status) || impactKey === "maintenance" // ?maintenance=0 skips scheduled maintenance entirely. if (maintenance && ["0", "false", "no"].includes(String(query.maintenance ?? "").toLowerCase())) { console.log("Skipping maintenance (?maintenance=0)") return null } // ?min=major only pushes incidents of at least that impact (none, minor, major, critical). const min = pick(IMPACTS, String(query.min ?? "").toLowerCase(), null) if (!maintenance && min && impact.rank < min.rank) { console.log(`Skipping ${impactKey ?? "unknown"}-impact incident (?min=${query.min})`) return null } const page = typeof query.name === "string" && query.name.trim() ? `${query.name.trim()}: ` : "" const name = plain(incident.name, 120) ?? (maintenance ? "Scheduled maintenance" : "An incident") const buttons = [...link(incident.shortlink, "View incident"), ...link(pageUrl(body), "Status page")].slice(0, 1) // A resolved incident that now carries a published postmortem (field presence per the // Incident model; the postmortem webhook's exact payload isn't documented). if (incident.status === "resolved" && incident.postmortem_published_at && plain(incident.postmortem_body)) { return { title: `📝 ${page}Postmortem: ${name}`, message: plain(incident.postmortem_body), topic: "Statuspage", priority: -1, // Low — the incident is over; this is the write-up buttons, } } const update = latestUpdate(incident.incident_updates) let label = state.label if (incident.status === "scheduled") { const start = when(incident.scheduled_for, request) if (start) label += ` for ${start}` } return { title: `${state.emoji ?? impact.emoji} ${page}${name}`, message: [label, plain(update?.body)].filter(Boolean).join(" — "), topic: "Statuspage", priority: state.priority ?? impact.priority, buttons, } } ``` ## Statuspage incident webhook payload (sample) ```json { "meta": { "unsubscribe": "https://status.example.com/?unsubscribe=j0vqr9kl3513", "documentation": "https://doers.statuspage.io/customer-notifications/webhooks/" }, "page": { "id": "j2mfxwj97wnj", "status_indicator": "critical", "status_description": "Major System Outage" }, "incident": { "id": "lbkhbwn21v5q", "name": "Virginia Is Down", "impact": "critical", "status": "monitoring", "shortlink": "https://stspg.io/803310a12", "backfilled": false, "created_at": "2026-09-29T15:08:51-06:00", "updated_at": "2026-09-29T16:30:35-06:00", "resolved_at": null, "monitoring_at": "2026-09-29T16:07:53-06:00", "scheduled_for": null, "impact_override": null, "organization_id": "j2mfxwj97wnj", "postmortem_body": null, "scheduled_until": null, "incident_updates": [ { "id": "drfcwbnpxnr6", "body": "A fix has been implemented and we are monitoring the results.", "status": "monitoring", "created_at": "2026-09-29T16:07:53-06:00", "display_at": "2026-09-29T16:07:53-06:00", "updated_at": "2026-09-29T16:09:09-06:00", "incident_id": "lbkhbwn21v5q", "twitter_updated_at": null, "wants_twitter_update": false }, { "id": "2rryghr4qgrh", "body": "We are waiting for the cloud to come back online and will update when we have further information", "status": "identified", "created_at": "2026-09-29T15:18:51-06:00", "display_at": "2026-09-29T15:18:51-06:00", "updated_at": "2026-09-29T15:28:51-06:00", "incident_id": "lbkhbwn21v5q", "twitter_updated_at": null, "wants_twitter_update": false }, { "id": "qbbsfhy5s9kk", "body": "The cloud, located in Northern Virginia, has once again gone the way of the dodo.", "status": "investigating", "created_at": "2026-09-29T15:08:51-06:00", "display_at": "2026-09-29T15:08:51-06:00", "updated_at": "2026-09-29T15:28:51-06:00", "incident_id": "lbkhbwn21v5q", "twitter_updated_at": null, "wants_twitter_update": false } ], "postmortem_ignored": false, "scheduled_reminded_at": null, "scheduled_remind_prior": false, "postmortem_published_at": null, "scheduled_auto_transition": false, "postmortem_notified_twitter": false, "postmortem_body_last_updated_at": null, "postmortem_notified_subscribers": false } } ``` ## FAQ ### 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.