Everything ntfy can do on the publish side, one recipe at a time. All options
are validated up front β a typo raises NimbleOptions.ValidationError instead
of silently dropping a feature. The full option reference lives in
ExNtfy.Publish.Options.
The basics
{:ok, message} = ExNtfy.publish("mytopic", "Backup completed π")
message.id #=> "xE73Iyuabi" β keep it if you want to update/clear later
ExNtfy.publish!("mytopic", "or raise on failure")Title, priority, tags
ExNtfy.publish("alerts", "Disk almost full",
title: "Server alert",
priority: :urgent, # 1..5 or :min | :low | :default | :high | :max | :urgent
tags: [:warning, "disk-42"] # emoji shortcodes render as emoji
)Markdown, click URLs, icons
ExNtfy.publish("deploys", "Deploy finished **successfully**",
markdown: true,
click: "https://example.com/deploys/42",
icon: "https://example.com/icon.png"
)Action buttons (up to 3)
alias ExNtfy.Action
ExNtfy.publish("garage", "Door open for 15 minutes",
actions: [
%Action{type: :view, label: "Camera", url: "https://cam.example.com", clear: true},
%Action{type: :http, label: "Close door", url: "https://api.example.com/door",
method: "PUT", body: ~s({"action":"close"})},
%Action{type: :broadcast, label: "Take picture", extras: %{"cmd" => "pic"}}
]
):view opens a URL, :http fires a request, :broadcast sends an Android
broadcast, :copy copies a value. Plain ntfy-shaped maps and raw JSON strings
are also accepted.
Scheduled delivery
ExNtfy.publish("reminders", "Standup!", delay: "30m")
ExNtfy.publish("reminders", "Happy new year", delay: ~U[2027-01-01 00:00:00Z])
ExNtfy.publish("reminders", "Lunch", delay: "tomorrow, 12pm")E-mail and phone-call forwarding
ExNtfy.publish("alerts", "Server down", email: "ops@example.com")
ExNtfy.publish("alerts", "Server REALLY down", call: true) # verified numberAttachments
By URL (the server never downloads it β clients do):
ExNtfy.publish("mytopic", "Check this out",
attach: "https://example.com/build.log", filename: "build.log")By upload β iodata, a stream of chunks, or {:file, path} (streamed from
disk; filename defaults to the basename):
{:ok, message} =
ExNtfy.publish_file("mytopic", {:file, "/tmp/flower.jpg"},
message: "Look what I found")
message.attachment.url #=> "https://ntfy.sh/file/..."ntfy.sh caps uploads (~2 MB); an oversized file returns
{:error, %ExNtfy.Error{http: 413}}.
Notification lifecycle: update, clear, delete
Messages sharing a sequence ID form a sequence; clients show the latest state.
The simplest idiom reuses the returned id:
{:ok, msg} = ExNtfy.publish("deploys", "Deploy startedβ¦")
ExNtfy.update("deploys", msg.id, "Deploy at 80%β¦")
ExNtfy.update("deploys", msg.id, "Deploy finished β
")
ExNtfy.clear("deploys", msg.id) # mark read + dismiss on all clients
ExNtfy.delete("deploys", msg.id) # remove from clients entirelyOr pick your own sequence ID up front:
ExNtfy.publish("deploys", "β¦", sequence_id: "deploy-42").
Templates: forward webhook payloads
publish_raw/3 sends the body byte-identical with options as headers β the
path for handing ntfy a JSON webhook payload to render server-side:
# pre-defined templates
ExNtfy.publish_raw("builds", github_webhook_json, template: :github)
# inline Go templates evaluated against the body
ExNtfy.publish_raw("builds", payload_json,
template: true,
title: "CI result",
message: "Build {{.status}} ({{.commit}})"
)Webhook-style GET
Everything in the query string β handy for curl one-liners and systems that can only fire GETs:
ExNtfy.trigger("mytopic", message: "cron ran", tags: [:clock])Header-only switches
ExNtfy.publish("mytopic", "ephemeral", cache: false) # live subscribers only
ExNtfy.publish("mytopic", "no FCM", firebase: false) # ntfy connections only
ExNtfy.publish("up-app-topic", "wake up", unified_push: true)Telemetry
Every publish emits [:ex_ntfy, :publish, :start | :stop | :exception] span
events with %{topic, base_url} metadata β never credentials or contents.