Skip to content

Attachments

Attachments are first-class in Apprise. When you pass attach= into Apprise.notify(), Apprise normalizes every entry into an AppriseAttachment object (internally backed by AttachBase implementations). Plugins that opt in (by setting attachment_support = True) receive a list of attachment objects through the attach argument of send().

From a plugin author perspective, an attachment is a small, uniform API that lets you:

  • Validate availability (if not attachment: ...)
  • Read from disk (attachment.path, attachment.open(), attachment.chunk())
  • Get metadata (attachment.name, attachment.mimetype, len(attachment))
  • Convert to base64 (attachment.base64()), for APIs that require inline payloads
  • Control privacy in logs (attachment.url(privacy=True))

When a plugin declares attachment support:

class NotifyFooBar(NotifyBase):
# Declare awareness to the Apprise library that this service supports
# attachments
attachment_support = True
def send(self, body, title="", notify_type=NotifyType.INFO, attach=None, **kwargs):
# Add 'attach' into your send() call as it will be populated when one
# or more attachments exist.
if attach:
for a in attach:
# ...
pass

attach is either:

  • None or an empty list when no attachments were provided
  • A list of AttachBase objects (for example: AttachFile, AttachHTTP, AttachMemory)

A quick rule of thumb:

  • if not a: means the attachment is not currently usable.
  • a.path triggers a download or validation step when needed.
  • len(a) returns the attachment size in bytes, when known.

Apprise supports multiple attachment sources. These are all normalized to the same API surface.

Local files reference server-side paths.

apobj.notify(
body="See log",
attach="/var/log/syslog",
)

You can use a set, tuple or list to pass in multiple attachments:

apobj.notify(
body="See log",
attach=["/var/log/syslog", "/var/log/messages"],
)

Key Behaviours:

  • The content is validated in-place. It is not copied elsewhere.
  • Size limits are enforced using max_file_size (defaults to 1 GB).

Attachment handling is governed by content location rules:

ValueDescription
LOCALAllows local files, memory attachments, and hosted content.
HOSTEDIntended for hosted services. Local file and memory attachments are rejected.
INACCESSIBLEAttachments are disabled entirely. All downloads fail and attachments evaluate to False.
ValueDescription
mimeAttachment URLs support a small set of common query parameters.
Forces the attachment MIME type, bypassing detection. This is useful when the upstream API chooses behaviour based on MIME type.
nameForces the filename presented to the plugin. This does not rename local files, it only changes the metadata (attachment.name) and what the plugin might send upstream.

Always verify attachments are available before using them:

for attachment in attach:
if not attachment:
self.logger.error(
"Could not access attachment %s.",
attachment.url(privacy=True),
)
return False

An attachment can fail because it is missing, exceeds size limits, is inaccessible for the current runtime location, or could not be downloaded.

Many services require multipart uploads. Use attachment.path, attachment.name, and attachment.mimetype:

path = attachment.path
filename = attachment.name
mimetype = attachment.mimetype
with open(path, "rb") as f:
files = {"file": (filename, f, mimetype)}
...

If you only need the raw bytes, attachment.open() can be used instead of opening the path directly.

All attachment types support Base64 export. Some APIs require base64 encoded attachments. Use attachment.base64():

encoded = attachment.base64() # returns a str by default
payload["base64_attachments"].append(encoded)
  • base64() returns a string
  • base64(encoding=None) returns raw bytes

If the attachment cannot be read, base64() raises an Apprise exception. Catch it and fail gracefully.

This is commonly used by APIs that do not support multipart uploads.

If you must avoid reading a full file into memory, use attachment.chunk():

for chunk in attachment.chunk(size=5 * 1024 * 1024):
# upload / write chunk
...

Plugins do not usually need to manually delete downloaded temporary files. Attachment objects manage their own cleanup through invalidate() and destructors.

If you hold on to attachment objects beyond send(), you are responsible for understanding the lifecycle. In general, treat attachments as ephemeral.

  • Size limits are enforced by AttachBase.max_file_size. If your service has a smaller limit, enforce it in your plugin using len(attachment) and fail early.
  • Attachment support is opt-in per plugin using attachment_support = True. If your service cannot accept files, leave this disabled.
  • Use attachment.url(privacy=True) in logs. This ensures any embedded secrets are redacted.

Plugins should validate attachment size and count early and handle inaccessible attachments gracefully

The core project includes common patterns you can copy.

  • Uploading attachments as files (multipart) with MIME-based selection.
  • Converting attachments to base64 for JSON APIs.
  • Iterating attachments and reporting partial failure.

See the Telegram and Signal API plugins for real-world implementations of both patterns.

Questions or Feedback?

Documentation

Notice a typo or an error? Report it or contribute a fix .

Technical Issues

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

Made with love from Canada