Documentation Index
Fetch the complete documentation index at: https://mintlify.com/jlucaso1/whatsapp-rust/llms.txt
Use this file to discover all available pages before exploring further.
Overview
This guide covers uploading and downloading media (images, videos, audio, documents, stickers) with automatic encryption and decryption.
From Message Objects
Download media directly from message types that implement Downloadable:
use wacore::download::Downloadable;
use whatsapp_rust::client::Client;
match event {
Event::Message(message, info) => {
// Image
if let Some(img) = &message.image_message {
let data = client.download(img.as_ref()).await?;
std::fs::write("image.jpg", data)?;
}
// Video
if let Some(video) = &message.video_message {
let data = client.download(video.as_ref()).await?;
std::fs::write("video.mp4", data)?;
}
// Audio
if let Some(audio) = &message.audio_message {
let data = client.download(audio.as_ref()).await?;
let ext = if audio.ptt() { "ogg" } else { "mp3" };
std::fs::write(format!("audio.{}", ext), data)?;
}
// Document
if let Some(doc) = &message.document_message {
let data = client.download(doc.as_ref()).await?;
let filename = doc.file_name.as_deref().unwrap_or("document");
std::fs::write(filename, data)?;
}
// Sticker
if let Some(sticker) = &message.sticker_message {
let data = client.download(sticker.as_ref()).await?;
std::fs::write("sticker.webp", data)?;
}
}
_ => {}
}
See \1
Streaming Download
For large files, use streaming to avoid memory overhead:
use std::fs::File;
if let Some(video) = &message.video_message {
let file = File::create("video.mp4")?;
let file = client.download_to_writer(video.as_ref(), file).await?;
println!("Video downloaded with constant memory usage!");
}
See \1
Streaming downloads use ~40KB of memory regardless of file size (8KB buffer + decryption state). The entire HTTP download, decryption, and file write happen in a single blocking thread for optimal performance.
From Raw Parameters
Download media when you have the raw encryption parameters:
use wacore::download::MediaType;
let data = client.download_from_params(
"/v/t62.1234-5/...", // direct_path
&media_key, // media_key (32 bytes)
&file_sha256, // file_sha256
&file_enc_sha256, // file_enc_sha256
12345, // file_length
MediaType::Image, // media_type
).await?;
std::fs::write("downloaded.jpg", data)?;
See \1
Streaming from Raw Parameters
let file = File::create("large_video.mp4")?;
let file = client.download_from_params_to_writer(
direct_path,
&media_key,
&file_sha256,
&file_enc_sha256,
file_length,
MediaType::Video,
file,
).await?;
See \1
Basic Upload
use wacore::download::MediaType;
use whatsapp_rust::upload::UploadResponse;
// Read file data
let data = std::fs::read("photo.jpg")?;
// Upload with automatic encryption
let upload: UploadResponse = client.upload(data, MediaType::Image).await?;
println!("URL: {}", upload.url);
println!("Direct path: {}", upload.direct_path);
println!("File SHA256: {:?}", upload.file_sha256);
See \1
Using Upload Response in Messages
use waproto::whatsapp as wa;
let upload = client.upload(image_data, MediaType::Image).await?;
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),
file_sha256: Some(upload.file_sha256),
file_enc_sha256: Some(upload.file_enc_sha256),
file_length: Some(upload.file_length),
mimetype: Some("image/jpeg".to_string()),
caption: Some("Check out this image!".to_string()),
width: Some(1920),
height: Some(1080),
..Default::default()
})),
..Default::default()
};
let message_id = client.send_message(to, message).await?;
pub enum MediaType {
Image,
Video,
Audio,
Document,
Sticker,
Thumbnail,
PTT, // Push-to-Talk (voice messages)
}
use wacore::download::MediaType;
// Get MMS type for upload URLs
let mms_type = MediaType::Image.mms_type();
// Returns: "image", "video", "audio", "document"
// Get HKDF info for encryption
let info = MediaType::Video.hkdf_info();
// Returns: b"WhatsApp Video Keys"
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
// Encryption happens automatically in upload()
let data = std::fs::read("file.pdf")?;
let upload = client.upload(data, MediaType::Document).await?;
// The UploadResponse contains all encryption metadata
assert_eq!(upload.media_key.len(), 32);
assert_eq!(upload.file_sha256.len(), 32);
assert_eq!(upload.file_enc_sha256.len(), 32);
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
// Decryption happens automatically in download()
let data = client.download(image_message.as_ref()).await?;
// `data` is already decrypted and verified
Never skip SHA256 verification! The library automatically verifies both encrypted and decrypted hashes to ensure data integrity.
Error Handling
Download Errors
use anyhow::Result;
async fn safe_download(
client: &Client,
downloadable: &dyn Downloadable,
) -> Result<Vec<u8>> {
match client.download(downloadable).await {
Ok(data) => {
println!("✅ Downloaded {} bytes", data.len());
Ok(data)
}
Err(e) => {
if e.to_string().contains("invalid mac") {
eprintln!("❌ Media corrupted (MAC mismatch)");
} else if e.to_string().contains("status") {
eprintln!("❌ Download failed (HTTP error)");
} else {
eprintln!("❌ Download error: {:?}", e);
}
Err(e)
}
}
}
Upload Errors
async fn safe_upload(
client: &Client,
data: Vec<u8>,
media_type: MediaType,
) -> Result<UploadResponse> {
match client.upload(data, media_type).await {
Ok(upload) => {
println!("✅ Uploaded to: {}", upload.url);
Ok(upload)
}
Err(e) => {
eprintln!("❌ Upload failed: {:?}", e);
Err(e)
}
}
}
Advanced Usage
use image::DynamicImage;
// Download and process image
if let Some(img) = &message.image_message {
let data = client.download(img.as_ref()).await?;
// Load with image crate
let image = image::load_from_memory(&data)?;
// Resize
let thumbnail = image.resize(200, 200, image::imageops::FilterType::Lanczos3);
// Save
thumbnail.save("thumbnail.jpg")?;
}
Streaming with Progress
use std::io::{Write, Seek};
use std::sync::{Arc, Mutex};
struct ProgressWriter<W> {
inner: W,
total: Arc<Mutex<usize>>,
}
impl<W: Write> Write for ProgressWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let n = self.inner.write(buf)?;
*self.total.lock().unwrap() += n;
println!("Downloaded {} bytes", *self.total.lock().unwrap());
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
impl<W: Seek> Seek for ProgressWriter<W> {
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
self.inner.seek(pos)
}
}
let file = File::create("video.mp4")?;
let progress_writer = ProgressWriter {
inner: file,
total: Arc::new(Mutex::new(0)),
};
let _file = client.download_to_writer(video.as_ref(), progress_writer).await?;
Retry Logic
use tokio::time::{sleep, Duration};
async fn download_with_retry(
client: &Client,
downloadable: &dyn Downloadable,
max_retries: u32,
) -> Result<Vec<u8>> {
let mut attempt = 0;
loop {
match client.download(downloadable).await {
Ok(data) => return Ok(data),
Err(e) if attempt < max_retries => {
attempt += 1;
eprintln!("Retry {}/{}: {:?}", attempt, max_retries, e);
sleep(Duration::from_secs(2u64.pow(attempt))).await;
}
Err(e) => return Err(e),
}
}
}
Automatic Refresh
The client automatically manages media connection tokens:
// Automatically refreshed when expired
let upload = client.upload(data, MediaType::Image).await?;
let download = client.download(downloadable).await?;
// No manual token management needed!
Manual Refresh
For advanced use cases:
// Force refresh media connection
let media_conn = client.refresh_media_conn(true).await?;
println!("Auth token: {}", media_conn.auth);
println!("Hosts: {:?}", media_conn.hosts);
Best Practices
Use Streaming for Large Files
// ❌ Bad: Loads entire file into memory
let data = client.download(video.as_ref()).await?;
std::fs::write("video.mp4", data)?;
// ✅ Good: Constant memory usage
let file = File::create("video.mp4")?;
let _file = client.download_to_writer(video.as_ref(), file).await?;
Check mimetype before processing:
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);
}
}
}
}
Media messages may have optional fields:
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);
}
For images and videos, include dimensions:
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),
file_sha256: Some(upload.file_sha256),
file_enc_sha256: Some(upload.file_enc_sha256),
file_length: Some(upload.file_length),
mimetype: Some("image/jpeg".to_string()),
width: Some(width),
height: Some(height),
..Default::default()
})),
..Default::default()
};
Next Steps