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

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