Uptime Kuma monitor alert notifications on your phone.

Get a Highest-priority push notification when an Uptime Kuma monitor goes down, a quiet one when it's back up, and a heads-up before a TLS certificate or domain expires.

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 Uptime Kuma

Add it as a webhook for Webhook.

STEP 03

Get a push on your phone

With the text, sound and buttons from the recipe.

Setup

How to set up the Uptime Kuma webhook 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 Uptime Kuma, open Settings > Notifications > Set Up Notification and choose Webhook.
  3. Paste your webhook URL as the Post URL, keep the method on POST and set Request Body to Preset - application/json.https://••••••••/•••••••• your personal URL, shown after install
  4. Enable it on the monitors that should notify you (or tick Apply on all existing monitors), then press Test.
  5. In Uptime Kuma, open **Settings → Notifications → Set Up Notification** (or the **Set Up Notification** button when editing a monitor) and set:
  6. **Notification Type:** Webhook
  7. **Post URL:** the URL above
  8. **HTTP Method:** POST
  9. **Request Body:** **Preset - application/json**
  10. Tick **Default enabled** and/or **Apply on all existing monitors**, or enable the notification per monitor. Press **Test**: you should get a quiet "🔔 Uptime Kuma connected" push.
  11. What you'll get:
  12. **Down** and **back up** pushes. Kuma doesn't notify for *pending* (retrying) or *maintenance*, so neither does this recipe. Set a monitor's **Retries** if you want to ride out short blips before the down push.
  13. **Still down** reminders, when you set **Resend Notification if Down X times consecutively** on a monitor.
  14. **Certificate expiry** warnings for HTTPS monitors with **Certificate Expiry Notification** on (at 7, 14 and 21 days by default; change that under **Settings → Notifications → TLS Certificate Expiry**), and **domain expiry** warnings for monitors with **Domain Name Expiry Notification** on (days set under **Settings → Notifications → Domain Expiry**).
  15. "Back up" pushes say how long the monitor was down on Uptime Kuma 2.x; 1.23 doesn't send that, so you'll see the check's message instead.
  16. Kuma's webhook doesn't include your Kuma address. To add a **View monitor** button, append `?kuma=` with your Kuma base URL to the Post URL, e.g. `your webhook URL?kuma=https://kuma.example.com` (use `&kuma=` if the URL already has a `?`).
  17. The **multipart/form-data** preset and the **GET** method also work, but stick with the JSON preset if you can. Don't use **Custom Body**: this recipe reads Kuma's standard `{ heartbeat, monitor, msg }` body.
  18. Uptime Kuma doesn't sign webhooks, so keep your endpoint URL private. If you want a secret header anyway, **Additional Headers** accepts JSON, but JustPush doesn't check it. Failed deliveries aren't retried.
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// Uptime Kuma → monitor down, up, or TLS certificate/domain expiring
2// Fires when a self-hosted Uptime Kuma calls a Webhook notification: a monitor goes down
3// (or is still down, with resend on), comes back up, or a certificate or domain nears expiry.
4// Built for "Preset - application/json" ({ heartbeat, monitor, msg }); also reads the
5// form-data preset and the GET method.
6// Docs: https://github.com/louislam/uptime-kuma/blob/master/server/notification-providers/webhook.js
7
8// Safe table lookup: inherited names like "toString" never match.
9function pick(table, key, fallback) {
10 return Object.prototype.hasOwnProperty.call(table, key) ? table[key] : fallback
11}
12
13// Only link to real http(s) URLs.
14function link(url, cta) {
15 return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? [{ cta, url }] : []
16}
17
18// A trimmed string, or null for missing/empty/"undefined"/"N/A" values.
19function text(value) {
20 if (typeof value !== "string" && typeof value !== "number") return null
21 const s = String(value).trim()
22 return s && s !== "undefined" && s !== "null" && s !== "N/A" ? s : null
23}
24
25function obj(value) {
26 return value && typeof value === "object" && !Array.isArray(value) ? value : null
27}
28
29function parse(value) {
30 if (obj(value)) return value
31 if (typeof value !== "string") return null
32 try {
33 return JSON.parse(value)
34 } catch (e) {
35 return null
36 }
37}
38
39// Kuma times are UTC "2026-09-30 08:14:03.512".
40function utc(value) {
41 const s = text(value)
42 if (!s) return NaN
43 return Date.parse(s.replace(" ", "T") + (/[zZ]|[+-]\d\d:?\d\d$/.test(s) ? "" : "Z"))
44}
45
46// "4m 12s" / "2h 5m" / "3d 4h" from a number of seconds.
47function duration(seconds) {
48 const s = Math.round(Number(seconds))
49 if (!Number.isFinite(s) || s <= 0) return null
50 if (s < 60) return `${s}s`
51 if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`
52 if (s < 86400) return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`
53 return `${Math.floor(s / 86400)}d ${Math.floor((s % 86400) / 3600)}h`
54}
55
56// What the monitor checks, like Kuma's own extractAddress().
57function address(monitor) {
58 const host = text(monitor.hostname)
59 const port = text(monitor.port)
60 const url = text(monitor.url)
61 switch (monitor.type) {
62 case "push":
63 case "group":
64 return null
65 case "ping":
66 case "dns":
67 return host
68 case "port":
69 case "gamedig":
70 case "steam":
71 return host && port ? `${host}:${port}` : host
72 default:
73 return url && !["https://", "http://"].includes(url) ? url : host
74 }
75}
76
77// { heartbeat, monitor, msg } from the JSON preset, the form-data preset (a "data" field
78// holding JSON) or the GET method (query parameters holding JSON strings).
79function payload(request) {
80 const body = obj(request.body) || {}
81 const query = obj(request.query) || {}
82 if (typeof body.msg === "string") return body
83 const data = parse(body.data)
84 if (data && typeof data.msg === "string") return data
85 if (typeof query.msg === "string") {
86 return { msg: query.msg, heartbeat: parse(query.heartbeat), monitor: parse(query.monitor) }
87 }
88 return null
89}
90
91const STATES = {
92 0: "down",
93 1: "up",
94}
95
96function handleRequest(request) {
97 const data = payload(request)
98 // Guard: Kuma always sends all three keys (heartbeat and monitor are null for tests).
99 if (!data || !("heartbeat" in data) || !("monitor" in data)) {
100 console.log("Not an Uptime Kuma webhook — skipping")
101 return null
102 }
103
104 const msg = data.msg.trim()
105 const heartbeat = obj(data.heartbeat)
106 const monitor = obj(data.monitor)
107 const topic = "Uptime Kuma"
108
109 // Optional ?kuma=https://kuma.example.com adds a "View monitor" button.
110 const base = typeof request.query?.kuma === "string" ? request.query.kuma.replace(/\/+$/, "") : ""
111
112 // Messages without a monitor or heartbeat: the Test button and expiry warnings.
113 if (!heartbeat || !monitor) {
114 const cert = msg.match(/^\[(.*)\]\[(.*)\] (.+?) certificate (.*) will expire in (-?\d+) days?$/)
115 if (cert) {
116 const [, name, url, type, cn, days] = cert
117 const when = Number(days) > 0 ? `expires in ${days} day${days === "1" ? "" : "s"}` : "has expired"
118 const kind = type.charAt(0).toUpperCase() + type.slice(1)
119 return {
120 title: `🔒 Certificate for ${text(name) ?? "your monitor"} ${when}`,
121 message: [`${kind} certificate${text(cn) ? ` ${cn}` : ""}`, text(url)].filter(Boolean).join(" · "),
122 topic,
123 priority: 0, // Normal — renew it before it becomes an outage
124 buttons: link(text(url), "Open site"),
125 }
126 }
127
128 const domain = msg.match(/^Domain name (.+) will expire in (-?\d+) days?$/)
129 if (domain) {
130 const [, name, days] = domain
131 const when = Number(days) > 0 ? `expires in ${days} day${days === "1" ? "" : "s"}` : "has expired"
132 return {
133 title: `🌐 Domain ${name} ${when}`,
134 message: "Renew it with your registrar before it lapses",
135 topic,
136 priority: 0, // Normal — renew it before it becomes an outage
137 }
138 }
139
140 // The notification form's Test button sends "<notification name> Testing".
141 if (!heartbeat && !monitor && / Testing$/.test(msg)) {
142 const name = text(msg.replace(/ Testing$/, ""))
143 return {
144 title: "🔔 Uptime Kuma connected",
145 message: name ? `Test from the "${name}" notification received` : "Test notification received",
146 topic,
147 priority: -1, // Low — just confirms the setup works
148 }
149 }
150
151 console.log("Uptime Kuma message without a monitor we don't handle — skipping")
152 return null
153 }
154
155 // Kuma only notifies on down (0) and up (1); pending (2) and maintenance (3) never
156 // reach a notification, so anything else is skipped.
157 const state = pick(STATES, String(heartbeat.status), null)
158 if (!state) {
159 console.log(`Uptime Kuma heartbeat status ${heartbeat.status} isn't down or up — skipping`)
160 return null
161 }
162
163 const name = text(monitor.pathName) ?? text(monitor.name) ?? "Your monitor"
164 const where = address(monitor)
165 const detail = text(heartbeat.msg)
166 const id = text(monitor.id)
167 const buttons = [
168 ...(base && id ? link(`${base}/dashboard/${encodeURIComponent(id)}`, "View monitor") : []),
169 ...link(where, "Open site"),
170 ]
171
172 if (state === "down") {
173 // A resent "still down" reminder is a non-important heartbeat.
174 const reminder = heartbeat.important === false || heartbeat.important === 0
175 return {
176 title: reminder ? `🔴 ${name} is still down` : `🔴 ${name} is down`,
177 message: [detail, where].filter(Boolean).join(" · ") || "Monitor is down",
178 topic,
179 // Highest — the service is unreachable right now. A resend reminder is High:
180 // you already got the Highest push for this outage.
181 priority: reminder ? 1 : 2,
182 buttons,
183 }
184 }
185
186 // lastDownTime is only sent by Uptime Kuma 2.x.
187 const down = duration((utc(heartbeat.time) - utc(heartbeat.lastDownTime)) / 1000)
188 return {
189 title: `✅ ${name} is back up`,
190 message: down ? `It was down for ${down}` : detail ?? "Monitor is up again",
191 topic,
192 priority: -1, // Low — good news, nothing to do
193 buttons,
194 }
195}
Payload

