Message Format Conversion
When adding a plugin, answer two questions:
- What format did the caller provide?
- What format does the service accept?
The caller describes the first value with body_format. Your plugin describes the second with notify_format. Apprise converts between them before it calls send().
Inside send(), body_format is the format of the body you received. It is no longer the caller’s original value. For example, one HTML message can arrive as plain text in one plugin, Markdown in another, and HTML in a third.
Finding the Input Format
Section titled “Finding the Input Format”The input format depends on how the notification was sent:
| Interface | Default when no format is declared |
|---|---|
| CLI | text: the CLI always sets an input format; override with --input-format |
| Python Library | none: unless body_format is passed to notify() or preset in AppriseAsset |
| Apprise API | none: formatting is entirely opt-in; set format in the request payload to change this |
See Formatting for the user-facing behavior.
Conversion Flow
Section titled “Conversion Flow”For each plugin, Apprise:
- Reads the caller’s input format.
- Chooses one format supported by the plugin.
- Converts the body when the input and plugin formats differ.
- Calls the plugin with the converted body and its selected format.
If the caller did not declare a source format, Apprise leaves the body unchanged. In that case, body_passthrough is True. Otherwise it is False.
Apprise.notify(body=..., body_format=HTML) | | target = plugin.resolve_format(HTML) | convert_between(HTML, target, body) | (skipped when the caller's body_format is None) vplugin.notify( body=<already converted to the resolved format>, body_format=target, # selected format; never None here body_passthrough=False, # the caller declared HTML above)When a service has no separate title field (title_maxlen <= 0), Apprise adds the title to the body before delivery. Otherwise, the title is passed separately.
Reading body_format and body_passthrough in send()
Section titled “Reading body_format and body_passthrough in send()”Accept body_format and body_passthrough in send(). Apprise has already calculated both values:
def send(self, body, title="", notify_type=..., body_format=None, body_passthrough=None, **kwargs): if body_format == NotifyFormat.MARKDOWN: ...You normally do not need to call resolve_format() yourself. Direct calls to plugin.notify() or plugin.send() are the exception: they do not run Apprise’s conversion step. If you call a plugin directly, prepare the body in the format that plugin expects.
When No Input Format Is Declared
Section titled “When No Input Format Is Declared”When the Python Library caller’s body_format is None, Apprise skips conversion entirely:
apobj.notify(body="<b>Hello</b>")The plugin receives <b>Hello</b> unchanged and body_passthrough=True. Apprise still applies its normal title and overflow handling.
The CLI does not produce this state. It always provides body_format=TEXT unless the caller passes --input-format to override it.
Choosing the Plugin Format
Section titled “Choosing the Plugin Format”NotifyBase uses plain text by default. Set notify_format when the service accepts another format:
from apprise import NotifyFormatfrom apprise.plugins.base import NotifyBase
class NotifyExample(NotifyBase): notify_format = NotifyFormat.MARKDOWNMost plugins support one format. In that case, self.notify_format is all you need. If a service supports several formats, use body_format inside send() to see which one Apprise selected.
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, so the Markdown characters arrive unchanged. If the service interprets those characters, escape them inside send().
Set notify_format = NotifyFormat.MARKDOWN when the service accepts Markdown.
Source body_format | Body delivered to the plugin |
|---|---|
TEXT | CommonMark-significant characters (*, _, `, #, etc.) are backslash-escaped so the text reads literally |
MARKDOWN | Unchanged Markdown |
HTML | Converted to standard Markdown |
None | Body unchanged; no conversion performed |
HTML and plain text are converted to standard Markdown. Services such as Telegram, Slack, WhatsApp, and Google Chat use their own Markdown-like syntax. Their plugins must perform one more service-specific conversion; see Service-Specific Adaptation.
Set notify_format = NotifyFormat.HTML when the 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 must make any other changes required by the service, such as removing unsupported tags or placing the HTML in an API request.
Multi-Format Plugins
Section titled “Multi-Format Plugins”If a service accepts several formats, declare them in order. The first entry is the default. A tuple is the clearest choice for a class setting:
from apprise import NotifyFormatfrom apprise.plugins.base import NotifyBase
class NotifyExample(NotifyBase): # The first entry is the default -- used when nothing else picks # a different one. notify_format = (NotifyFormat.HTML, NotifyFormat.MARKDOWN)Read body_format in send() to learn which format Apprise selected:
def send(self, body, title="", notify_type=..., body_format=None, body_passthrough=None, **kwargs): if body_format == NotifyFormat.MARKDOWN: ... else: ...You may also define send_text(), send_html(), or send_markdown() instead of branching inside one send() method. Apprise calls the method that matches the selected format and falls back to send() when no matching method exists:
class NotifyExample(NotifyBase): notify_format = (NotifyFormat.HTML, NotifyFormat.MARKDOWN)
def send_html(self, body, title="", notify_type=..., index=0, total=1, **kwargs): ...
def send_markdown(self, body, title="", notify_type=..., index=0, total=1, **kwargs): ...
# send() can still be used as a fallback.Most plugins only need one format and can ignore this section. Relay plugins such as Custom JSON/XML/Form and Apprise API may support all three. A relay should forward a format field only when body_passthrough is False; otherwise it would choose a format on the caller’s behalf.
Tooling that introspects Apprise().details() (a URL builder, for example) can read a service’s declared formats from args.format.supported — see The format Argument for the full schema shape.
Optional ?format= URL Override
Section titled “Optional ?format= URL Override”Without an override, Apprise uses notify_format, or its first entry for a multi-format plugin:
example://host/tokenThe format= query parameter changes the format for one plugin URL:
example://host/token?format=textexample://host/token?format=markdownexample://host/token?format=htmlFor a single-format plugin, the override becomes that instance’s format. For a multi-format plugin, the requested value must appear in notify_format. Unsupported values log a warning and Apprise uses its normal selection rules instead.
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 do not need to track this separately.
What the Plugin Receives
Section titled “What the Plugin Receives”By the time send() runs, Apprise has prepared these values:
body: converted and sized according tooverflow_mode. Apprise may add closing Markdown characters when it splits formatted text.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 ofbody, such asNotifyFormat.HTML; it is neverNonehere.body_passthrough:Truewhen no input format was declared and automatic format conversion was skipped. Normal title and overflow handling still applies.
Use body_format to choose the correct delivery path. Use body_passthrough only when your plugin must distinguish untouched input from content prepared by Apprise.
Service-Specific Adaptation in Plugins
Section titled “Service-Specific Adaptation in Plugins”Apprise converts content to standard Markdown. Some services use a different syntax:
- 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
Keep those rules inside the service plugin. Shared conversion code should not know anything about Slack, Telegram, or another specific service.
Override dialect_convert() to translate standard Markdown into the service’s syntax before send() runs:
class NotifyExample(NotifyBase): def dialect_convert(self, body, body_format=None, *args, **kwargs): """Translate one prepared piece into the service's markup.""" if body_format != NotifyFormat.MARKDOWN: # This service only has a dialect of its own for Markdown; # everything else passes through unchanged. return body return self._commonmark_to_example_dialect(body)Apprise skips this method when body_passthrough is True. Return unsupported formats unchanged. Do not save state or send a notification here. Apprise may call the method more than once while preparing a message.
Escaping for a service can make text longer. Apprise checks the size again after dialect_convert() for SPLIT and TRUNCATE, so the plugin does not need to split the body itself.
Chat services without heading syntax can reuse commonmark_headings_to_bold(). It turns # Alert into **Alert** before dialect conversion and leaves inline or fenced code unchanged.
Labeled-link destination scans share one message-wide work budget. After it is spent, each later destination receives a short bounded check; longer malformed links remain literal so repeated bad input cannot cause excessive scanning.
Some services send more than one representation in the same request. Email and Brevo can send HTML together with a plain-text alternative. Use body_format to identify the body you received, then create the other representation if the service needs it:
from ..conversion import convert_between
def send(self, body, title="", notify_type=..., body_format=None, **kwargs): # body is already in the resolved format (HTML or TEXT). if body_format == NotifyFormat.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} else: html_body = convert_between(NotifyFormat.TEXT, NotifyFormat.HTML, body) payload = {"html": html_body, "text": body} # ... send payload ...This is a service decision. Apprise selects the primary format; the plugin decides whether its payload also needs another representation.
Overflow Handling before Service Adaptation
Section titled “Overflow Handling before Service Adaptation”Set overflow_mode to choose what happens when content exceeds body_maxlen or title_maxlen. Apprise handles it before send() runs.
| Mode | send() calls | Body guarantee |
|---|---|---|
UPSTREAM (default) | One | Unmodified; may exceed body_maxlen |
TRUNCATE | One | Best effort near body_maxlen |
SPLIT | One per chunk | Best effort near body_maxlen |
from apprise.common import OverflowMode
class NotifyExample(NotifyBase): body_maxlen = 4096 title_maxlen = 255 overflow_mode = OverflowMode.SPLITApprise tries to keep declared Markdown readable when a split crosses formatting. Service-specific escaping can add characters afterward, so SPLIT and TRUNCATE check the size again after dialect_convert(). UPSTREAM does not enforce the limit; the service handles the full body.
The ?overflow= URL parameter lets users change the mode for one plugin URL. The class default should match what the service accepts.
Knowing Your Position Among Multiple send() Calls
Section titled “Knowing Your Position Among Multiple send() Calls”One notify() call may produce several send() calls. Accept index and total when your plugin needs to know whether a piece is first or last:
def send(self, body, title="", notify_type=..., index=0, total=1, **kwargs): is_first = index == 0 is_last = index == total - 1 # ... rest of send() ...An unsplit message uses index=0 and total=1. Most plugins do not need special handling.
Attachment placement does not use index or total. Apprise passes attachments to one send() call only and uses the first call by default. To place them on the last call instead, override _attachment_send_index():
def _attachment_send_index(self, total: int) -> int: # Place the attachment on the last call instead of the first. return total - 1Telegram uses this override when an attachment must follow all text pieces.
Plugin Boundaries
Section titled “Plugin Boundaries”apprise.conversionhandles general text, HTML, and Markdown conversion.- Service-specific syntax, escaping, and payload rules belong in the plugin.
- Override
dialect_convert()for a service-specific format. Do not split the converted text in the plugin; Apprise already handles that. - Removing a plugin should remove all behavior for that service without requiring changes to shared conversion code.
Questions or Feedback?
Technical Issues
Having trouble with the code? Open an issue on GitHub: