# Heroku dyno crashed notifications on your phone

> Get a High-priority push notification when a dyno of your Heroku app crashes, with its exit status.

- Company: Heroku (https://www.justpush.io/recipes/heroku)
- Event: `dyno` (dyno)
- Tags: Hosting & deployment, Errors & crashes, Uptime & downtime
- Install: https://studio.justpush.io/recipes/heroku/dyno-crashed
- Web page: https://www.justpush.io/recipes/heroku/dyno-crashed

## 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 the Heroku Dashboard, open your app and choose More > View webhooks, then create a webhook. Or use the CLI command in the setup notes below.
3. Paste your webhook URL (shown in Studio after install) as the URL and subscribe to the dyno event type.
4. Save. Each app needs its own webhook.
5. Subscribe the webhook to the **`dyno`** entity (not `api:dyno`, which only reports new dynos). With the Heroku CLI:
6. ```
7. heroku webhooks:add -i dyno -l notify -u your webhook URL (shown in Studio after install) -a your-app
8. ```
9. Dyno events are in public beta at Heroku. The recipe pushes only when a dyno reaches the `crashed` state; starting, up, stopping and down stay quiet. One-off dynos (`heroku run`) often report `crashed` even when the command succeeded, so for those the recipe only pushes on a non-zero exit status.
10. Heroku restarts crashed dynos automatically, so a crash loop sends a push per crash.
11. Heroku signs every delivery (`Heroku-Webhook-Hmac-SHA256`), 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
// Heroku → dyno crashed
// Fires when a Heroku app webhook subscribed to the "dyno" entity reports a dyno in the
// "crashed" state. Other dyno states (starting, up, stopping, down) stay quiet.
// Docs: https://devcenter.heroku.com/articles/webhook-dyno-events

// Only link to real http(s) URLs.
function link(url, cta) {
    return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
}

function handleRequest(request) {
    const body = request.body && typeof request.body === "object" ? request.body : {}
    const data = body.data && typeof body.data === "object" ? body.data : {}
    const include = body.webhook_metadata?.event?.include

    // Guard: only dyno lifecycle events. (api:dyno "create" events have no state and are skipped below.)
    if (body.resource !== "dyno" && include !== "dyno") {
        console.log(`Ignoring Heroku "${include ?? body.resource ?? "unknown"}" event`)
        return null
    }
    if (data.state !== "crashed") {
        console.log(`Skipping dyno state "${data.state ?? "none"}"`)
        return null
    }

    // One-off dynos (heroku run) often end as "crashed" even when the command succeeded,
    // so for those only a non-zero exit status counts.
    const oneOff = typeof data.management === "string" && data.management.startsWith("run:")
    const exit = Number.isInteger(data.exit_status) ? data.exit_status : null
    if (oneOff && !exit) {
        console.log("Skipping a one-off dyno that exited cleanly")
        return null
    }

    const app = typeof data.app?.name === "string" && data.app.name ? data.app.name : null
    const type = typeof data.type === "string" && data.type ? data.type : null
    const dyno = typeof data.name === "string" && data.name ? data.name : "The dyno"
    const release = Number.isInteger(data.release?.version) ? `release v${data.release.version}` : null
    const exited = exit !== null ? `${dyno} exited with status ${exit}` : `${dyno} crashed`

    return {
        title: oneOff
            ? `⚠️ ${app ?? "Your app"} one-off dyno failed`
            : `💥 ${app ?? "Your app"} ${type ? `${type} ` : ""}dyno crashed`,
        message: [exited, release].filter(Boolean).join(" · "),
        topic: "Heroku",
        // A crashed formation dyno needs you soon (Heroku restarts it, but it may keep crashing);
        // a failed one-off command is worth knowing.
        priority: oneOff ? 0 : 1,
        buttons: app ? link(`https://dashboard.heroku.com/apps/${encodeURIComponent(app)}/logs`, "View logs") : [],
    }
}
```

## Heroku dyno webhook payload (sample)

```json
{
  "id": "615fbe2a-5add-4658-93e6-a1b905d1c9a1",
  "data": {
    "id": "c86bee75-8523-46a7-b4d0-08663c975de8",
    "app": {
      "id": "7921ed93-b8b3-4437-9d13-509fcf32595c",
      "name": "acme-api"
    },
    "name": "web.76bcb7fb4-552lh",
    "size": "dyno-1c-0.5gb",
    "type": "web",
    "state": "crashed",
    "command": "/cnb/process/web",
    "release": {
      "id": "1c462e25-e6b9-47db-b7d8-160e6a714677",
      "version": 42
    },
    "management": "formation",
    "exit_status": 2
  },
  "actor": {
    "id": "1030c06a-bcbe-4738-9134-89af5c717fb1",
    "email": "noreply+webhooks@heroku.com"
  },
  "action": "destroy",
  "version": "application/vnd.heroku+json; version=3",
  "resource": "dyno",
  "created_at": "2026-09-29T08:32:57Z",
  "published_at": null,
  "previous_data": [],
  "webhook_metadata": {
    "event": {
      "id": "615fbe2a-5add-4658-93e6-a1b905d1c9a1",
      "include": "dyno"
    },
    "attempt": {
      "id": "92515333-3d4f-4ca9-ae43-760ec63322a4"
    },
    "webhook": {
      "id": "24cb449e-864f-4aa1-bb9c-f6051d4cc500"
    },
    "delivery": {
      "id": "aaa950ff-84b9-48ac-9f29-093e33c64ea6"
    }
  }
}
```

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