# incident.io incident declared notifications on your phone

> Get a push notification when an incident is declared in incident.io, with its severity, status and summary, and a Highest-priority one for critical incidents.

- Company: incident.io (https://www.justpush.io/recipes/incident-io)
- Event: `public_incident.incident_created_v2` (public_incident.incident_created_v2)
- Tags: Monitoring & observability, Issues & tickets, Uptime & downtime
- Install: https://studio.justpush.io/recipes/incident-io/incident-declared
- Web page: https://www.justpush.io/recipes/incident-io/incident-declared

## 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_created_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_created_v2**. Add **private_incident.incident_created_v2** too if you want a push for private incidents: incident.io only sends their ID, so that push just says a private incident was declared. Webhooks need the Team plan or higher.
6. Incidents opened in triage (for example from an alert) push at Normal priority; declared incidents at High, or Highest when the severity is Critical (or named like SEV1/P1). 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. Don't subscribe to **public_incident.incident_updated_v2** for this: it fires on every edit. Use the **Incident status changed** recipe for progress updates.
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_created_v2 (and private_incident.incident_created_v2)
// Fires when an incident is declared in incident.io, or opened in triage (for example from an alert).
// Docs: https://docs.incident.io/api-reference/incident-v2/created-public

// The created events this recipe handles. Private incidents only carry the incident's ID.
const EVENTS = {
    "public_incident.incident_created_v2": "public",
    "private_incident.incident_created_v2": "private",
}

// The documented example payloads use these IDs, so a test send from Settings → Webhooks
// is recognisable (unconfirmed: that the test sends exactly these examples).
const EXAMPLE_IDS = ["01FDAG4SAP5TYPT98WGR2N7W91", "abc123"]

// 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 summaries 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 defaults (Minor, Major,
// Critical) and common schemes like "SEV1" or "P1".
function severityPriority(name) {
    const s = String(name ?? "").toLowerCase()
    if (/critical|urgent|emergency|\bsev ?[01]\b|\bp[01]\b/.test(s)) return 2 // Highest — the worst kind of incident, happening now
    if (/minor|low|\bsev ?[3-9]\b|\bp[3-9]\b/.test(s)) return 0 // Normal — a small incident, worth knowing
    return 1 // High — an incident was declared and needs people soon
}

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) ?? Object.keys(EVENTS).find((k) => body[k] && typeof body[k] === "object")
    const kind = pick(EVENTS, type, null)
    const incident = kind ? body[type] : null

    // Guard: only incident-created events, so updates, follow-ups, alerts and schedule events stay quiet.
    if (!kind || !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 (EXAMPLE_IDS.includes(incident.id)) {
        return {
            title: "🔔 incident.io connected",
            message: "Webhook endpoint is working",
            topic: "incident.io",
            priority: -1,
        }
    }

    // Private incidents only include the ID, so there's nothing more to show.
    if (kind === "private") {
        return {
            title: "🔒 Private incident declared",
            message: "Open incident.io to see the details",
            topic: "incident.io",
            priority: 1, // High — a real incident, but we can't see its severity
        }
    }

    // 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
    }

    // Only incidents that are open: triage, active (live) or paused. Anything else was created already over.
    const category = text(incident.incident_status?.category) ?? "live"
    if (!["triage", "live", "paused"].includes(category)) {
        console.log(`Skipping incident created as "${category}"`)
        return null
    }

    const ref = text(incident.reference)
    const name = text(incident.name) ?? "New incident"
    const severity = text(incident.severity?.name)
    const status = text(incident.incident_status?.name)
    const details = [severity, status].filter(Boolean).join(" · ")
    const summary = clip(incident.summary, 180)

    let emoji = "🚨"
    let priority = severityPriority(severity)
    if (category === "triage") {
        emoji = "🔎"
        priority = Math.min(priority, 0) // Normal — still being triaged, may not be a real incident
    }
    if (mode === "test") {
        emoji = "🧪"
        priority = -1 // Low — a test incident you asked for with ?test=1
    }

    return {
        title: `${emoji} ${ref ? `${ref}: ` : ""}${name}`,
        message: [details, summary].filter(Boolean).join(" — ") || "Incident declared",
        topic: "incident.io",
        priority,
        buttons: [...link(incident.permalink, "Open incident"), ...link(incident.slack_channel_url, "Slack channel")],
    }
}
```

## incident.io public_incident.incident_created_v2 webhook payload (sample)

```json
{
  "event_type": "public_incident.incident_created_v2",
  "public_incident.incident_created_v2": {
    "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-30T10:48:12.201345Z",
    "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": "01J8ZJ0000STATUSINVESTIG00",
      "name": "Investigating",
      "rank": 1,
      "category": "live",
      "created_at": "2025-01-10T09:00:00Z",
      "updated_at": "2025-01-10T09:00:00Z",
      "description": "We're investigating the cause of the incident."
    },
    "last_activity_at": "2026-09-30T10:48:12.201345Z",
    "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": []
  }
}
```

## 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.
