ntfy

Send ntfy push notifications from curl and Python

ntfy sends a push notification to your phone with one HTTP request, curl -d "Backup done" ntfy.sh/your-topic. There is no signup, the apps are free, and the public ntfy.sh server allows 250 messages a day of up to 4,096 bytes each. Topics are public, so pick a long random name, add access control or self-host.

4 min readNotifications and alerts

At a glance

  • Publish with a POST to ntfy.sh/<topic>; subscribe to the same topic in the Android, iOS or web app.
  • Headers set the Title, Priority (1 to 5), Tags and a Click URL.
  • ntfy.sh caches messages for 12 hours and allows 250 a day per visitor.
  • Anyone who knows a topic name can read and publish to it.

Send your first notification

Install the ntfy app on Android or iOS, or open the web app, and subscribe to a topic name. There is no signup: a topic exists the moment you use it, so your server or laptop only has to publish to the same name.

Bash
curl -d "Backup done" ntfy.sh/your-topic

The message arrives on every device subscribed to that topic.

Plain text works, but HTTP headers control how the notification looks and behaves. Pass them with -H when you publish.

  • Title sets a heading above the message.
  • Priority goes from 1 to 5, or use the names min, low, default, high and urgent.
  • Tags takes emoji shortcodes such as warning or white_check_mark.
  • Click is a URL that opens when you tap the notification.
Bash
curl \
  -H "Title: Backup finished" \
  -H "Priority: high" \
  -H "Tags: white_check_mark,floppy_disk" \
  -H "Click: https://example.com/backups/2026-10-30" \
  -d "1,284 files, 3.2 GB in 42 minutes" \
  ntfy.sh/your-topic

Send from Python

You don't need an SDK. A POST with requests does the same thing as curl: the message is the body and the options are headers.

For unattended scripts, wrap the job in try/except so a crash sends an urgent notification with a warning tag, while a normal run sends the regular message. Set timeout=10 so a network hiccup never hangs the script, and catch errors inside the helper so a failed push never hides the job's own traceback.

Python
import os

import requests

TOPIC_URL = f"https://ntfy.sh/{os.environ['NTFY_TOPIC']}"


def push(message, title, priority="default", tags="bell"):
    try:
        requests.post(
            TOPIC_URL,
            data=message.encode("utf-8"),
            headers={"Title": title, "Priority": priority, "Tags": tags},
            timeout=10,
        )
    except requests.RequestException as exc:
        print(f"ntfy push failed: {exc!r}")


try:
    rows = run_import()
except Exception as exc:
    push(repr(exc), "Import failed", priority="urgent", tags="warning")
    raise
else:
    push(f"{rows:,} rows imported", "Import finished", tags="white_check_mark")

Attach a log file and check what arrived

Send a file with curl -T and name it with the Filename header. The notification reads "You received a file: backup.log" and links to the download; on ntfy.sh attachments expire after 3 hours.

Bash
curl -T backup.log -H "Filename: backup.log" -H "Title: backup.sh failed" ntfy.sh/your-topic

curl -s "ntfy.sh/your-topic/json?poll=1"

To see what reached a topic, poll it: ?poll=1 returns the cached messages as one JSON object per line, then closes the connection. Without poll=1 the connection stays open and prints each new message as it arrives, which makes a small subscriber for another server.

Keep your topic private

On ntfy.sh, topics are public. Anyone who knows the name can subscribe and read your alerts, or publish their own. As the ntfy FAQ puts it, without access control the topic name is your password, and a randomly generated name is as good as a good password.

Don't use a guessable name. Generate a long random one:

Bash
python -c "import secrets; print(secrets.token_urlsafe(18))"

For real access control, use a server with users and access tokens, or run your own. Protected topics accept a username and password through Basic auth, or an access token in an Authorization: Bearer tk_... header.

Self-host ntfy

ntfy is open source and runs with one Docker command. By default it keeps messages in memory for 12 hours, so they are lost when the server restarts. Mount a volume and pass --cache-file to keep them on disk.

Bash
docker run -p 80:80 -it binwiederhier/ntfy serve

docker run -p 80:80 -v /var/cache/ntfy:/var/cache/ntfy -it \
  binwiederhier/ntfy serve --cache-file /var/cache/ntfy/cache.db

On your own server you set every limit yourself.

ntfy.sh free limits

The public ntfy.sh server is free to use, and paid ntfy Pro tiers exist if you need more. Here is how it compares to a self-hosted server's defaults:

Limitntfy.sh (public server)Self-hosted default
Messages per day250 per visitorNo daily cap; request limits apply
Message size4,096 bytes; longer becomes an attachment4,096 bytes, configurable
Request rateBurst of 60, then 1 every 5 secondsBurst of 60, then 1 every 5 seconds
Message cache12 hours12 hours in memory, unless you set a cache file
Attachment expiry3 hours3 hours
Emails5 per dayBurst of 16, then 1 per hour

For long Python jobs, a live progress link in the Click header turns the notification into a one-tap status check.

FAQ

Is ntfy free?

Yes. ntfy is open source, so self-hosting is free, and the public ntfy.sh server is free within its limits of 250 messages a day. Paid ntfy Pro tiers on ntfy.sh raise those limits.

Are ntfy topics private?

Not on ntfy.sh by default: anyone who knows the topic name can read and publish to it. Without access control the name acts as the password, so generate a long random one.

How long does ntfy.sh keep messages?

ntfy.sh caches messages for 12 hours, so a phone that was offline still receives them when it reconnects. Attachments are deleted after 3 hours.

Does ntfy work on iPhone?

Yes. There is an iOS app next to the Android and web apps, though the ntfy FAQ notes that the iOS app lags behind the Android one.

How do I open a link when I tap an ntfy notification?

Send a Click header with the URL when you publish. Tapping the notification on your phone then opens that URL.

Check on long runs from your phone

Change one import and every tqdm bar prints a link you can open on any device. Get a Discord or Slack message when the run finishes, fails or stalls.

Get a live linkpip install tqdm-live

Sources