# incident.io incident status changed notifications on your phone > Get a push notification when an incident.io incident changes status, with the update message: a High-priority one when it's accepted from triage and quiet ones when it's resolved, closed or declined. - Company: incident.io (https://www.justpush.io/recipes/incident-io) - Event: `public_incident.incident_status_updated_v2` (public_incident.incident_status_updated_v2) - Tags: Monitoring & observability, Issues & tickets, Uptime & downtime - Install: https://studio.justpush.io/recipes/incident-io/incident-status - Web page: https://www.justpush.io/recipes/incident-io/incident-status ## Setup 1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL. 2. In incident.io, go to Settings > Webhooks and add an endpoint. 3. Paste your webhook URL (shown in Studio after install) as the Endpoint URL and subscribe to public_incident.incident_status_updated_v2. 4. Save the endpoint. You can send a test event from its page. 5. In incident.io, go to **Settings β†’ Webhooks**, add an endpoint with the URL above, and subscribe to **public_incident.incident_status_updated_v2**. Webhooks need the Team plan or higher. 6. You get one push per status change, with the update message that was shared with it. An incident accepted from triage (or resumed from paused) pushes at High priority, or Highest when the severity is Critical (or named like SEV1/P1). Moves between active statuses, such as Fixing β†’ Monitoring, are Normal; resolved, closed, declined, merged and canceled are Low. Tutorial and retrospective incidents stay quiet, and so do test incidents unless you add `?test=1`: 7. ``` 8. your webhook URL (shown in Studio after install)?test=1 9. ``` 10. Severity changes and other edits only arrive as **public_incident.incident_updated_v2**, which fires on every edit and doesn't say what changed, so this recipe ignores it. incident.io has no status-change event for private incidents. 11. incident.io sends webhooks through Svix and signs every delivery (`webhook-id`, `webhook-timestamp`, `webhook-signature`), but JustPush doesn't check that signature yet, so keep your endpoint URL private. ## 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 // incident.io β†’ public_incident.incident_status_updated_v2 // Fires when an incident's status changes: accepted from triage, moved between active statuses, // paused, resolved (post-incident), closed, declined, merged or canceled. // Docs: https://docs.incident.io/api-reference/incident-status-v2/updated-public const EVENT = "public_incident.incident_status_updated_v2" // The documented example payload uses this incident ID, so a test send from Settings β†’ Webhooks // is recognisable (unconfirmed: that the test sends exactly this example). const EXAMPLE_ID = "01FDAG4SAP5TYPT98WGR2N7W91" // Each status category (the new status), how to show it and how loud to be. // "live" shows as Active in the app, "learning" as Post-incident. const CATEGORIES = { triage: { emoji: "πŸ”Ž", priority: 0 }, // Normal β€” back in triage, worth knowing live: { emoji: "πŸ”„", priority: 0 }, // Normal β€” moving along, e.g. Fixing β†’ Monitoring paused: { emoji: "⏸️", priority: 0 }, // Normal β€” on hold, worth knowing learning: { emoji: "βœ…", priority: -1 }, // Low β€” resolved, good news closed: { emoji: "βœ…", priority: -1 }, // Low β€” all done declined: { emoji: "🚫", priority: -1 }, // Low β€” triage decided it isn't an incident merged: { emoji: "πŸ”€", priority: -1 }, // Low β€” folded into another incident canceled: { emoji: "🚫", priority: -1 }, // Low β€” called off } // Look a key up without tripping over inherited names like "toString". function pick(table, key, fallback) { return 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 }] : [] } // A trimmed string, or null. function text(value) { return typeof value === "string" && value.trim() ? value.trim() : null } // Cut long update messages down to something that fits a notification. function clip(value, max) { const s = text(value) if (!s) return null const flat = s.replace(/\s+/g, " ") return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}…` : flat } // ?test=1 on the webhook URL also sends a quiet push for test incidents. function flag(request, name) { return ["1", "true", "yes"].includes(String(request.query?.[name] ?? "").toLowerCase()) } // Severity names are yours to configure; this matches incident.io's default "Critical" and // common schemes like "SEV1" or "P1". function critical(name) { return /critical|urgent|emergency|\bsev ?[01]\b|\bp[01]\b/.test(String(name ?? "").toLowerCase()) } function handleRequest(request) { const body = request.body && typeof request.body === "object" ? request.body : {} // incident.io sends no event header (only Svix's webhook-id/-timestamp/-signature), so // the payload's event_type decides. Without it, look for the event's key in the body. const type = text(body.event_type) ?? (body[EVENT] ? EVENT : null) const change = type === EVENT ? body[EVENT] : null const incident = change?.incident // Guard: only status changes, so created/updated, follow-up, alert and schedule events stay quiet. if (!change || typeof change !== "object" || !incident || typeof incident !== "object") { console.log(`Ignoring incident.io "${type ?? "unknown"}" event`) return null } // A test send from Settings β†’ Webhooks: confirm the connection quietly. if (incident.id === EXAMPLE_ID) { return { title: "πŸ”” incident.io connected", message: "Webhook endpoint is working", topic: "incident.io", priority: -1, } } // Tutorials and retrospective (back-filled) incidents aren't happening now; test incidents only with ?test=1. const mode = text(incident.mode) ?? "standard" if (mode === "tutorial" || mode === "retrospective" || (mode === "test" && !flag(request, "test"))) { console.log(`Skipping ${mode} incident`) return null } const next = change.new_status ?? incident.incident_status ?? {} const previous = change.previous_status ?? {} const category = text(next.category) const before = text(previous.category) // Same status on both sides isn't a change worth a push. if (text(next.id) && next.id === previous.id) { console.log("Status didn't change β€” skipping") return null } let { emoji, priority } = pick(CATEGORIES, category, { emoji: "πŸ”„", priority: 0 }) // Normal β€” a status we don't know yet // Accepted from triage (or resumed from paused): it's now a real, active incident. if (category === "live" && before && before !== "live") { emoji = "🚨" priority = critical(incident.severity?.name) ? 2 : 1 // Highest for critical incidents, otherwise High β€” people are needed now } if (mode === "test") { emoji = "πŸ§ͺ" priority = -1 // Low β€” a test incident you asked for with ?test=1 } const ref = text(incident.reference) const name = text(incident.name) ?? "An incident" const to = text(next.name) ?? "a new status" const from = text(previous.name) const move = from ? `${from} β†’ ${to}` : `Now ${to}` const update = clip(change.message, 180) const open = ["triage", "live", "paused"].includes(category) return { title: `${emoji} ${ref ? `${ref}: ` : ""}${name}`, message: [move, update].filter(Boolean).join(" β€” "), topic: "incident.io", priority, buttons: [...link(incident.permalink, "Open incident"), ...(open ? link(incident.slack_channel_url, "Slack channel") : [])], } } ``` ## incident.io public_incident.incident_status_updated_v2 webhook payload (sample) ```json { "event_type": "public_incident.incident_status_updated_v2", "public_incident.incident_status_updated_v2": { "message": "Rolled back the 10:40 deploy. Card payments are going through again; watching error rates.", "incident": { "id": "01J8ZK4Q7N3V2XH5M6T9RB1C0D", "mode": "standard", "name": "Checkout is returning 500s", "creator": { "user": { "id": "01J8ZK1A2B3C4D5E6F7G8H9J0K", "name": "Sam Lee", "role": "responder", "email": "sam@example.com" } }, "summary": "Card payments fail at the last checkout step for most customers since the 10:40 deploy.", "call_url": null, "severity": { "id": "01J8ZJ0000SEVERITYMAJOR000", "name": "Major", "rank": 2, "created_at": "2025-01-10T09:00:00Z", "updated_at": "2025-01-10T09:00:00Z", "description": "Issues causing **significant** customer impact." }, "team_ids": [], "permalink": "https://app.incident.io/incidents/482", "reference": "INC-482", "created_at": "2026-09-30T10:48:12.201345Z", "updated_at": "2026-09-30T11:32:40.118902Z", "visibility": "public", "incident_type": { "id": "01J8ZJ0000TYPEPRODUCTION00", "name": "Production Outage", "created_at": "2025-01-10T09:00:00Z", "is_default": false, "updated_at": "2025-01-10T09:00:00Z", "description": "Customer facing production outages", "create_in_triage": "optional", "private_incidents_only": false }, "slack_team_id": "T0000EXAMPLE", "incident_status": { "id": "01J8ZJ0000STATUSMONITORING", "name": "Monitoring", "rank": 3, "category": "live", "created_at": "2025-01-10T09:00:00Z", "updated_at": "2025-01-10T09:00:00Z", "description": "A fix is out and we are watching to make sure it holds." }, "last_activity_at": "2026-09-30T11:32:40.118902Z", "slack_channel_id": "C0000EXAMPLE", "slack_channel_url": "https://slack.com/app_redirect?team=T0000EXAMPLE&channel=C0000EXAMPLE", "slack_channel_name": "inc-482-checkout-is-returning-500s", "custom_field_entries": [], "incident_role_assignments": [] }, "new_status": { "id": "01J8ZJ0000STATUSMONITORING", "name": "Monitoring", "rank": 3, "category": "live", "created_at": "2025-01-10T09:00:00Z", "updated_at": "2025-01-10T09:00:00Z", "description": "A fix is out and we are watching to make sure it holds." }, "previous_status": { "id": "01J8ZJ0000STATUSFIXING0000", "name": "Fixing", "rank": 2, "category": "live", "created_at": "2025-01-10T09:00:00Z", "updated_at": "2025-01-10T09:00:00Z", "description": "We know the cause and are working on a fix." } } } ``` ## 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 incident.io 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.