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
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 fromsecret_key_ref(anapp_settingskey, decrypted live each call){<config-key>}→ any other key in the row’sconfig(e.g.{chat_id})
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
| Row | secret_key_ref | config.apprise_url | other config |
|---|---|---|---|
discord_ops | discord_ops_webhook_url | {secret} | — |
telegram_ops | telegram_bot_token | tgram://{secret}/{chat_id}/ | chat_id |
https://discord.com/api/webhooks/<id>/<token>) directly, so the Discord secret is passed straight through.
Caller usage
Operator runbook
Adding a new channel (no code)
Example: an ntfy push channel.- Store the secret (if the service needs one):
poindexter settings set ntfy_token '<token>' --secret - Insert a row:
- Call it:
await deliver("ntfy_ops", "hello from poindexter", db_service=db, site_config=sc)
config.apprise_url is the only field that changes per service.
Rotating a secret
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 recordslast_erroron 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.
Related
- Framework overview: Integrations
- Sibling handler:
outbound.vercel_isr - Operator-notify shim:
services.integrations.operator_notify.notify_operator