Send a push notification to your iPhone from a script

One curl request sends a push notification to your iPhone. Examples for shell scripts, long-running commands, cron, GitHub Actions, Python and Node.

A script that runs for twenty minutes, a cron job that should never fail, a CI build you are waiting on. Each is easier to live with if it can tap you on the wrist when it finishes. The shortest way there is one HTTP request to a service that already has an app on your phone.

Why not send it to Apple directly

Every push notification to an iPhone goes through Apple's push service, and Apple only accepts them from the developer of the app that will show them. Sending one yourself means a paid Apple Developer account, an app of your own on the phone, a signing key, and the device token that app registered. That is a weekend of work for a message saying a backup finished.

A push service has done that part. You install its app once, and your scripts send it plain HTTP.

The request

Sign in to Acciti, install the app on your iPhone or Mac, and create a sender token. Put the token and your user key in the environment:

~/.profile
export ACCITI_TOKEN=...export ACCITI_USER=...

Then any script can do this:

curl
curl -fsS https://acciti.com/v1/messages \  -H "Authorization: Bearer $ACCITI_TOKEN" \  -H "Content-Type: application/json" \  -d '{"user": "'"$ACCITI_USER"'", "title": "Deploy", "message": "Finished in 4m 12s"}'

14:07

Deploy

now

Finished in 4m 12s

Scripts · chime

The request above, on a Lock Screen. It reaches your Apple Watch and your Mac too.

message takes up to 1024 characters, and title, sound, url and priority are optional.

Tell me when this command finishes

A shell function wraps any long command and reports whether it worked:

~/.zshrc or ~/.bashrc
notify() {  "$@"  status=$?  if [ "$status" -eq 0 ]; then result="finished"; else result="failed ($status)"; fi  curl -fsS https://acciti.com/v1/messages \    -H "Authorization: Bearer $ACCITI_TOKEN" \    -H "Content-Type: application/json" \    -d "{\"user\": \"$ACCITI_USER\", \"title\": \"$1 $result\", \"message\": \"$*\"}" > /dev/null  return "$status"} notify make release

It returns the command's own exit status, so it can sit inside other scripts without hiding failures.

From cron

For a job on a schedule, the most useful alert is the one that fires when the job didn't run at all. That is a heartbeat rather than a message:

crontab
0 3 * * *  /usr/local/bin/backup.sh && curl -fsS https://acciti.com/v1/heartbeats/$HEARTBEAT_TOKEN/ping

If the ping doesn't arrive on time, your phone rings. Monitoring backups has recipes for restic, borgmatic, rsync and systemd.

From GitHub Actions, only on failure

Store the token and user key as repository secrets, and add a last step that runs only when an earlier one failed:

.github/workflows/deploy.yml
      - name: Tell my phone        if: failure()        env:          ACCITI_TOKEN: ${{ secrets.ACCITI_TOKEN }}          ACCITI_USER: ${{ secrets.ACCITI_USER }}        run: |          curl -fsS https://acciti.com/v1/messages \            -H "Authorization: Bearer $ACCITI_TOKEN" \            -H "Content-Type: application/json" \            -d "{\"user\": \"$ACCITI_USER\", \"priority\": 1                 \"title\": \"${{ github.workflow }} failed\",                 \"message\": \"${{ github.repository }} on ${{ github.ref_name }}\",                 \"url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}"

The url opens the failed run when you tap the notification. To hear about pushes, releases and failed checks across every repository without editing workflows, use the GitHub connector instead.

From Python

Python
import os, urllib.request, json request = urllib.request.Request(    "https://acciti.com/v1/messages",    data=json.dumps({        "user": os.environ["ACCITI_USER"],        "title": "Training run",        "message": "Epoch 40 done, loss 0.031",    }).encode(),    headers={        "Authorization": f"Bearer {os.environ['ACCITI_TOKEN']}",        "Content-Type": "application/json",    },)urllib.request.urlopen(request)

Standard library only, so it works in a notebook or a container with nothing installed.

From Node

JavaScript
await fetch("https://acciti.com/v1/messages", {  method: "POST",  headers: {    Authorization: `Bearer ${process.env.ACCITI_TOKEN}`,    "Content-Type": "application/json",  },  body: JSON.stringify({    user: process.env.ACCITI_USER,    message: "Import finished: 48,210 rows",  }),})

Picking a priority

Most script notifications are Normal: a sound and a banner, held during your quiet hours. Use -1 for a banner with no sound, and 1 for something that should get through Focus. Keep 2, Emergency, for alerts that should repeat until you acknowledge them, such as production being down.

A script that sends Emergency for a finished build will teach you to ignore Emergency. The API documentation has every field, with examples in Swift and Go as well.

Make your first request ring.

Sign in, install the app on your iPhone, and paste the curl command above.