Skip to content

Message Format Conversion

When adding a plugin, answer two questions:

  1. What format did the caller provide?
  2. 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.

The input format depends on how the notification was sent:

InterfaceDefault when no format is declared
CLItext: the CLI always sets an input format; override with --input-format
Python Librarynone: unless body_format is passed to notify() or preset in AppriseAsset
Apprise APInone: formatting is entirely opt-in; set format in the request payload to change this

See Formatting for the user-facing behavior.

For each plugin, Apprise:

  1. Reads the caller’s input format.
  2. Chooses one format supported by the plugin.
  3. Converts the body when the input and plugin formats differ.
  4. 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)
v
plugin.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 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.

NotifyBase uses plain text by default. Set notify_format when the service accepts another format:

from apprise import NotifyFormat
from apprise.plugins.base import NotifyBase
class NotifyExample(NotifyBase):
notify_format = NotifyFormat.MARKDOWN

Most 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_formatBody delivered to the plugin
TEXTUnchanged text
MARKDOWNUnchanged Markdown source (no destructive conversion)
HTMLTags removed; block structure flattened to plain text
NoneBody 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().

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 NotifyFormat
from 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.

Without an override, Apprise uses notify_format, or its first entry for a multi-format plugin:

example://host/token

The format= query parameter changes the format for one plugin URL:

example://host/token?format=text
example://host/token?format=markdown
example://host/token?format=html

For 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.

By the time send() runs, Apprise has prepared these values:

  • body: converted and sized according to overflow_mode. Apprise may add closing Markdown characters when it splits formatted text.
  • title: truncated to title_maxlen characters. When title_maxlen <= 0, the title has been folded into body by the base class and arrives as an empty string.
  • body_format: the format of body, such as NotifyFormat.HTML; it is never None here.
  • body_passthrough: True when 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.

Apprise converts content to standard Markdown. Some services use a different syntax:

  • Slack uses mrkdwn instead 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.

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.

Modesend() callsBody guarantee
UPSTREAM (default)OneUnmodified; may exceed body_maxlen
TRUNCATEOneBest effort near body_maxlen
SPLITOne per chunkBest effort near body_maxlen
from apprise.common import OverflowMode
class NotifyExample(NotifyBase):
body_maxlen = 4096
title_maxlen = 255
overflow_mode = OverflowMode.SPLIT

Apprise 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 - 1

Telegram uses this override when an attachment must follow all text pieces.

  • apprise.conversion handles 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?

Documentation

Notice a typo or an error?

Technical Issues

Having trouble with the code? Open an issue on GitHub:

Made with love from Canada