Skip to main content

Handler: outbound.apprise_notify

Generic notification delivery via Apprise. One handler for every push destination — the channel is described entirely by the row’s config.apprise_url, so adding Slack / ntfy / Pushover / email / SMS is a row insert, not a new handler module.

Payload

The handler reduces any of these to a single body string (first non-empty of content / text / body / message). The notification title is always empty, matching the plain-content posts the legacy Discord/Telegram handlers produced.

The apprise_url template

config.apprise_url is an Apprise URL with optional placeholders, substituted at dispatch time:
  • {secret} → the value resolved from secret_key_ref (an app_settings key, decrypted live each call)
  • {<config-key>} → any other key in the row’s config (e.g. {chat_id})
The secret stays in app_settings; only the non-secret template lives on the row. Rotation works (the secret is resolved per call) and the token never lands in the config column.

Seeded rows

Rowsecret_key_refconfig.apprise_urlother config
discord_opsdiscord_ops_webhook_url{secret}
telegram_opstelegram_bot_tokentgram://{secret}/{chat_id}/chat_id
Apprise accepts the native Discord webhook URL (https://discord.com/api/webhooks/<id>/<token>) directly, so the Discord secret is passed straight through.

Caller usage

Most internal callers go through the operator-notify shim, which routes by urgency:

Operator runbook

Adding a new channel (no code)

Example: an ntfy push channel.
  1. Store the secret (if the service needs one): poindexter settings set ntfy_token '<token>' --secret
  2. Insert a row:
  3. Call it: await deliver("ntfy_ops", "hello from poindexter", db_service=db, site_config=sc)
The Apprise wiki lists the URL format for each of the ~100 supported services. config.apprise_url is the only field that changes per service.

Rotating a secret

Takes effect on the next call — the secret is resolved live each dispatch.

Disabling

deliver() then raises OutboundWebhookError; notify_operator treats that as “operator turned this channel off” and moves on.

Discord rendering note

If a Discord destination renders as an embed rather than plain content, append ?format=text to its apprise_url:

Response contract

  • Success → the dispatcher returns {"ok": true, "name": "<row>", "delivered": true}
  • Failure (malformed URL, delivery failure, missing apprise_url, unresolved {secret}, or unknown placeholder) → the handler raises; the dispatcher records last_error on the row and re-raises.

Telegram streaming note

Only the fire-and-forget notify path uses Apprise. The pipeline edit-in-place streaming path (services/pipeline_streaming.py) still uses the Bot API helpers retained in services/integrations/handlers/outbound_telegram.py (send_telegram_message / edit_telegram_message) — Apprise cannot edit a message after sending.
  • Framework overview: Integrations
  • Sibling handler: outbound.vercel_isr
  • Operator-notify shim: services.integrations.operator_notify.notify_operator