Skip to content

Attachments

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.

AttachFile — local files reference server-side paths. The simplest usage is passing a path string directly — Apprise converts it to a file:// URL internally:

# Single path — Apprise wraps it in AttachFile automatically
apobj.notify(body="See log", attach="/var/log/syslog")
# Multiple paths
apobj.notify(
body="Build artifacts",
attach=["/var/log/syslog", "/tmp/report.pdf"],
)

Direct constructor — use AttachFile when you need to override the filename or MIME type presented to the upstream service without renaming the file on disk:

from apprise.attachment import AttachFile
# Override the filename and MIME type the service sees
a = AttachFile(
"/var/log/app.log",
name="2026-03-20-app.log", # presented name, not a rename
mimetype="text/plain",
)
apobj.notify(body="Nightly log", attach=a)

Key Behaviours:

  • Content is validated in-place — it is not copied or moved.
  • name= and mimetype= override what the plugin sends upstream; the file on disk is unchanged.
  • Size limits are enforced via max_file_size (defaults to 1 GB).
  • attachment.path returns the absolute path to the file.
  • attachment.open() returns a readable file handle and is the correct way to stream content in plugins.

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.open() to get a file-like object — this works correctly for all attachment types, including AttachMemory which has no path on disk:

filename = attachment.name
mimetype = attachment.mimetype
fh = attachment.open()
try:
files = {"file": (filename, fh, mimetype)}
r = requests.post(url, files=files, ...)
finally:
fh.close()

Or using the context manager form:

with attachment as f:
files = {"file": (attachment.name, f, attachment.mimetype)}
r = requests.post(url, files=files, ...)

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