Message Format Pipeline
When writing a plugin, you declare notify_format to tell Apprise what format your upstream service expects. When a notification arrives, Apprise resolves the caller’s declared format (body_format), converts the body if necessary, and hands it — already in notify_format — to your plugin. By the time send() runs, the body is pre-formatted. Your code does not need to re-convert it.
| Value | Meaning | Set by |
|---|---|---|
body_format | Format of the incoming message | notify(), AppriseAsset, CLI, or API caller |
notify_format | Format the plugin delivers to its service | Plugin class; optionally overridden by ?format= |
Both values are independent. The same HTML source can become plain text for one plugin, Markdown for another, and stay HTML for a third — all in a single notify() call.
How body_format Is Resolved
Section titled “How body_format Is Resolved”The source format is not always None. It depends on which interface triggers the notification:
| Interface | Default when no format is declared |
|---|---|
| CLI | text — the CLI always sets a source format; override with --input-format |
| Python Library | none — unless body_format is passed to notify() or preset in AppriseAsset |
| Apprise API | none — unless format is included in the request payload |
See Formatting for the end-user perspective on these defaults and how they affect delivery across mixed destinations.
Conversion Flow
Section titled “Conversion Flow”For every matched plugin, Apprise:
- Resolves
body_formatfrom thenotify()call. When the Python Library or Apprise API are used without a format, this isNone. When the CLI is used without--input-format, this isTEXT. Ifbody_formatis not provided tonotify(), it falls back toAppriseAsset.body_format, which defaults toNone. - Reads
notify_formatfrom the plugin instance (class default or?format=override). - When
body_formatis notNone, callsconvert_between(body_format, notify_format, body)and replaces the body with the result. Results are cached per unique destination format so the same conversion is not repeated for multiple plugins that share a format. - Calls
plugin.notify(body=<converted>, body_format=<original_format>). The body that arrives at the plugin is already innotify_format. The pre-conversion content is not preserved or passed separately.body_formatcarries only the format identifier (e.g.NotifyFormat.HTML) — not a copy of the original body text.
Apprise.notify(body=..., body_format=HTML) | | convert_between(HTML, plugin.notify_format, body) | (skipped when body_format is None) vplugin.notify( body=<already converted to notify_format>, body_format=HTML, # the original format tag -- not the original body)Title conversion follows the same path but only when the plugin has no native title field (title_maxlen <= 0). When title_maxlen > 0, the title is passed unchanged and the plugin is responsible for any further formatting.
When No Source Format Is Declared
Section titled “When No Source Format Is Declared”When body_format is None (Python Library or API call without a format), Apprise skips conversion entirely:
apobj.notify(body="<b>Hello</b>")The plugin receives "<b>Hello</b>" unchanged and body_format=None. Declaring a notify_format on the plugin class does not alter this — conversion only runs when the caller declares a source format.
The CLI does not produce this state. It always provides body_format=TEXT unless the caller passes --input-format to override it.
Setting the Plugin Destination
Section titled “Setting the Plugin Destination”NotifyBase defaults to plain text. Set notify_format when the upstream service accepts a richer format:
from apprise import NotifyFormatfrom apprise.plugins.base import NotifyBase
class NotifyExample(NotifyBase): notify_format = NotifyFormat.MARKDOWNThe effective value is always accessible as self.notify_format.
NotifyFormat.TEXT is the default when a plugin does not declare a format.
Source body_format | Body delivered to the plugin |
|---|---|
TEXT | Unchanged text |
MARKDOWN | Unchanged Markdown source (no destructive conversion) |
HTML | Tags removed; block structure flattened to plain text |
None | Body unchanged; no conversion performed |
Markdown-to-text has no conversion step today; the Markdown syntax arrives as-is. If the upstream service still interprets Markdown characters on its text delivery path, escape them inside send().
Set notify_format = NotifyFormat.MARKDOWN when the upstream service accepts Markdown.
Source body_format | Body delivered to the plugin |
|---|---|
TEXT | Unchanged text (plain text is already valid Markdown) |
MARKDOWN | Unchanged Markdown |
HTML | Converted to standard Markdown |
None | Body unchanged; no conversion performed |
HTML conversion always produces standard Markdown. Services that use their own custom Markdown syntax (Telegram MarkdownV2, Slack mrkdwn, WhatsApp, Google Chat) must translate that standard Markdown into their own format inside the plugin — Apprise’s built-in conversion step is intentionally generic.
Set notify_format = NotifyFormat.HTML when the upstream service accepts HTML.
Source body_format | Body delivered to the plugin |
|---|---|
TEXT | HTML-escaped text with line breaks converted to <br> |
MARKDOWN | Rendered HTML |
HTML | Unchanged HTML |
None | Body unchanged; no conversion performed |
The plugin is responsible for any further sanitization, tag allowlisting, or payload wrapping that its upstream API requires.
Optional ?format= URL Override
Section titled “Optional ?format= URL Override”Without an override, the plugin class’s notify_format is used:
example://host/tokenA format= query parameter changes the destination for that plugin instance only:
example://host/token?format=textexample://host/token?format=markdownexample://host/token?format=htmlThe effective destination is always self.notify_format regardless of whether it came from the class or a URL override. Automatic conversion still requires a non-None source body_format.
When plugin logic must distinguish a URL override from the class default, inspect kwargs before calling the base __init__:
def __init__(self, **kwargs): # Capture whether the caller explicitly set a format override. self.format_overridden = "format" in kwargs super().__init__(**kwargs)Most plugins only need self.notify_format and do not need to track the override separately.
What the Plugin Receives
Section titled “What the Plugin Receives”By the time send() runs, conversion and sizing are already done. What your plugin receives:
body— already converted toself.notify_formatand sized according tooverflow_mode. WithSPLIT, eachsend()call receives one chunk of at mostbody_maxlencharacters. WithTRUNCATE, the body is capped atbody_maxlen. WithUPSTREAM(the default), the body arrives unmodified and may exceed the declared limit.title— truncated totitle_maxlencharacters. Whentitle_maxlen <= 0, the title has been folded intobodyby the base class and arrives as an empty string.body_format— the format the content started as (e.g.NotifyFormat.HTML); not the original body text itself
Plugins read body_format to decide whether any extra work is needed. If body_format is HTML and notify_format is MARKDOWN, the body was converted from HTML to standard Markdown by Apprise — and the plugin may need to translate that Markdown into the service’s own syntax. If body_format was already MARKDOWN when the caller sent it, the body is the caller’s own Markdown and can usually be passed through unchanged.
Service-Specific Adaptation in Plugins
Section titled “Service-Specific Adaptation in Plugins”Apprise converts the body to standard Markdown. What the plugin does with that Markdown is entirely its own responsibility. Several real-world services require further work beyond what Apprise’s built-in conversion provides:
- Slack uses
mrkdwninstead of standard Markdown - Telegram uses MarkdownV2, its own custom syntax
- WhatsApp (via Evolution) and Google Chat each have their own format
- Email can include both HTML and plain text in the same message
Keeping the conversion step generic means a plugin can be removed and all service-specific behavior disappears with it. The examples below show the most practical patterns.
Handle dialect translation directly inside send(). The body has already been converted by the time send() runs — check the two format values to decide whether any extra work is needed. If notify_format is MARKDOWN and body_format is HTML, Apprise converted the body from HTML to standard Markdown before calling your plugin, and you can now translate it to the service’s own syntax. Slack uses this approach:
def send(self, body, title="", notify_type=..., body_format=None, **kwargs): if ( self.notify_format == NotifyFormat.MARKDOWN and body_format == NotifyFormat.HTML ): # Apprise converted this from HTML to standard Markdown. # Translate it now to the dialect this service expects. body = self._commonmark_to_service_dialect(body)
# ... rest of send() ...For a complete real-world example, see the Slack plugin source.
Some services accept more than one format in the same request. Email is the clearest example: the plugin sets notify_format = NotifyFormat.HTML, so the body arrives as HTML. Because many email clients also display a plain-text version, the plugin derives one from the body it already has:
from ..conversion import convert_between
def send(self, body, title="", notify_type=..., body_format=None, **kwargs): # body is already in self.notify_format (HTML). # Build a plain-text version for clients that cannot render HTML. text_body = convert_between(NotifyFormat.HTML, NotifyFormat.TEXT, body)
payload = { "html": body, "text": text_body, } # ... send payload ...The same pattern applies to any service that accepts multiple representations. Pushover and Brevo both use it.
Overflow Handling after Adaptation
Section titled “Overflow Handling after Adaptation”Set overflow_mode on the class to declare how the base class handles content that exceeds body_maxlen or title_maxlen. Each send() call receives content that has already been sized accordingly — do not re-split or re-truncate inside send():
| Mode | send() calls | Body guarantee |
|---|---|---|
UPSTREAM (default) | One | Unmodified; may exceed body_maxlen |
TRUNCATE | One | Capped at body_maxlen |
SPLIT | One per chunk | Each chunk ≤ body_maxlen |
from apprise.common import OverflowMode
class NotifyExample(NotifyBase): body_maxlen = 4096 title_maxlen = 255 overflow_mode = OverflowMode.SPLITThe ?overflow= URL parameter lets users override the mode per instance, but the class default should match what the upstream API actually accepts.
Plugin Boundaries
Section titled “Plugin Boundaries”apprise/conversion.pyconverts between text, HTML, and standard Markdown. It must not contain syntax rules for any specific service.- Service-specific syntax, escaping rules, payload fields, and API modes belong exclusively in the plugin file.
- Removing a plugin file must remove all behavior for that service with no changes required to the shared conversion code.
- Generic utilities with no service-specific knowledge may live in
conversion.py. Current exports:build_backtick_run_index,find_unescaped_run,commonmark_escape_link_url,commonmark_scan_angle_dest,commonmark_emphasis_run,commonmark_force_close_spans,commonmark_prepend_title.
Implementation Checklist
Section titled “Implementation Checklist”The sections above cover the full picture. This checklist summarizes the key decisions for quick reference:
- Declare
notify_formaton the class to tell Apprise what format to convert to before your plugin runs. Omit it to accept plain text (TEXT). - Do not re-convert the body in
send(). It is already inself.notify_format. Usebody_formatto check what the content originally was, not to trigger another conversion pass. - Check
body_formatto decide whether service-specific formatting is needed. If your service uses its own Markdown syntax (Slack mrkdwn, Telegram MarkdownV2, WhatsApp), apply that translation only whenbody_formatisHTML— meaning the body was originally HTML and Apprise converted it to standard Markdown. - For multi-format payloads (such as email with separate HTML and text parts), call
convert_between()insidesend()to derive additional representations from the already-converted body. - Treat
?format=as an optional per-instance override. The effective destination is alwaysself.notify_formatregardless of its source. Most plugins do not need to track whether the value came from the class or a URL parameter. - Set
overflow_mode,body_maxlen, andtitle_maxlenon the class to declare the limits and splitting strategy for your upstream service. Eachsend()call receives content already sized to those limits — no additional changes to the payload is required insidesend(). - Test the full matrix:
body_format=None, each supported source format, URL?format=overrides, and split or truncated messages.
Questions or Feedback?
Technical Issues
Having trouble with the code? Open an issue on GitHub: