Back to blog
blog·June 17, 2026·4 min read

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:

  • -f makes 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.
  • -s hides the progress meter so your script logs stay clean.
  • -S still 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}."
fi

Now 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 automatically

With 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.

Get the app →