The Uptime Kuma monitor webhook

This is what Uptime Kuma sends to your URL for webhook. It is a sample, trimmed to the fields recipes use.

monitor · sample.json
1{
2 "msg": "[Acme website] [🔴 Down] Request failed with status code 503",
3 "monitor": {
4 "id": 12,
5 "url": "https://acme.example.com",
6 "name": "Acme website",
7 "path": [
8 "Acme website"
9 ],
10 "port": null,
11 "tags": [],
12 "type": "http",
13 "active": true,
14 "method": "GET",
15 "parent": null,
16 "weight": 2000,
17 "keyword": null,
18 "timeout": 48,
19 "hostname": null,
20 "interval": 60,
21 "pathName": "Acme website",
22 "ignoreTls": false,
23 "maxretries": 0,
24 "upsideDown": false,
25 "childrenIDs": [],
26 "description": null,
27 "maintenance": false,
28 "maxredirects": 10,
29 "retryInterval": 60,
30 "resendInterval": 0,
31 "expiryNotification": true,
32 "accepted_statuscodes": [
33 "200-299"
34 ]
35 },
36 "heartbeat": {
37 "msg": "Request failed with status code 503",
38 "ping": null,
39 "time": "2026-09-30 08:14:03.512",
40 "status": 0,
41 "retries": 0,
42 "duration": 60,
43 "response": null,
44 "timezone": "Europe/Amsterdam",
45 "important": true,
46 "monitorID": 12,
47 "localDateTime": "2026-09-30 10:14:03",
48 "timezoneOffset": "+02:00"
49 }
50}
FAQ

Uptime Kuma monitor alert 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 Uptime Kuma 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

Uptime Kuma on your phone in two minutes.

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