Publishing to ntfy topics — usually called through the ExNtfy facade.
Three publish shapes, matching the ntfy API:
publish/3— JSON publish (POST /), the default path.publish_raw/3— raw-body publish (POST /<topic>): the body passes through byte-identical and options travel as headers. This is the path for templating (:template) and webhook payloads.trigger/2— webhook-styleGET /<topic>/triggerwith options only in the query string.
All accept publish options (see ExNtfy.Publish.Options) mixed with client
options (see ExNtfy.Config) in one keyword list.
Telemetry
Every request is wrapped in :telemetry.span/3, emitting
[:ex_ntfy, :publish, :start], [:ex_ntfy, :publish, :stop], and
[:ex_ntfy, :publish, :exception] with metadata %{topic: topic, base_url: base_url} — never credentials or message contents.
Summary
Functions
Clears (marks read and dismisses) a delivered notification:
PUT /<topic>/<sequence_id>/clear. The server also accepts a /read alias
and a GET form for header-limited clients; this SDK exposes only the
canonical endpoint.
Deletes a notification from clients: DELETE /<topic>/<sequence_id>.
History remains server-side (storage is append-only), and a deleted
sequence revives if republished.
Publishes a message as JSON (POST /).
Same as publish/3, but returns the message directly and raises
ExNtfy.Error on failure.
Uploads a binary attachment: PUT /<topic> with the file as the request
body (reference §1.4).
Same as publish_file/3, but returns the message directly and raises
ExNtfy.Error on failure.
Publishes a raw body to POST /<topic> — the body is sent byte-identical
and every publish option becomes its canonical X- header (non-ASCII values
RFC 2047-encoded).
Webhook-style publish: GET /<topic>/trigger with every option in the query
string and no body. An empty :message defaults to "triggered" server-side.
Updates a delivered notification by publishing again with the same sequence
ID (reference §1.7) — sugar over publish/3 with :sequence_id set.
Functions
@spec clear(String.t(), String.t(), keyword()) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Clears (marks read and dismisses) a delivered notification:
PUT /<topic>/<sequence_id>/clear. The server also accepts a /read alias
and a GET form for header-limited clients; this SDK exposes only the
canonical endpoint.
Returns the emitted event: :message_clear message.
@spec delete(String.t(), String.t(), keyword()) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Deletes a notification from clients: DELETE /<topic>/<sequence_id>.
History remains server-side (storage is append-only), and a deleted
sequence revives if republished.
Returns the emitted event: :message_delete message.
@spec publish(String.t(), String.t() | nil, keyword()) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Publishes a message as JSON (POST /).
message may be nil for option-only publishes. Header-only options
(:cache, :firebase, :unified_push, :template, :poll_id) are sent
as headers on the same request.
Returns the created message parsed from the server response.
@spec publish!(String.t(), String.t() | nil, keyword()) :: ExNtfy.Message.t()
Same as publish/3, but returns the message directly and raises
ExNtfy.Error on failure.
@spec publish_file( String.t(), iodata() | Enumerable.t() | {:file, Path.t()}, keyword() ) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Uploads a binary attachment: PUT /<topic> with the file as the request
body (reference §1.4).
body may be iodata, an Enumerable of chunks, or {:file, path} — the
latter streams from disk without reading the whole file into memory, and
defaults :filename to Path.basename(path). All publish options ride
along as headers (the body is the file, so this is the header path); use
:message for the notification text shown alongside the attachment.
Server limits apply (~2 MB per file on ntfy.sh); an oversized upload comes
back as {:error, %ExNtfy.Error{http: 413}}.
@spec publish_file!( String.t(), iodata() | Enumerable.t() | {:file, Path.t()}, keyword() ) :: ExNtfy.Message.t()
Same as publish_file/3, but returns the message directly and raises
ExNtfy.Error on failure.
@spec publish_raw(String.t(), binary(), keyword()) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Publishes a raw body to POST /<topic> — the body is sent byte-identical
and every publish option becomes its canonical X- header (non-ASCII values
RFC 2047-encoded).
Use this for templating (reference §1.6), where the body is an arbitrary JSON payload rather than the ntfy publish schema, or for bodies that must not be re-encoded.
@spec trigger( String.t(), keyword() ) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Webhook-style publish: GET /<topic>/trigger with every option in the query
string and no body. An empty :message defaults to "triggered" server-side.
@spec update(String.t(), String.t(), String.t() | nil, keyword()) :: {:ok, ExNtfy.Message.t()} | {:error, ExNtfy.Error.t()}
Updates a delivered notification by publishing again with the same sequence
ID (reference §1.7) — sugar over publish/3 with :sequence_id set.
Two idioms: reuse the id of a previously returned message as the sequence
ID for follow-ups, or pick your own sequence ID up front on the first
publish (sequence_id: option) and keep using it.