Decrypt incoming media
Version 2.0.0 brings incoming WhatsApp attachments directly into Home Assistant
automations. Images, voice notes, audio, videos, documents, and stickers can be
downloaded, decrypted, and saved automatically. Each attachment gets its own
temporary file and an authenticated download link in new_whatsapp_message.
This opens up workflows such as extracting text from photos and PDFs, transcribing voice messages, classifying documents, or passing a received image to an automation. Pass the saved file to your chosen Home Assistant integration or service for OCR, transcription, conversion, or AI processing.
Enable incoming media
- Update both the WhatsApp add-on and HACS integration to 2.0.0 or newer.
- Restart Home Assistant after updating the integration.
- Enable Download incoming media in the add-on configuration, then save and restart the add-on.
The option is off by default, so existing installations do not start saving private attachments unexpectedly. Enabling it applies to incoming media across all configured WhatsApp sessions and chats. Automation filters determine which files to process afterwards, not which files are downloaded.
download_media: true
media_retention_hours: 24
media_max_file_mb: 64
media_max_storage_mb: 1024
| Option | Default | Meaning |
|---|---|---|
download_media |
false |
Automatically save incoming attachments. |
media_retention_hours |
24 |
Keep each completed file for 1-720 hours. |
media_max_file_mb |
64 |
Maximum attachment size, 1-1024 MiB. |
media_max_storage_mb |
1024 |
Attachment-content budget, 1-102400 MiB; at least the maximum file size. |
Home Assistant OS shares /media with the add-on. Files are stored under
/media/whatsapp/<unique-id>/file.<extension>. The integration serves those
same files. A custom container deployment must mount the same storage at
/media in both containers. Installing only the add-on provides the files and
event metadata; the 2.0.0 integration provides authenticated HTTP access.
Event data
The event keeps its original clientId, type, key, and message fields.
A successful media download adds the following object (example values):
media:
status: ready
id: "12345678-1234-4123-8123-123456789abc"
filename: "file.pdf"
original_filename: "report.pdf"
mime_type: "application/pdf"
size: 48219
local_path: "/media/whatsapp/12345678-1234-4123-8123-123456789abc/file.pdf"
url: "/api/whatsapp/media/12345678-1234-4123-8123-123456789abc"
expires_at: "2026-09-21T12:00:00.000Z"
original_filename is present only if WhatsApp supplied a filename. size
is the actual decrypted size in bytes. Unknown MIME types use file.bin;
the original name is metadata and never determines the storage path.
The event fires after the complete file has been saved and its SHA-256
checked against the message. Two messages with the same filename get
different directories. Text messages and outgoing whatsapp_message_sent
events are unchanged and have no new media object. Wrapped attachments are
detected, including document captions and view-once wrappers when the linked
session receives downloadable media. Quoted attachments are not downloaded
merely because a text message quotes them.
Media events wait for their own download. Other messages continue immediately, so a later text message or smaller attachment can arrive in Home Assistant before an earlier large attachment. Existing duplicate-message checks run before downloading. The feature does not backfill history or create a second "media ready" event.
Use the file in an automation
This example forwards ready attachments to a processing script. Create
script.process_whatsapp_attachment separately using the OCR, transcription,
or document integration of your choice. It is a user-defined script, not an
action supplied by this project.
- alias: Process a received WhatsApp attachment
triggers:
- trigger: event
event_type: new_whatsapp_message
event_data:
clientId: default
conditions:
- condition: template
value_template: >-
{{ trigger.event.data.get('media', {}).get('status') == 'ready' }}
actions:
- action: script.process_whatsapp_attachment
data:
file_path: "{{ trigger.event.data.media.local_path }}"
file_url: "{{ trigger.event.data.media.url }}"
mime_type: "{{ trigger.event.data.media.mime_type }}"
expires_at: "{{ trigger.event.data.media.expires_at }}"
mode: parallel
max: 10
Use local_path for a processor running inside Home Assistant that accepts
local files. Some integrations require the folder in
allowlist_external_dirs; follow that integration's file-access requirements.
Another add-on needs the same /media mount to read that path.
Use url for authenticated HTTP access. It is relative to the Home Assistant
base URL and requires a Home Assistant login session or an
Authorization: Bearer <Home Assistant access token> header. The add-on's
optional API token does not authenticate this URL. The endpoint supports
documents as well as images, audio, and video and serves them as downloads.
A cloud OCR or AI provider generally cannot fetch a private Home Assistant URL. Its Home Assistant integration should read the local file and upload the bytes, or otherwise handle authenticated fetching. This feature does not publish files anonymously or create signed public links.
Retention and failures
Each file's retention starts when its download completes. The expiry is saved
with the file and survives restarts. Changing the retention setting affects
new downloads; existing files keep their advertised expiry. Cleanup runs at
startup and every five minutes, including while download_media is disabled.
HTTP access ends at expiry immediately, even before cleanup removes the file.
Cleanup cannot run while the add-on is stopped; it catches up on startup.
To keep a file permanently or process it after expiry, copy it into a separate
folder before expires_at. Do not use /media/whatsapp for user-managed files.
The storage budget counts attachment bytes; filesystem metadata consumes some
additional disk space. Each active download reserves the maximum file size,
so new downloads may be rejected before the remaining free budget reaches zero.
There is also a limit of 10,000 retained or reserved attachments to bound
metadata and directory growth even for very small files.
Reducing the budget never deletes an unexpired attachment to make room.
Two downloads can run at once, with up to 32 pending attachments including active downloads. Each has a 60-second deadline, including queue time. Unavailable media can request a reupload through the linked WhatsApp session, but that depends on another device still having the file.
If downloading fails, the incoming message event still fires:
media:
status: error
error: timeout
| Error | Meaning |
|---|---|
timeout |
The download, reupload request, or queue wait exceeded its deadline. |
queue_full |
The pending-attachment limit was reached. |
file_too_large |
Decrypted bytes exceeded the per-file limit. |
storage_full |
The storage budget, retained-file limit, or available disk space was exhausted. |
storage_unavailable |
The shared media directory could not be initialized. |
invalid_metadata |
Required media metadata, including its checksum, was missing or invalid. |
checksum_mismatch |
The decrypted file did not match the message's checksum. |
download_failed |
Downloading or saving failed for another reason. |
stopped |
The add-on stopped before the file was ready. |
Error events do not include a file path or download link. Incomplete files are removed, and interrupted-download leftovers are cleaned on startup. There is no separate automatic retry action after an error event.
For a missing link, check that both components are updated, the integration is
loaded, and both containers see the same /media directory. HTTP 401 means
authentication is required, 404 means the file or integration is unavailable,
and 410 means the attachment has expired.
Saved files, original filenames, and automation traces may contain private information. Retention removes the add-on's saved copies, not copies kept by downstream processors, backups, or Home Assistant traces.