Postulo's notifications through Apprise: Telegram, ntfy, Discord, Matrix, Gotify, Pushover, Signal, email and well over a hundred more, each named by one URL. https://source.tiagoagueda.com/postulo/postulo
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tiago Águeda 34abf8713d
All checks were successful
CI / Security audit (push) Successful in 1m1s
CI / test (push) Successful in 9m4s
Run CI through the shared plugin action (#39)
2026-11-12 10:24:06 +00:00
.forgejo Run CI through the shared plugin action (#39) 2026-11-12 10:24:06 +00:00
src/postulo_apprise chore(l10n): update Urdu translation 2026-10-30 23:16:55 +00:00
tests Run CI through the shared plugin action (#39) 2026-11-12 10:24:06 +00:00
.gitignore Take translations from Weblate only, and move to the Postulo that checks it 2026-10-10 12:55:18 +02:00
LICENSE Notifications from Postulo through Apprise 2026-09-06 11:59:42 +02:00
pyproject.toml Version 0.5.0, tested against Postulo 0.5.0 (#37) 2026-11-01 15:01:42 +00:00
README.md Hold the release gate to the languages the core release selects (#33) 2026-10-23 20:24:04 +00:00
uv.lock Take oauthlib 4.0.1 for CVE-2026-49272 (#38) 2026-11-12 07:31:00 +00:00

postulo-apprise

Notifications from Postulo through Apprise: Telegram, ntfy, Discord, Matrix, Gotify, Pushover, Signal, email and well over a hundred other services, each named by one URL.

Postulo tells you when a reminder falls due, when a posting arrives through the capture API, and when an application has gone quiet. Out of the box it says so by email. This plugin lets it say so wherever you already look: add a connection, paste the URL of the service, tick the events you want, and that is the whole set-up.

It is the first plugin built outside Postulo's core, and deliberately small. Postulo's notifier interface draws the form, stores the URLs encrypted, offers the Test button and the switch per event; this package turns a notification into an Apprise call.

Install

Into the environment of a running Postulo:

uv pip install git+https://source.tiagoagueda.com/postulo/postulo-apprise.git

Restart Postulo. In a container, bake it into the image instead — the two ways are on Installing Postulo:

docker compose -f docker/compose.yml build \
  --build-arg POSTULO_EXTRA_PACKAGES="git+https://source.tiagoagueda.com/postulo/postulo-apprise.git"

Use

Under Settings → Connections → Add a connection, choose Apprise. The one field takes Apprise URLs, one per line:

tgram://123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11/987654
ntfy://ntfy.example.org/job-hunt
mailto://me:app-password@example.org

Every service Apprise speaks, and the form of its URL, is in Apprise's own list. Press Test: one message goes to every line. Tick the events you want under the URLs.

Some things worth knowing:

  • The URLs are secrets. A Telegram URL carries the bot token; a mail URL carries the password. The whole field is stored encrypted and is never shown back. The connections list shows only the name of each service and, for one on a server of your own, that server's host and port (Telegram · ntfy ntfy.example.org): never a path, a topic, a user name or a token, because a webhook's path and an ntfy topic are credentials Apprise does not know how to mask. To change the list, type it again.
  • A typo is caught at the form. Each line must be a URL Apprise understands; the form says which line is not, and nothing is saved until every line is.
  • Your own servers are subject to the instance's destination policy. An ntfy, Gotify or Matrix server, or a json://, xml:// or form:// webhook, on a private address is refused unless the operator set POSTULO_CONNECTIONS_ALLOW_PRIVATE=true, exactly as for every other connection. That includes a name with no dot in it — json://redis:6379/, form://paperless:8000/, a Docker service or a machine on the LAN — which is a real destination like any other and was let through unchecked before 0.3.0. Which URLs name a server of yours is read from Apprise's own description of each service; hosted ones (Telegram, Discord, Pushover…) keep a token where the host would be, name no address of yours, and are not affected. What is checked is the address a service really opens: a mailto:// line's smtp= server rather than the sender's domain, a Firebase keyfile= given as an address. ntfy://my-topic, which uses ntfy.sh, and the API mailers (Mailgun, SparkPost, SMTP2GO, SendPulse) have no server of yours and are not looked up.
  • Redirects are never followed. A public address that answers with a redirect to a private one would otherwise lead the server past the destination policy, so a line that sets redirect=yes is refused and a server that redirects http to https needs the https form of the scheme (jsons://, ntfys://).
  • Lines that act on the server itself are refused: syslog://, the desktop notifiers (dbus, kde, qt, glib, gnome, macosx, windows), evolution:// and blink1://, and a key file named by a path on the server (keyfile=, subfile=).
  • Waits are bounded. A line's rto= and cto= can lower Apprise's socket timeouts but not raise them past ten seconds, one send is given up after 45 seconds, and one connection holds at most twenty URLs, so a slow endpoint cannot hold up other people's reminders.
  • Words are text. A title or a body is sent as plain text and escaped for the services that send HTML (Telegram, email), so a captured page's title cannot arrive as markup.
  • Several connections or several lines? Either. One connection with three URLs sends every chosen event to all three; two connections let you send reminders to the phone and captures to a chat.
  • Email stays built in. Apprise has mailto://, and Postulo's own Email notifier is not going anywhere: the core works with no plugin installed.

Translations

French and Portuguese ship with the package, in src/postulo_apprise/locale/. Postulo never translates a plugin's strings, and nobody translates this one in a commit: every translation is made in Weblate, at translate.tiagoagueda.com, which drafts new strings by machine and sends what speakers save back as a pull request from its weblate branch. A commit runs uv run postulo-messages extract, which leaves an empty slot for a new string in every catalogue, and uv run postulo-messages guard in CI refuses one that writes a translation (postulo/postulo#706). The .mo files are not committed: uv run postulo-messages compile writes them, before the tests and before uv build. uv run pytest -m release holds the languages the paired core release selects complete before a release.

A language code here is a BCP 47 tag in its canonical form, as everywhere in Postulo: the language in lower case, a script with a capital, a region in capitals (fr-FR, pt-BR, sr-Cyrl). The directories under locale/ are the one exception: fr_FR and sr_Cyrl are gettext's names for a catalogue, not codes, and postulo-messages derives them from the tags, so nobody types one.

Develop

uv sync        # brings in Postulo itself from its repository, for the tests
uv run pytest
uv run ruff check .

The tests run the plugin inside a real Postulo, through its registry and its connection form, with Apprise's delivery replaced by a recorder. No message leaves the machine.

Licence

Copyright (C) 2026 Postulo contributors. AGPL-3.0-or-later, like Postulo. Apprise is BSD-2-Clause.