Expo build finished notifications on your phone.

Get a High-priority push notification when an EAS build fails, and a quiet one when it's ready or canceled.

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 Expo

Add it as a webhook for BUILD.

STEP 03

Get a push on your phone

With the text, sound and buttons from the recipe.

Setup

How to set up the Expo build 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 your project's directory, run eas webhook:create and choose the BUILD event.
  3. Paste your webhook URL as the webhook URL.https://••••••••/•••••••• your personal URL, shown after install
  4. Enter any signing secret of at least 16 characters. EAS requires one, but JustPush doesn't check it yet.
  5. Webhooks are per project: repeat this in every project you want to hear about.
  6. Run this in your project's directory (it needs EAS CLI and a logged-in account):
  7. ```
  8. eas webhook:create --event BUILD --url "your webhook URL"
  9. ```
  10. EAS asks for a signing secret of at least 16 characters; any random value works. Webhooks are set per project, so repeat this in each project.
  11. You get a push for every build that finishes: High priority when it errors, Low when it's ready or canceled. A failed build shows EAS's error message; tap **View build** for the full logs.
  12. EAS signs each request with `expo-signature` (HMAC-SHA1), 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// Expo → EAS build finished, errored or canceled
2// Fires when an EAS Build completes, from a webhook created with `eas webhook:create --event BUILD`.
3// Docs: https://docs.expo.dev/eas/webhooks/
4
5// Final build statuses: failures are loud, a finished build is a quiet "it's ready".
6const STATUSES = {
7 errored: { emoji: "❌", verb: "build failed", priority: 1 }, // needs you soon: fix and rebuild
8 finished: { emoji: "✅", verb: "build ready", priority: -1 }, // good news, no action needed
9 canceled: { emoji: "⏹️", verb: "build canceled", priority: -1 }, // someone stopped it on purpose
10}
11
12const PLATFORMS = {
13 ios: "iOS",
14 android: "Android",
15}
16
17// Look a key up in a table without tripping over inherited names like "toString".
18function pick(table, key, fallback) {
19 return typeof key === "string" && Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
20}
21
22// Treat blank or non-string values as missing.
23function text(value) {
24 if (typeof value === "number" && Number.isFinite(value)) return String(value)
25 return typeof value === "string" && value.trim() ? value.trim() : null
26}
27
28// Only link to real http(s) URLs.
29function link(url, cta) {
30 return typeof url === "string" && /^https?:\/\//.test(url) ? [{ cta, url }] : []
31}
32
33function handleRequest(request) {
34 const body = request.body && typeof request.body === "object" ? request.body : {}
35
36 // Guard: EAS sends no event header, so recognise a build by its shape. Submission
37 // webhooks have submissionDetailsPageUrl instead and are handled by expo/submission.
38 if (!body.buildDetailsPageUrl || body.submissionDetailsPageUrl || body.submissionInfo) {
39 console.log("Not an EAS build webhook, skipping")
40 return null
41 }
42
43 const status = pick(STATUSES, body.status, null)
44 if (!status) {
45 console.log(`Skipping EAS build status "${body.status ?? "unknown"}"`)
46 return null
47 }
48
49 const meta = body.metadata && typeof body.metadata === "object" ? body.metadata : {}
50 const app = text(meta.appName) ?? text(body.projectName) ?? "Your app"
51 const platform = pick(PLATFORMS, body.platform, null)
52
53 // "1.4.0 (57)": the app version plus the build number or version code.
54 const version = text(meta.appVersion)
55 ? text(meta.appVersion) + (text(meta.appBuildVersion) ? ` (${text(meta.appBuildVersion)})` : "")
56 : null
57 // Failed builds explain why; others show the commit (or the message attached to the build).
58 const detail = body.status === "errored"
59 ? text(body.error?.message) ?? "See the build logs for details"
60 : text(meta.gitCommitMessage)?.split("\n")[0] ?? text(meta.message)
61
62 const message = [text(meta.buildProfile), version, detail].filter(Boolean).join(" · ")
63
64 return {
65 title: `${status.emoji} ${app} ${platform ? `${platform} ` : ""}${status.verb}`,
66 message: message || `EAS build ${body.status}`,
67 topic: "Expo",
68 priority: status.priority,
69 buttons: [
70 ...link(body.buildDetailsPageUrl, "View build"),
71 ...(body.status === "finished" ? link(body.artifacts?.buildUrl, "Download") : []),
72 ],
73 }
74}
Payload

The Expo BUILD webhook

This is what Expo sends to your URL for build. It is a sample, trimmed to the fields recipes use.

BUILD · sample.json
1{
2 "id": "147a3212-49fd-446f-b4e3-a6519acf264a",
3 "appId": "bc0a82de-65a5-4497-ad86-54ff1f53edf7",
4 "error": {
5 "message": "The \"Run fastlane\" step failed because of an error in the Xcode build process.",
6 "errorCode": "XCODE_BUILD_ERROR"
7 },
8 "status": "errored",
9 "metadata": {
10 "appName": "Acme Shop",
11 "username": "janedoe",
12 "workflow": "managed",
13 "runFromCI": false,
14 "appVersion": "1.4.0",
15 "sdkVersion": "53.0.0",
16 "buildProfile": "production",
17 "distribution": "store",
18 "appIdentifier": "com.example.acmeshop",
19 "gitCommitHash": "564b61ebdd403d28b5dc616a12ce160b91585b5b",
20 "appBuildVersion": "57",
21 "gitCommitMessage": "Add checkout screen"
22 },
23 "platform": "ios",
24 "priority": "high",
25 "artifacts": {
26 "logsS3KeyPrefix": "production/f9609423-5072-4ea2-a0a5-c345eedf2c2a"
27 },
28 "createdAt": "2026-09-24T09:53:01.155Z",
29 "updatedAt": "2026-09-24T10:07:42.715Z",
30 "accountName": "acme",
31 "completedAt": "2026-09-24T10:07:42.715Z",
32 "projectName": "acme-shop",
33 "cancelingUserId": null,
34 "initiatingUserId": "d1041496-1a59-423a-8caf-479bb978203a",
35 "buildDetailsPageUrl": "https://expo.dev/accounts/acme/projects/acme-shop/builds/147a3212-49fd-446f-b4e3-a6519acf264a"
36}
FAQ

Expo build finished 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 Expo 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

Expo on your phone in two minutes.

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