Send Push Notifications from a Bash Script
Send a bash push notification to your iPhone with one reusable curl function. Drop it into any shell script to get pinged when a job finishes or fails.
Send Push Notifications from a Bash Script
Your nightly backup script finished at 2 a.m. Did it actually work, or did it choke on a full disk and quietly exit? You won't know until you check — and you won't check, because you're asleep.
That gap is the whole reason to wire a bash push notification into your scripts. One curl call at the end of a job, and your phone buzzes the moment it finishes or fails. No dashboard to babysit, no log to tail, no SDK to install.
Here's the thing most tutorials get wrong: they show you a one-off curl command and call it a day. But you don't want to paste a curl command every time. You want one reusable function you write once and drop into every script you own. That's what we're building.
What you'll need
Two things, and you almost certainly already have both:
curl— preinstalled on macOS and every Linux distro worth using- An app key from TheNotificationApp — grab one free, no card required
No message queue, no broker, no agent running in the background — just curl and a key.
Step 1 — Grab your app key
Sign in with Apple at thenotification.app, create an app, and copy the app_key it gives you. This single header is the only auth the API needs — there's no OAuth dance, no token refresh, no client library. If you've used the curl docs, this is the exact same key.
Don't hardcode it into a script you'll commit. Keep it in an environment variable instead:
export TNA_APP_KEY="your_app_key_here"Drop that line in your ~/.zshrc or ~/.bashrc and it's available to every script you run.
Step 2 — Write the notify function
Here's the core. A handful of lines you'll reuse forever:
#!/usr/bin/env bash
notify() {
curl -fsS -X POST https://thenotification.app/api/sendNotification \
-H "app_key: ${TNA_APP_KEY}" \
-H "Content-Type: application/json" \
-d "$(printf '{"title":"%s","body":"%s"}' "$1" "$2")"
}
notify "Backup done" "Nightly backup finished without errors."Call it with two arguments — a title and a body — and your phone lights up. The title is the bold headline; the body is the line underneath.
Those curl flags matter more than they look:
-fmakes curl return a non-zero exit code on an HTTP error (a 401 or 429), instead of silently printing the error body and pretending it succeeded.-shides the progress meter so your script logs stay clean.-Sstill shows the real error message if something breaks.
That -f is the one people forget, and it's the one that bites. Without it, your script thinks every notification went through — even the ones the API rejected.
Step 3 — Actually handle failure
A notification you only send on success is half a system. The interesting case is when the job fails — that's the message you actually need at 2 a.m.
if ./long_job.sh; then
notify "Job done ✅" "long_job.sh finished cleanly."
else
status=$?
notify "Job failed ❌" "long_job.sh exited with status ${status}."
fiNow you get pinged either way, and the failure message carries the exit code so you know whether it was a missing file (often 1) or a killed process (137) before you even open your laptop.
Step 4 — Drop it into any script with a trap
Wrapping every command in an if gets old. For a script that should ping you the instant anything goes wrong, use a trap instead:
#!/usr/bin/env bash
set -euo pipefail
trap 'notify "Script crashed" "Failed at line ${LINENO}, exit ${?}"' ERR
# ... the rest of your script ...
# any failing command now fires the notification automaticallyWith set -e and an ERR trap, the first command that fails sends you the line number and exit code, then the script stops. This is the same pattern that makes a cron job failure notification reliable — the cron daemon swallows output, but it can't swallow a push to your phone.
One gotcha: quotes in your message
The printf version above is fine for plain text, but if your title or body contains a double quote, it'll break the JSON and the API returns a 400. If your messages include arbitrary data — a commit message, a filename, an error string — build the body with jq instead, which escapes everything for you:
notify() {
local payload
payload=$(jq -n --arg t "$1" --arg b "$2" '{title: $t, body: $b}')
curl -fsS -X POST https://thenotification.app/api/sendNotification \
-H "app_key: ${TNA_APP_KEY}" \
-H "Content-Type: application/json" \
-d "${payload}"
}A successful call comes back like {"success": true, "message": "Notification sent to 1 device(s)", "devices_sent": 1, "failed_count": 0}. If you want the optional tap-through link or an image, the full field list is in the API reference.
The honest tradeoff
Fair warning: the free tier is capped at 100 notifications total — that's across the lifetime of the account, not per month. It's plenty for "ping me when the backup finishes," but if you stick notify inside a loop that runs every minute, you'll burn through all 100 in under two hours and start getting 429 responses. Send on the events that matter — finished, failed, crashed — not on every iteration. If you need more headroom, Pro is $2.99/month for 1,000 notifications a month that reset on the 1st. The rate limits page spells out exactly how the 429 looks so you can handle it in code.
That's it
One function, written once, that turns any shell script into something that tells you how it went. Wire it into a backup, a deploy, a data migration — anything where "did it work?" is a question you'd rather have answered for you. If you've got a long-running script you keep checking on, this is worth a look. Free to start at thenotification.app.
Stop babysitting your scripts.
Free to download. Free tier available. Swiss-hosted, private by design.