Skip to content

Subscribing

Scope: the receiving end — the ntfy command held open, the raw HTTP streams under it, and subscriptions that run a command per message.

Subscribing is publishing turned around: a GET to the topic's address that does not end. The server holds the connection open and writes each new message as it is published. Nothing polls; the message arrives when it arrives.

With the ntfy command

$ ntfy subscribe http://127.0.0.1:8099/lab-alerts
{"id":"0nLWsWUJqPGW","time":1792610704,"expires":1792653904,"event":"message","topic":"lab-alerts","message":"Live from the second shell"}

The command prints each message as one line of JSON — the same receipt shape chapter 2 published — and keeps running until you stop it (Ctrl-C). It shows only messages published after it started; what came before is the cache's story, and it is chapter 5. (In the capture above, a second shell published to lab-alerts while this one listened. Yours will sit silent until something publishes — silence means an idle topic, not a broken one.)

The browser-shaped subscription is the web app: the server's root address, which answered 200 in chapter 1, is a page where you subscribe to topics and read them in the browser, with the titles, tags, and priorities of chapter 3 rendered. The phone apps subscribe the same way under the hood. Their behaviour — background delivery, sounds per priority — belongs to the phone and could not be exercised by this guide's lab; it is listed as documented-only in criteria.md.

The raw streams

The CLI is a convenience over three stream formats, all the same held-open GET with a different suffix:

  • /<topic>/json — one JSON object per line (the format the CLI prints).
  • /<topic>/sse — the same events in Server-Sent Events framing, the shape browsers consume natively.
  • /<topic>/raw — nothing but the message texts, one per line: the format for feeding another program that wants words, not objects.

Check: the JSON stream, held open while a second shell publishes:

$ curl -sN http://127.0.0.1:8099/lab-alerts/json
{"id":"nKb6DpNVimbX","time":1792610763,"expires":1792653963,"event":"open","topic":"lab-alerts"}
{"id":"G4ZLRJfSgCsK","time":1792610763,"expires":1792653963,"event":"message","topic":"lab-alerts","message":"Streamed over HTTP"}
$ curl -s -d "Streamed over HTTP" http://127.0.0.1:8099/lab-alerts
$ curl -sN http://127.0.0.1:8099/lab-alerts/sse
event: open
data: {"id":"v11J4nryKtFr","time":1792610763,"expires":1792653963,"event":"open","topic":"lab-alerts"}

event: message
data: {"id":"bOFzswkTTTAX","time":1792610763,"expires":1792653963,"event":"message","topic":"lab-alerts","message":"Streamed over SSE"}

Two things to notice. The first line of a stream is an open event, not a message — the server announcing that the subscription is live, so a program can tell "connected, nothing yet" from "not connected." And curl needs -N (unbuffered) to show lines as they arrive; without it the stream still flows, but your terminal shows it in lumps and you may conclude — wrongly — that nothing is happening. If a held- open subscription looks dead, buffering is the first suspect (chapter 11).

The raw stream, for contrast — bob is reading the locked lab-private topic of chapter 8, credentials and all, and what arrives is bare text:

$ curl -sN -u bob:bob-lab-pass http://127.0.0.1:8099/lab-private/raw   (held open; the publish above arrives)

Build finished, tap for the log

Subscriptions that act

Give ntfy subscribe a command after the topic address and it runs that command once per message instead of printing. The message's fields arrive as environment variables — $id, $time, $topic, $title, $message, $priority, $tags:

$ ntfy subscribe --user alice:alice-lab-pass http://127.0.0.1:8099/lab-private 'echo "GOT: [$priority] $title - $message"'
$ curl -s -u alice:alice-lab-pass -H "X-Title: Sensor" -H "X-Priority: 4" -d "Door opened" http://127.0.0.1:8099/lab-private
--- what the subscribe command printed ---
GOT: [4] Sensor - Door opened

This is the pattern that turns a notification service into a trigger: a message on a topic starts a script on the machine that heard it. Hold it to the same standard as any other remote trigger — it runs, as you, whatever the message says to run. On a server anyone can publish to, that is a door, not a feature; chapter 8's access control is what makes it a reasonable one.