# ntfy Phone Alerts Cookbook Push a notification to your phone from any script, scheduled task or automation, in one HTTP request. This cookbook covers how ntfy works, a five-minute setup, the headers that matter, a dozen small-business recipes you can paste today, a private self-hosted setup, and the rules that keep customer data out of your notifications. It comes from the alert setup described in [Build Your Own AI Prompt Hub](https://jwatte.com/blog/build-your-own-ai-prompt-hub/). Facts about ntfy are from its official documentation, linked at the end. Replace `T` in every example with your own topic. ## 1. How ntfy works - A **topic** is a name, like `shop-alerts-7Kq2vX9pLmZ4`. Anything that sends an HTTP POST or PUT to `https://ntfy.sh/` publishes a message to it. - Every phone, browser or computer **subscribed** to that topic gets the message as a notification. - There is no account and no setup step for a topic: it exists the first time you use it. ntfy's documentation puts the consequence plainly: "the topic is essentially a password." Anyone who knows the name can read your messages and send new ones. Use a long random name and keep it out of public code. - ntfy is open source ("Send push notifications to your phone or desktop via PUT/POST"), dual licensed Apache 2.0 and GPLv2. You can use the public server at ntfy.sh or run your own. ## 2. Five-minute setup 1. Install the app: Android from Google Play, F-Droid or a GitHub download, or the iPhone app from the App Store. There is also a web app you can add to your home screen. 2. Make a topic name nobody will guess. One way: `python3 -c "import secrets; print('shop-' + secrets.token_urlsafe(12))"`. 3. In the app, tap **+**, keep the server `ntfy.sh`, and enter the topic. 4. Send a test from any computer: ```sh curl -d "Hello from the shop" ntfy.sh/T ``` 5. On Android, turn on **instant delivery** for the topic if messages arrive late. It keeps a small permanent notification so the app can receive messages in doze mode. ## 3. The parts of a message | Header | Purpose | Example | |---|---|---| | (body) | The message text, up to 4,096 bytes | `-d "Backup finished"` | | `Title` | Bold first line | `-H "Title: Website down"` | | `Priority` | 1 min, 2 low, 3 default, 4 high, 5 urgent | `-H "Priority: 5"` | | `Tags` | Comma-separated; emoji short codes become icons | `-H "Tags: warning,moneybag"` | | `Click` | URL opened when the notification is tapped | `-H "Click: https://example.com/admin/orders/123"` | | `Actions` | Up to three buttons: `view`, `http` or `broadcast` | `-H "Actions: view, Open orders, https://example.com/admin"` | | `At` | Deliver later: `30m`, `tomorrow, 9am`, a timestamp (10 seconds to 3 days on ntfy.sh) | `-H "At: tomorrow, 9am"` | | `Attach` | URL of a file to attach | `-H "Attach: https://example.com/report.pdf"` | | `Markdown` | Render the body as Markdown | `-H "Markdown: yes"` | | `Email` | Also send by email (ntfy.sh needs a verified address; 5 a day on the free plan) | `-H "Email: you@example.com"` | | `Authorization` | Access token for a protected topic | `-H "Authorization: Bearer tk_..."` | ## 4. Limits on the free public server From ntfy's publishing documentation, for ntfy.sh: - 250 messages per day per visitor. - Bursts of 60 requests, refilling at one request every 5 seconds. - Attachments up to 2 MB each and 20 MB per visitor, deleted after 3 hours. - Scheduled delivery between 10 seconds and 3 days ahead. - Phone calls (`Call` header) need a verified number and a paid plan. A small business sending a few dozen alerts a day fits comfortably. A script that alerts in a loop does not: put a guard on it (see recipe 12). ## 5. Privacy rules ntfy's privacy policy says messages are cached on the server (12 hours by default), the server does not log message content by default, and there is no end-to-end encryption; it suggests self-hosting for sensitive content. So: - **Never put customer data in a message**: no phone numbers, emails, addresses, card details, health or legal details. - **Send what happened plus a link**: "New booking #1042, tap to open" with a `Click` link into your admin page, which sits behind its own sign-in. - **Treat the topic like a password**: keep it in an environment variable or secret store, not in a public repository or a web page. - **Self-host** when the event itself is sensitive (section 7). ## 6. Recipes for a small business Each recipe is one command or one automation step. `T` is your topic. **1. Website or booking page down.** Every 5 minutes from cron (`*/5 * * * *`) or Windows Task Scheduler: ```sh curl -fsS -m 20 https://example.com/book/ >/dev/null || \ curl -H "Priority: 5" -H "Tags: rotating_light" -H "Title: Booking page down" -d "No answer from the booking page" ntfy.sh/T ``` **2. Nightly backup failed.** Only fires when the backup command fails: ```sh restic backup /srv/data || curl -H "Priority: 4" -H "Tags: floppy_disk,x" -d "Nightly backup failed on $(hostname)" ntfy.sh/T ``` **3. Any Linux service failed (systemd).** A template unit that systemd starts when another unit fails: ```ini # /etc/systemd/system/notify-failure@.service [Unit] Description=Push an alert when %i fails [Service] Type=oneshot ExecStart=/usr/bin/curl -s -H "Priority: 4" -H "Tags: warning" -d "%i failed on %H" https://ntfy.sh/T ``` Then add `OnFailure=notify-failure@%n.service` to the `[Unit]` section of any service you care about and run `systemctl daemon-reload`. **4. New booking or contact form.** In n8n, Zapier or Make, after the form or booking trigger, add an HTTP request step: method POST, URL `https://ntfy.sh/T`, body `New booking for {date}`, header `Click` = the booking's admin URL. Leave the customer's name, phone and message in the booking system. **5. Payment failed.** The same HTTP step after your payment provider's webhook (failed charge or dispute): `Title: Payment failed`, `Priority: 4`, `Tags: credit_card,x`, `Click` = the order in your admin. **6. Low stock.** A daily query in your inventory system or a scheduled script: ```sh [ "$(stock_count SKU-123)" -lt 5 ] && curl -H "Tags: package" -H "Title: Low stock" -d "SKU-123 is under 5" ntfy.sh/T ``` **7. A long AI or batch job finished.** Last line of the script: ```sh curl -H "Tags: white_check_mark" -H "Click: https://hub.example.com/#job=$JOB" -d "Job $JOB finished" ntfy.sh/T ``` **8. Reminders scheduled today for tomorrow:** ```sh curl -H "At: tomorrow, 8am" -H "Title: Deposit due" -d "Bank the weekend cash before 10" ntfy.sh/T ``` **9. Certificate or domain about to expire.** Weekly: ```sh end=$(echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2) [ $(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 )) -lt 14 ] && curl -H "Priority: 4" -d "TLS certificate for example.com expires $end" ntfy.sh/T ``` **10. A build or deploy failed (GitHub Actions).** Last step of the job; store the topic as a repository secret: ```yaml - if: failure() run: curl -H "Priority: 4" -H "Tags: x" -d "Build failed on ${GITHUB_REF_NAME}" "ntfy.sh/${NTFY_TOPIC}" env: NTFY_TOPIC: ${{ secrets.NTFY_TOPIC }} ``` **11. From Windows PowerShell** (a printer, a scanner folder, a Windows backup): ```powershell Invoke-RestMethod -Method Post -Uri "https://ntfy.sh/T" ` -Headers @{ Title = "Printer offline"; Priority = "4"; Tags = "printer" } ` -Body "Front desk printer is not responding" ``` **12. One alert, not a flood.** Wrap repeated checks so they alert once until the problem clears: ```sh flag=/tmp/site-down.flag if curl -fsS -m 20 https://example.com/ >/dev/null; then [ -f "$flag" ] && rm -f "$flag" && curl -H "Tags: white_check_mark" -d "Site is back" ntfy.sh/T else [ -f "$flag" ] || { touch "$flag"; curl -H "Priority: 5" -d "Site is down" ntfy.sh/T; } fi ``` ### The same message from code Python (standard library only): ```python import urllib.request req = urllib.request.Request("https://ntfy.sh/T", data="Order #1042 shipped".encode(), headers={"Title": "Shipped", "Tags": "package"}) urllib.request.urlopen(req, timeout=10) ``` Node 18 or later: ```js await fetch("https://ntfy.sh/T", { method: "POST", body: "Order #1042 shipped", headers: { Title: "Shipped", Tags: "package" } }); ``` ## 7. A private ntfy server Use this when the events are sensitive, when you want your own limits, or when several people share alerts. `compose.yaml`: ```yaml services: ntfy: image: binwiederhier/ntfy command: serve restart: unless-stopped ports: ["127.0.0.1:8090:80"] volumes: - ./etc:/etc/ntfy - ./cache:/var/cache/ntfy - ./lib:/var/lib/ntfy ``` `etc/server.yml`: ```yaml base-url: "https://ntfy.example.com" cache-file: "/var/cache/ntfy/cache.db" auth-file: "/var/lib/ntfy/user.db" auth-default-access: "deny-all" upstream-base-url: "https://ntfy.sh" # needed for instant delivery to iPhones ``` Then create a user, give it a topic, and make a token for your scripts: ```sh docker compose exec ntfy ntfy user add --role=user shop docker compose exec ntfy ntfy access shop shop-alerts rw docker compose exec ntfy ntfy token add shop curl -H "Authorization: Bearer tk_..." -d "Private hello" https://ntfy.example.com/shop-alerts ``` Put the server behind HTTPS (a reverse proxy or a tunnel). In the phone app, add your server, sign in as the user, and subscribe to the topic. About `upstream-base-url`: ntfy's configuration documentation explains that without it, iPhones still get notifications eventually, but delivery can take hours, because iOS restricts background processing. ## 8. Troubleshooting - **Nothing arrives**: send `curl -d test ntfy.sh/T` and watch the topic in a browser at `https://ntfy.sh/T`. If the browser shows it, the phone subscription or battery settings are the problem. - **Late on Android**: turn on instant delivery, or exempt the app from battery optimization. - **Late on iPhone with a private server**: set `upstream-base-url`. - **HTTP 429**: you hit the rate limit. Add the guard from recipe 12. - **Someone else posted to your topic**: change the topic name everywhere, or move to a private server with tokens. ## Sources - ntfy project: https://github.com/binwiederhier/ntfy (description, licenses) - Publishing, headers, scheduled delivery and limits: https://docs.ntfy.sh/publish/ - Phone apps and instant delivery: https://docs.ntfy.sh/subscribe/phone/ - Privacy: https://docs.ntfy.sh/privacy/ - Self-hosting configuration, access control and iOS: https://docs.ntfy.sh/config/ - systemd `OnFailure=` and `%i`, `%n`, `%H` specifiers: https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html