# 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. - Company: Uptime Kuma (https://www.justpush.io/recipes/uptime-kuma) - Event: `monitor` (Webhook) - Tags: Monitoring & observability, Uptime & downtime - Install: https://studio.justpush.io/recipes/uptime-kuma/monitor-alert - Web page: https://www.justpush.io/recipes/uptime-kuma/monitor-alert ## 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 Uptime Kuma, open Settings > Notifications > Set Up Notification and choose Webhook. 3. Paste your webhook URL (shown in Studio after install) as the Post URL, keep the method on POST and set Request Body to Preset - application/json. 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 (shown in Studio after install)?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. ## 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 // Uptime Kuma โ†’ monitor down, up, or TLS certificate/domain expiring // Fires when a self-hosted Uptime Kuma calls a Webhook notification: a monitor goes down // (or is still down, with resend on), comes back up, or a certificate or domain nears expiry. // Built for "Preset - application/json" ({ heartbeat, monitor, msg }); also reads the // form-data preset and the GET method. // Docs: https://github.com/louislam/uptime-kuma/blob/master/server/notification-providers/webhook.js // Safe table lookup: inherited names like "toString" never match. 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?:\/\/\S+$/.test(url) ? [{ cta, url }] : [] } // A trimmed string, or null for missing/empty/"undefined"/"N/A" values. function text(value) { if (typeof value !== "string" && typeof value !== "number") return null const s = String(value).trim() return s && s !== "undefined" && s !== "null" && s !== "N/A" ? s : null } function obj(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null } function parse(value) { if (obj(value)) return value if (typeof value !== "string") return null try { return JSON.parse(value) } catch (e) { return null } } // Kuma times are UTC "2026-09-30 08:14:03.512". function utc(value) { const s = text(value) if (!s) return NaN return Date.parse(s.replace(" ", "T") + (/[zZ]|[+-]\d\d:?\d\d$/.test(s) ? "" : "Z")) } // "4m 12s" / "2h 5m" / "3d 4h" from a number of seconds. function duration(seconds) { const s = Math.round(Number(seconds)) if (!Number.isFinite(s) || s <= 0) return null if (s < 60) return `${s}s` if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s` if (s < 86400) return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m` return `${Math.floor(s / 86400)}d ${Math.floor((s % 86400) / 3600)}h` } // What the monitor checks, like Kuma's own extractAddress(). function address(monitor) { const host = text(monitor.hostname) const port = text(monitor.port) const url = text(monitor.url) switch (monitor.type) { case "push": case "group": return null case "ping": case "dns": return host case "port": case "gamedig": case "steam": return host && port ? `${host}:${port}` : host default: return url && !["https://", "http://"].includes(url) ? url : host } } // { heartbeat, monitor, msg } from the JSON preset, the form-data preset (a "data" field // holding JSON) or the GET method (query parameters holding JSON strings). function payload(request) { const body = obj(request.body) || {} const query = obj(request.query) || {} if (typeof body.msg === "string") return body const data = parse(body.data) if (data && typeof data.msg === "string") return data if (typeof query.msg === "string") { return { msg: query.msg, heartbeat: parse(query.heartbeat), monitor: parse(query.monitor) } } return null } const STATES = { 0: "down", 1: "up", } function handleRequest(request) { const data = payload(request) // Guard: Kuma always sends all three keys (heartbeat and monitor are null for tests). if (!data || !("heartbeat" in data) || !("monitor" in data)) { console.log("Not an Uptime Kuma webhook โ€” skipping") return null } const msg = data.msg.trim() const heartbeat = obj(data.heartbeat) const monitor = obj(data.monitor) const topic = "Uptime Kuma" // Optional ?kuma=https://kuma.example.com adds a "View monitor" button. const base = typeof request.query?.kuma === "string" ? request.query.kuma.replace(/\/+$/, "") : "" // Messages without a monitor or heartbeat: the Test button and expiry warnings. if (!heartbeat || !monitor) { const cert = msg.match(/^\[(.*)\]\[(.*)\] (.+?) certificate (.*) will expire in (-?\d+) days?$/) if (cert) { const [, name, url, type, cn, days] = cert const when = Number(days) > 0 ? `expires in ${days} day${days === "1" ? "" : "s"}` : "has expired" const kind = type.charAt(0).toUpperCase() + type.slice(1) return { title: `๐Ÿ”’ Certificate for ${text(name) ?? "your monitor"} ${when}`, message: [`${kind} certificate${text(cn) ? ` ${cn}` : ""}`, text(url)].filter(Boolean).join(" ยท "), topic, priority: 0, // Normal โ€” renew it before it becomes an outage buttons: link(text(url), "Open site"), } } const domain = msg.match(/^Domain name (.+) will expire in (-?\d+) days?$/) if (domain) { const [, name, days] = domain const when = Number(days) > 0 ? `expires in ${days} day${days === "1" ? "" : "s"}` : "has expired" return { title: `๐ŸŒ Domain ${name} ${when}`, message: "Renew it with your registrar before it lapses", topic, priority: 0, // Normal โ€” renew it before it becomes an outage } } // The notification form's Test button sends " Testing". if (!heartbeat && !monitor && / Testing$/.test(msg)) { const name = text(msg.replace(/ Testing$/, "")) return { title: "๐Ÿ”” Uptime Kuma connected", message: name ? `Test from the "${name}" notification received` : "Test notification received", topic, priority: -1, // Low โ€” just confirms the setup works } } console.log("Uptime Kuma message without a monitor we don't handle โ€” skipping") return null } // Kuma only notifies on down (0) and up (1); pending (2) and maintenance (3) never // reach a notification, so anything else is skipped. const state = pick(STATES, String(heartbeat.status), null) if (!state) { console.log(`Uptime Kuma heartbeat status ${heartbeat.status} isn't down or up โ€” skipping`) return null } const name = text(monitor.pathName) ?? text(monitor.name) ?? "Your monitor" const where = address(monitor) const detail = text(heartbeat.msg) const id = text(monitor.id) const buttons = [ ...(base && id ? link(`${base}/dashboard/${encodeURIComponent(id)}`, "View monitor") : []), ...link(where, "Open site"), ] if (state === "down") { // A resent "still down" reminder is a non-important heartbeat. const reminder = heartbeat.important === false || heartbeat.important === 0 return { title: reminder ? `๐Ÿ”ด ${name} is still down` : `๐Ÿ”ด ${name} is down`, message: [detail, where].filter(Boolean).join(" ยท ") || "Monitor is down", topic, // Highest โ€” the service is unreachable right now. A resend reminder is High: // you already got the Highest push for this outage. priority: reminder ? 1 : 2, buttons, } } // lastDownTime is only sent by Uptime Kuma 2.x. const down = duration((utc(heartbeat.time) - utc(heartbeat.lastDownTime)) / 1000) return { title: `โœ… ${name} is back up`, message: down ? `It was down for ${down}` : detail ?? "Monitor is up again", topic, priority: -1, // Low โ€” good news, nothing to do buttons, } } ``` ## Uptime Kuma monitor webhook payload (sample) ```json { "msg": "[Acme website] [๐Ÿ”ด Down] Request failed with status code 503", "monitor": { "id": 12, "url": "https://acme.example.com", "name": "Acme website", "path": [ "Acme website" ], "port": null, "tags": [], "type": "http", "active": true, "method": "GET", "parent": null, "weight": 2000, "keyword": null, "timeout": 48, "hostname": null, "interval": 60, "pathName": "Acme website", "ignoreTls": false, "maxretries": 0, "upsideDown": false, "childrenIDs": [], "description": null, "maintenance": false, "maxredirects": 10, "retryInterval": 60, "resendInterval": 0, "expiryNotification": true, "accepted_statuscodes": [ "200-299" ] }, "heartbeat": { "msg": "Request failed with status code 503", "ping": null, "time": "2026-09-30 08:14:03.512", "status": 0, "retries": 0, "duration": 60, "response": null, "timezone": "Europe/Amsterdam", "important": true, "monitorID": 12, "localDateTime": "2026-09-30 10:14:03", "timezoneOffset": "+02:00" } } ``` ## 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 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.