# Heroku release notifications on your phone

> Get a push notification when a new release of your Heroku app succeeds or fails, with the release version and what changed.

- Company: Heroku (https://www.justpush.io/recipes/heroku)
- Event: `api:release` (api:release)
- Tags: Hosting & deployment, Deployments
- Install: https://studio.justpush.io/recipes/heroku/release
- Web page: https://www.justpush.io/recipes/heroku/release

## 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 api:release event type.
4. Save. Each app needs its own webhook.
5. Subscribe the webhook to the **`api:release`** entity. In the Dashboard that's **More → View webhooks** on your app; with the Heroku CLI:
6. ```
7. heroku webhooks:add -i api:release -l notify -u your webhook URL (shown in Studio after install) -a your-app
8. ```
9. Use `-l sync` instead of `-l notify` if you want Heroku to retry failed deliveries.
10. Heroku sends two or three events per release (created, then updated as the release phase runs). The recipe pushes once, when the status becomes `succeeded` or `failed`.
11. Every config var change and add-on change also creates a release. Add `?deploys=1` to only get successful releases that ship code (failed releases always push):
12. ```
13. your webhook URL (shown in Studio after install)?deploys=1
14. ```
15. 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 → api:release (release succeeded or failed)
// Fires when a Heroku app webhook subscribed to api:release reports a release reaching
// "succeeded" or "failed". Pending steps (and updates that don't change the status) stay quiet.
// Docs: https://devcenter.heroku.com/articles/webhook-events#api-release

// Final release statuses, their labels and how loud to be.
const STATUSES = {
    // A failed release (usually the release phase) means the new version isn't live.
    failed: { emoji: "❌", verb: "release failed", priority: 1 },
    // A successful release is routine.
    succeeded: { emoji: "✅", verb: "released", priority: -1 },
}

// Look a key up in one of the tables above, ignoring 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 }] : []
}

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 release events.
    if (body.resource !== "release" && include !== "api:release") {
        console.log(`Ignoring Heroku "${include ?? body.resource ?? "unknown"}" event`)
        return null
    }

    const status = pick(STATUSES, data.status, null)
    // One push per release: on create when it's already final (no release phase), or on the
    // update that changes the status. Updates that only flip "current" are skipped.
    const previous = body.previous_data && typeof body.previous_data === "object" ? body.previous_data : {}
    const changed = body.action === "create" || Object.prototype.hasOwnProperty.call(previous, "status")
    if (!status || !changed) {
        console.log(`Skipping release ${body.action ?? "event"} with status "${data.status ?? "none"}"`)
        return null
    }

    // ?deploys=1 skips successful releases that aren't code deploys (config vars, add-ons).
    const deploysOnly = ["1", "true", "yes"].includes(String(request.query?.deploys ?? "").toLowerCase())
    if (deploysOnly && data.status === "succeeded" && !data.slug) {
        console.log("Skipping a release without a slug (the URL has ?deploys=1)")
        return null
    }

    const app = typeof data.app?.name === "string" && data.app.name ? data.app.name : null
    const version = Number.isInteger(data.version) ? ` v${data.version}` : ""
    const who = data.user?.email ?? body.actor?.email
    const message = [data.description, who].filter((part) => typeof part === "string" && part).join(" · ")

    return {
        title: `${status.emoji} ${app ?? "Your app"}${version} ${status.verb}`,
        message: message || (data.status === "failed" ? "The release failed" : "The release is live"),
        topic: "Heroku",
        priority: status.priority,
        buttons: app ? link(`https://dashboard.heroku.com/apps/${encodeURIComponent(app)}/activity`, "View activity") : [],
    }
}
```

## Heroku api:release webhook payload (sample)

```json
{
  "id": "4273290b-193c-42fa-8931-7f2c92c5b28f",
  "data": {
    "id": "ddc2fba2-b2ad-41f5-bf7c-6e9b4614a53b",
    "app": {
      "id": "61cfe066-573f-45b4-a566-138323880eeb",
      "name": "acme-api"
    },
    "slug": {
      "id": "7adfdf15-f4a8-429d-bfbf-09d130ee2182"
    },
    "user": {
      "id": "125f0179-2fc8-4833-9e20-6e9314f2b233",
      "email": "jane@example.com"
    },
    "status": "failed",
    "current": false,
    "version": 42,
    "created_at": "2026-09-29T07:29:51Z",
    "updated_at": "2026-09-29T07:30:32Z",
    "description": "Deploy 3f2a1bc9"
  },
  "actor": {
    "id": "125f0179-2fc8-4833-9e20-6e9314f2b233",
    "email": "jane@example.com"
  },
  "action": "update",
  "version": "application/vnd.heroku+json; version=3",
  "resource": "release",
  "sequence": null,
  "created_at": "2026-09-29T07:30:32Z",
  "updated_at": "2026-09-29T07:30:32Z",
  "published_at": null,
  "previous_data": {
    "status": "pending"
  },
  "webhook_metadata": {
    "event": {
      "id": "4273290b-193c-42fa-8931-7f2c92c5b28f",
      "include": "api:release"
    },
    "attempt": {
      "id": "8a44f820-2354-489d-9a11-a793cbf49979"
    },
    "webhook": {
      "id": "b54e6a7e-d162-4bd7-ab42-1011582c19cc"
    },
    "delivery": {
      "id": "d244009a-670f-4340-88e9-789a4f9002d5"
    }
  }
}
```

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