Overview
This guide covers uploading and downloading media (images, videos, audio, documents, stickers) with automatic encryption and decryption.Downloading Media
From Message Objects
Download media directly from message types that implementDownloadable:
Streaming Download
For large files, use streaming to avoid memory overhead:When the HTTP client supports streaming (like the default
UreqHttpClient), downloads use ~40KB of memory regardless of file size (8KB buffer + decryption state). If the HTTP client doesn’t support streaming, download_to_writer automatically falls back to a buffered download — the file is fetched into memory first, then decrypted and written. This fallback ensures the method works with any HttpClient implementation.From Raw Parameters
Download media when you have the raw encryption parameters:Streaming from Raw Parameters
Thumbnails
Thumbnails are not generated by the library — you provide them as raw JPEG bytes when constructing media messages. WhatsApp clients use these thumbnails as low-resolution previews while the full media is downloading.Setting thumbnails on media messages
Set thejpeg_thumbnail field on image, video, or document messages:
Thumbnail fields by message type
| Message type | Inline thumbnail field | Server-hosted thumbnail fields |
|---|---|---|
ImageMessage | jpeg_thumbnail | thumbnail_direct_path, thumbnail_sha256, thumbnail_enc_sha256 |
VideoMessage | jpeg_thumbnail | thumbnail_direct_path, thumbnail_sha256, thumbnail_enc_sha256 |
DocumentMessage | jpeg_thumbnail | thumbnail_direct_path, thumbnail_sha256, thumbnail_enc_sha256 |
StickerMessage | png_thumbnail | — |
Inline thumbnails (
jpeg_thumbnail / png_thumbnail) are embedded directly in the encrypted message payload. Server-hosted thumbnail fields (thumbnail_direct_path, etc.) reference a separately uploaded thumbnail that clients can download independently. For most use cases, inline thumbnails are sufficient.Reusing thumbnails from incoming messages
When re-uploading media (for example, after processing or transcoding), you can clone the thumbnail from the original message:Downloading link thumbnails
TheLinkThumbnail variant in MediaType supports downloading thumbnails for URL previews in ExtendedTextMessage:
Forwarding media via CDN reuse
When you receive a media message (image, video, audio, document, sticker), you can “forward” it by reusing the original CDN fields directly — no download or re-upload needed. This is instant regardless of file size because no media bytes are transferred. The technique works because every media message contains all the CDN metadata (url, direct_path, media_key, file_sha256, etc.) needed for recipients to fetch the blob. You simply clone those fields into a new wa::Message and call send_message:
When CDN reuse won’t work
CDN URLs expire after some time. If the original media URL has expired, recipients won’t be able to download it. In that case, use media reupload to request the server to provide a fresh download path, or fall back to downloading and re-uploading the media.Uploading Media
Basic Upload
Using Upload Response in Messages
Sending media as an album
You can group multiple images and/or videos into an album. See Sending Messages - Album messages for a full walkthrough of the parent/child album pattern usingAlbumMessage and wrap_as_album_child.
Resumable uploads
For files 5 MiB or larger, the client automatically checks whether a previous upload of the same file exists on the CDN before uploading the full payload. This avoids re-uploading data that the server already has. The resume check flow works as follows:- The client sends a
?resume=1probe to the CDN for each host - If the server responds with complete, the existing URL is returned immediately — no upload occurs
- If the server responds with a byte offset, only the remaining bytes are uploaded using the
file_offsetquery parameter - If the server responds with not found (or the check fails), a full upload proceeds
Resumable upload detection is fully transparent. You call
client.upload() the same way regardless of file size. The resume check is non-fatal — if it fails for any reason, the client falls back to a full upload.Media Types
Available Media Types
Media type properties
ProductCatalogImage is the only unencrypted media type. It uploads to /product/image instead of the /mms/ prefix used by other types. This matches WhatsApp Web’s behavior where CreateMediaKeys.js skips encryption for product catalog images.Encryption Details
Upload Encryption
Media is automatically encrypted before upload:- Generate random 32-byte
media_key - Derive encryption keys using HKDF-SHA256:
- IV: 16 bytes
- Cipher key: 32 bytes
- MAC key: 32 bytes
- Encrypt with AES-256-CBC
- Append HMAC-SHA256 MAC (10 bytes)
- Compute SHA256 hashes of plaintext and ciphertext
Chunk-based encryption with MediaEncryptor
MediaEncryptor provides a chunk-based API for encrypting media incrementally. Unlike encrypt_media_streaming, it doesn’t require a sync Read source — you can feed plaintext in arbitrarily sized chunks, making it suitable for async streams and network sources:
Streaming encryption from a reader
For syncRead sources, encrypt_media_streaming wraps MediaEncryptor and reads in 8KB chunks:
Download Decryption
Decryption is automatic when downloading:- Fetch encrypted data from WhatsApp CDN
- Verify MAC (last 10 bytes)
- Decrypt with AES-256-CBC
- Verify plaintext SHA256
- Return decrypted data
Error Handling
Download Errors
Upload Errors
Advanced Usage
Custom Media Processing
Streaming with Progress
Automatic retry and failover
Bothdownload and upload methods automatically retry when WhatsApp’s CDN returns certain HTTP errors. The client handles three categories of errors:
Auth errors (401/403): The client invalidates the cached MediaConn, fetches fresh credentials from WhatsApp’s servers, and retries the operation once with new auth tokens.
Media not found (404/410): When a download URL has expired or the media has been moved, the CDN returns 404 or 410. The client treats this the same as an auth error — it invalidates the cached media connection, re-derives download URLs with fresh credentials and hosts, and retries once. This matches WhatsApp Web’s MediaNotFoundError handling.
Other errors (e.g., 500): The client tries the next available CDN host without refreshing credentials. Media connections include multiple hosts sorted by priority (primary hosts first, then fallback hosts), and the client iterates through them sequentially.
This retry behavior is fully transparent — no changes are needed in your code. Both
download and upload handle auth refresh, URL re-derivation, and host failover automatically, with a maximum of one credential refresh per operation.download_to_writer), the writer is automatically seeked back to position 0 before each retry so partial writes don’t corrupt the output.
Additional retry logic
For transient errors beyond auth failures, you can implement your own retry wrapper:Media connection
Automatic management
The client automatically manages media connection tokens, including refreshing them when they expire or when the CDN rejects them with 401/403/404/410:Host types and failover order
Each media connection includes multiple CDN hosts with ahost_type field indicating whether the host is "primary" or "fallback". The client sorts hosts with primary hosts first, so uploads and downloads always try the fastest hosts before falling back to alternatives. Each host may also include a fallback_hostname for additional resilience.
Token expiration
Media connections have two TTL values:ttl— how long the route/host information is validauth_ttl— how long the auth token specifically is valid (may be shorter thanttl)
Internal refresh
Media connection refresh and invalidation are managed internally by the client. When the CDN rejects a request (401/403/404/410), the client automatically invalidates the cached connection and fetches fresh credentials before retrying. You don’t need to manage this manually.Best Practices
// ❌ Bad: Loads entire file into memory
let data = client.download(video.as_ref()).await?;
std::fs::write("video.mp4", data)?;
// ✅ Good: Constant memory usage (with streaming HTTP client)
let file = File::create("video.mp4")?;
let _file = client.download_to_writer(video.as_ref(), file).await?;
download_to_writer works with any HTTP client. With streaming-capable clients (like UreqHttpClient), memory usage is constant ~40KB. With non-streaming clients, it falls back to buffered downloads automatically.if let Some(img) = &message.image_message {
if let Some(mimetype) = &img.mimetype {
match mimetype.as_str() {
"image/jpeg" | "image/png" => {
// Process image
}
_ => {
eprintln!("Unsupported image type: {}", mimetype);
}
}
}
}
if let Some(doc) = &message.document_message {
let filename = doc.file_name.as_deref().unwrap_or("unknown");
let mimetype = doc.mimetype.as_deref().unwrap_or("application/octet-stream");
let size = doc.file_length.unwrap_or(0);
println!("Document: {} ({}, {} bytes)", filename, mimetype, size);
}
use image::GenericImageView;
let img = image::open("photo.jpg")?;
let (width, height) = img.dimensions();
let message = wa::Message {
image_message: Some(Box::new(wa::message::ImageMessage {
url: Some(upload.url),
direct_path: Some(upload.direct_path),
media_key: Some(upload.media_key_vec()),
file_sha256: Some(upload.file_sha256_vec()),
file_enc_sha256: Some(upload.file_enc_sha256_vec()),
file_length: Some(upload.file_length),
media_key_timestamp: Some(upload.media_key_timestamp),
mimetype: Some("image/jpeg".to_string()),
width: Some(width),
height: Some(height),
..Default::default()
})),
..Default::default()
};
Next Steps
- Sending Messages - Send media with captions and quotes
- Receiving Messages - Handle incoming media
- Custom Backends - Implement custom HTTP clients for media operations