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.

public_incident.incident_status_updated_v2Monitoring & observabilityIssues & ticketsUptime & downtime
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 incident.io

Add it as a webhook for public_incident.incident_status_updated_v2.

STEP 03

Get a push on your phone

With the text, sound and buttons from the recipe.

Setup

How to set up the incident.io public_incident.incident_status_updated_v2 webhook

  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 as the Endpoint URL and subscribe to public_incident.incident_status_updated_v2.https://••••••••/•••••••• your personal URL, shown after install
  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?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.
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// incident.io → public_incident.incident_status_updated_v2
2// Fires when an incident's status changes: accepted from triage, moved between active statuses,
3// paused, resolved (post-incident), closed, declined, merged or canceled.
4// Docs: https://docs.incident.io/api-reference/incident-status-v2/updated-public
5
6const EVENT = "public_incident.incident_status_updated_v2"
7
8// The documented example payload uses this incident ID, so a test send from Settings → Webhooks
9// is recognisable (unconfirmed: that the test sends exactly this example).
10const EXAMPLE_ID = "01FDAG4SAP5TYPT98WGR2N7W91"
11
12// Each status category (the new status), how to show it and how loud to be.
13// "live" shows as Active in the app, "learning" as Post-incident.
14const CATEGORIES = {
15 triage: { emoji: "🔎", priority: 0 }, // Normal — back in triage, worth knowing
16 live: { emoji: "🔄", priority: 0 }, // Normal — moving along, e.g. Fixing → Monitoring
17 paused: { emoji: "⏸️", priority: 0 }, // Normal — on hold, worth knowing
18 learning: { emoji: "✅", priority: -1 }, // Low — resolved, good news
19 closed: { emoji: "✅", priority: -1 }, // Low — all done
20 declined: { emoji: "🚫", priority: -1 }, // Low — triage decided it isn't an incident
21 merged: { emoji: "🔀", priority: -1 }, // Low — folded into another incident
22 canceled: { emoji: "🚫", priority: -1 }, // Low — called off
23}
24
25// Look a key up without tripping over inherited names like "toString".
26function pick(table, key, fallback) {
27 return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
28}
29
30// Only link to real http(s) URLs.
31function link(url, cta) {
32 return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
33}
34
35// A trimmed string, or null.
36function text(value) {
37 return typeof value === "string" && value.trim() ? value.trim() : null
38}
39
40// Cut long update messages down to something that fits a notification.
41function clip(value, max) {
42 const s = text(value)
43 if (!s) return null
44 const flat = s.replace(/\s+/g, " ")
45 return flat.length > max ? `${flat.slice(0, max - 1).trimEnd()}…` : flat
46}
47
48// ?test=1 on the webhook URL also sends a quiet push for test incidents.
49function flag(request, name) {
50 return ["1", "true", "yes"].includes(String(request.query?.[name] ?? "").toLowerCase())
51}
52
53// Severity names are yours to configure; this matches incident.io's default "Critical" and
54// common schemes like "SEV1" or "P1".
55function critical(name) {
56 return /critical|urgent|emergency|\bsev ?[01]\b|\bp[01]\b/.test(String(name ?? "").toLowerCase())
57}
58
59function handleRequest(request) {
60 const body = request.body && typeof request.body === "object" ? request.body : {}
61
62 // incident.io sends no event header (only Svix's webhook-id/-timestamp/-signature), so
63 // the payload's event_type decides. Without it, look for the event's key in the body.
64 const type = text(body.event_type) ?? (body[EVENT] ? EVENT : null)
65 const change = type === EVENT ? body[EVENT] : null
66 const incident = change?.incident
67
68 // Guard: only status changes, so created/updated, follow-up, alert and schedule events stay quiet.
69 if (!change || typeof change !== "object" || !incident || typeof incident !== "object") {
70 console.log(`Ignoring incident.io "${type ?? "unknown"}" event`)
71 return null
72 }
73
74 // A test send from Settings → Webhooks: confirm the connection quietly.
75 if (incident.id === EXAMPLE_ID) {
76 return {
77 title: "🔔 incident.io connected",
78 message: "Webhook endpoint is working",
79 topic: "incident.io",
80 priority: -1,
81 }
82 }
83
84 // Tutorials and retrospective (back-filled) incidents aren't happening now; test incidents only with ?test=1.
85 const mode = text(incident.mode) ?? "standard"
86 if (mode === "tutorial" || mode === "retrospective" || (mode === "test" && !flag(request, "test"))) {
87 console.log(`Skipping ${mode} incident`)
88 return null
89 }
90
91 const next = change.new_status ?? incident.incident_status ?? {}
92 const previous = change.previous_status ?? {}
93 const category = text(next.category)
94 const before = text(previous.category)
95
96 // Same status on both sides isn't a change worth a push.
97 if (text(next.id) && next.id === previous.id) {
98 console.log("Status didn't change — skipping")
99 return null
100 }
101
102 let { emoji, priority } = pick(CATEGORIES, category, { emoji: "🔄", priority: 0 }) // Normal — a status we don't know yet
103
104 // Accepted from triage (or resumed from paused): it's now a real, active incident.
105 if (category === "live" && before && before !== "live") {
106 emoji = "🚨"
107 priority = critical(incident.severity?.name) ? 2 : 1 // Highest for critical incidents, otherwise High — people are needed now
108 }
109 if (mode === "test") {
110 emoji = "🧪"
111 priority = -1 // Low — a test incident you asked for with ?test=1
112 }
113
114 const ref = text(incident.reference)
115 const name = text(incident.name) ?? "An incident"
116 const to = text(next.name) ?? "a new status"
117 const from = text(previous.name)
118 const move = from ? `${from} → ${to}` : `Now ${to}`
119 const update = clip(change.message, 180)
120 const open = ["triage", "live", "paused"].includes(category)
121
122 return {
123 title: `${emoji} ${ref ? `${ref}: ` : ""}${name}`,
124 message: [move, update].filter(Boolean).join(" — "),
125 topic: "incident.io",
126 priority,
127 buttons: [...link(incident.permalink, "Open incident"), ...(open ? link(incident.slack_channel_url, "Slack channel") : [])],
128 }
129}
Payload

