Skip to content

Message Anatomy

Scope: the optional parts of a message — title, priority, tags, and a few display extras — set as HTTP headers, and what each one is for.

A plain message is one line of text, and for many jobs that is enough. The extras exist for the moment the message lands on a phone, where the first question is not "what does it say" but "do I need to look at this now." They are set with headers when publishing with curl (-H "X-Title: ..."), with flags when publishing with the ntfy command (--title ...), or as fields in the JSON form from chapter 2. All three routes produce the same message.

Title, priority, tags

$ curl -s -H "X-Title: Backup" -H "X-Priority: 5" -H "X-Tags: floppy_disk,warning" -d "Nightly backup failed" http://127.0.0.1:8099/lab-alerts
{"id":"UEaJynbc46zy","time":1792610721,"expires":1792653921,"event":"message","topic":"lab-alerts","title":"Backup","message":"Nightly backup failed","priority":5,"tags":["floppy_disk","warning"]}
  • Title (X-Title / --title) — the bold first line of the notification. Keep it to a few words: it answers "what is this about" before the message is opened.
  • Priority (X-Priority / --priority) — how insistently the receiving device presents the message. It is a number from 1 to 5, and the names work anywhere the number does:
Value Name Meaning
1 min quietest; no sound, folded away
2 low present, but unobtrusive
3 (default) the ordinary notification
4 high prominent
5 max most insistent the device allows

The default row is written the way it is because a plain message's receipt has no priority field at all — absence means 3. How a device renders each level (sounds, vibration, bypassing a quiet mode) is the phone app's business and differs between Android and iOS; the server only carries the number. Priority is a budget: a topic where everything is max teaches its owner to ignore it. - Tags (X-Tags / --tags) — comma-separated labels, carried as a list in the receipt. Tags are free text for your own organising, with one decoration: a tag that names a standard emoji shortcode (like warning or white_check_mark) is shown as that emoji by the phone and web apps, in front of the title. That rendering happens on the receiving side; the message itself only ever carries the names.

Check: the server validates what it can. A priority outside 1–5 is refused on the spot, with the reason and a link to the relevant page of the manual:

$ curl -s -w "\nHTTP %{http_code}\n" -H "X-Priority: 9" -d "Loud" http://127.0.0.1:8099/lab-alerts
{"code":40007,"http":400,"error":"invalid priority parameter: 9, please refer to the docs","link":"https://ntfy.sh/docs/publish/#message-priority"}

HTTP 400

Every ntfy error has this shape — a code, the HTTP status, an error sentence meant for humans, and link. When ntfy says no, it says why.

Display extras

Three more headers shape how a message is presented. All three were verified the same way as above, by reading them back in the receipt; what a phone does with them is the app's side of the bargain.

Markdown — the message body is Markdown, and the web and phone apps render it as such:

$ curl -s -u alice:alice-lab-pass -H "X-Markdown: true" -d "**Bold** news about the build" http://127.0.0.1:8099/lab-private
{"id":"40TjEe61KVnn","time":1792611234,"expires":1792654434,"event":"message","topic":"lab-private","message":"**Bold** news about the build","content_type":"text/markdown"}

Click (X-Click) — the address a tap on the notification opens:

$ curl -s -u alice:alice-lab-pass -H "X-Click: https://example.org/builds/412" -d "Build finished, tap for the log" http://127.0.0.1:8099/lab-private
{"id":"Nie5iWb2Nlo0","time":1792611407,"expires":1792654607,"event":"message","topic":"lab-private","message":"Build finished, tap for the log","click":"https://example.org/builds/412"}

Actions (X-Actions) — up to three buttons on the notification. A view action opens an address; http actions make a request of your choosing, which is how a notification grows an "acknowledge" or "restart it" button:

$ curl -s -u alice:alice-lab-pass -H "X-Actions: view, Open the build log, https://example.org/builds/412" -d "Build 412 finished" http://127.0.0.1:8099/lab-private
{"id":"GEJbnd0AJyAX","time":1792611235,"expires":1792654435,"event":"message","topic":"lab-private","message":"Build 412 finished","actions":[{"id":"Fpl57zmrfW","action":"view","label":"Open the build log","clear":false,"url":"https://example.org/builds/412"}]}

The receipt shows the header parsed into a small object — the button's own id, its label, where it goes. An http action is a standing instruction from a message to a phone, and phones ask before obeying some of them; treat actions as a convenience for the reader, never as a mechanism something depends on.

Check: publish with one extra at a time and read it back in the receipt. If a field is missing from the receipt, the message does not have it — spelling of header names is the usual suspect (chapter 11).