The incident.io public_incident.incident_status_updated_v2 webhook

This is what incident.io sends to your URL for public_incident.incident_status_updated_v2. It is a sample, trimmed to the fields recipes use.

public_incident.incident_status_updated_v2 · sample.json
1{
2 "event_type": "public_incident.incident_status_updated_v2",
3 "public_incident.incident_status_updated_v2": {
4 "message": "Rolled back the 10:40 deploy. Card payments are going through again; watching error rates.",
5 "incident": {
6 "id": "01J8ZK4Q7N3V2XH5M6T9RB1C0D",
7 "mode": "standard",
8 "name": "Checkout is returning 500s",
9 "creator": {
10 "user": {
11 "id": "01J8ZK1A2B3C4D5E6F7G8H9J0K",
12 "name": "Sam Lee",
13 "role": "responder",
14 "email": "[email protected]"
15 }
16 },
17 "summary": "Card payments fail at the last checkout step for most customers since the 10:40 deploy.",
18 "call_url": null,
19 "severity": {
20 "id": "01J8ZJ0000SEVERITYMAJOR000",
21 "name": "Major",
22 "rank": 2,
23 "created_at": "2025-01-10T09:00:00Z",
24 "updated_at": "2025-01-10T09:00:00Z",
25 "description": "Issues causing **significant** customer impact."
26 },
27 "team_ids": [],
28 "permalink": "https://app.incident.io/incidents/482",
29 "reference": "INC-482",
30 "created_at": "2026-09-30T10:48:12.201345Z",
31 "updated_at": "2026-09-30T11:32:40.118902Z",
32 "visibility": "public",
33 "incident_type": {
34 "id": "01J8ZJ0000TYPEPRODUCTION00",
35 "name": "Production Outage",
36 "created_at": "2025-01-10T09:00:00Z",
37 "is_default": false,
38 "updated_at": "2025-01-10T09:00:00Z",
39 "description": "Customer facing production outages",
40 "create_in_triage": "optional",
41 "private_incidents_only": false
42 },
43 "slack_team_id": "T0000EXAMPLE",
44 "incident_status": {
45 "id": "01J8ZJ0000STATUSMONITORING",
46 "name": "Monitoring",
47 "rank": 3,
48 "category": "live",
49 "created_at": "2025-01-10T09:00:00Z",
50 "updated_at": "2025-01-10T09:00:00Z",
51 "description": "A fix is out and we are watching to make sure it holds."
52 },
53 "last_activity_at": "2026-09-30T11:32:40.118902Z",
54 "slack_channel_id": "C0000EXAMPLE",
55 "slack_channel_url": "https://slack.com/app_redirect?team=T0000EXAMPLE&channel=C0000EXAMPLE",
56 "slack_channel_name": "inc-482-checkout-is-returning-500s",
57 "custom_field_entries": [],
58 "incident_role_assignments": []
59 },
60 "new_status": {
61 "id": "01J8ZJ0000STATUSMONITORING",
62 "name": "Monitoring",
63 "rank": 3,
64 "category": "live",
65 "created_at": "2025-01-10T09:00:00Z",
66 "updated_at": "2025-01-10T09:00:00Z",
67 "description": "A fix is out and we are watching to make sure it holds."
68 },
69 "previous_status": {
70 "id": "01J8ZJ0000STATUSFIXING0000",
71 "name": "Fixing",
72 "rank": 2,
73 "category": "live",
74 "created_at": "2025-01-10T09:00:00Z",
75 "updated_at": "2025-01-10T09:00:00Z",
76 "description": "We know the cause and are working on a fix."
77 }
78 }
79}
FAQ

incident.io incident status changed 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 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.

Ready when you are

incident.io on your phone in two minutes.

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