Skip to main content

download

Download and decrypt media from a message.
Only use download when you need the plaintext bytes (processing, transcoding, re-upload). To forward existing media unchanged, reuse the original message’s CDN fields directly — no download required. See media forwarding via CDN reuse.
pub async fn download(
    &self,
    downloadable: &dyn Downloadable
) -> Result<Vec<u8>, anyhow::Error>
downloadable
&dyn Downloadable
required
Any message type that implements the Downloadable trait. Includes:
  • ImageMessage
  • VideoMessage
  • AudioMessage
  • DocumentMessage
  • StickerMessage
  • ExternalBlobReference (app state)
  • HistorySyncNotification
bytes
Vec<u8>
Decrypted media bytes. For encrypted media (E2EE), automatically decrypts using AES-256-CBC and verifies HMAC-SHA256. For plaintext media (newsletters/channels), validates SHA-256 hash.

Example: download image

use waproto::whatsapp as wa;

// From a received message
if let Some(image_msg) = message.image_message {
    let image_bytes = client.download(image_msg.as_ref()).await?;
    std::fs::write("downloaded_image.jpg", image_bytes)?;
}

Example: download with error handling

match client.download(downloadable).await {
    Ok(data) => {
        println!("Downloaded {} bytes", data.len());
        // Process data...
    }
    Err(e) => {
        eprintln!("Download failed: {}", e);
        // Fallback logic...
    }
}

Automatic retry and URL re-derivation

All download methods handle three categories of CDN errors automatically:
  • Auth errors (401/403): The client invalidates the cached media connection, fetches fresh credentials, and retries the download once.
  • Media not found (404/410): When a media URL has expired or the file has been relocated, the CDN returns 404 or 410. The client treats this the same as an auth error — it invalidates the cached 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. Hosts are tried in priority order (primary first, then fallback).
For streaming downloads, the writer is seeked back to position 0 before retrying so partial writes are overwritten.

download_to_writer

Download media to a writer using streaming when available, with automatic buffered fallback. This is the method to use for downloading straight to a file — pass a File or BufWriter<File>. When the HTTP client supports streaming (supports_streaming() returns true), the entire HTTP download, decryption, and file write happen in a single blocking thread with ~40KB memory usage regardless of file size. When streaming is not available, the method automatically falls back to a buffered download — fetching the full response into memory, then decrypting and writing to the writer. This ensures download_to_writer works with any HttpClient implementation.
pub async fn download_to_writer<W: Write + Seek + Send + 'static>(
    &self,
    downloadable: &dyn Downloadable,
    writer: W,
) -> Result<W, anyhow::Error>
downloadable
&dyn Downloadable
required
Message containing downloadable media
writer
W: Write + Seek + Send + 'static
required
Writer for streaming output. Must be Send + ‘static for use in blocking task.
writer
W
Returns the writer after successful download, seeked back to position 0.

Example: streaming download

use std::fs::File;

let file = File::create("large_video.mp4")?;
let file = client.download_to_writer(video_msg.as_ref(), file).await?;
// File is seeked back to start and can be reused
When using an HTTP client that supports streaming (like the default UreqHttpClient), memory usage is constant ~40KB (8KB read buffer + decryption state). HTTP clients that don’t support streaming fall back to buffered downloads, which load the full file into memory before writing.

download_from_params

Download and decrypt media from raw CDN parameters without the original message. The parameters are bundled into a DownloadParams struct.
pub async fn download_from_params(
    &self,
    params: &DownloadParams,
) -> Result<Vec<u8>, anyhow::Error>
params
&DownloadParams
required
The CDN/crypto fields needed to fetch and decrypt the media. Build one with DownloadParams::encrypted.
bytes
Vec<u8>
Decrypted media bytes

Example: download from stored metadata

use wacore::download::MediaType;
use whatsapp_rust::download::DownloadParams;

// If you stored media metadata separately
let params = DownloadParams::encrypted(
    "/v/t62.7118-24/12345_67890",
    &media_key,
    &file_sha256,
    &file_enc_sha256,
    file_length,
    MediaType::Image,
);

let image_bytes = client.download_from_params(&params).await?;
DownloadParams implements Downloadable, so you can also pass it straight to download: client.download(&params).await?.

download_from_params_to_writer

Streaming variant of download_from_params that writes to a writer.
pub async fn download_from_params_to_writer<W: Write + Seek + Send + 'static>(
    &self,
    params: &DownloadParams,
    writer: W,
) -> Result<W, anyhow::Error>
params
&DownloadParams
required
The CDN/crypto fields needed to fetch and decrypt the media. See DownloadParams.
writer
W
required
Writer for streaming output
writer
W
Returns the writer after successful download

DownloadParams

A Downloadable built from raw CDN fields, for re-downloading media without the original message.
pub struct DownloadParams {
    pub direct_path: String,
    pub media_key: Option<Vec<u8>>,
    pub file_sha256: Vec<u8>,
    pub file_enc_sha256: Option<Vec<u8>>,
    pub file_length: u64,
    pub media_type: MediaType,
}
FieldTypeDescription
direct_pathStringWhatsApp CDN path (e.g. /v/t62.7118-24/12345_67890)
media_keyOption<Vec<u8>>32-byte media key. None for plaintext (newsletter/channel) media
file_sha256Vec<u8>SHA-256 of the decrypted file
file_enc_sha256Option<Vec<u8>>SHA-256 of the encrypted file (encrypted media only)
file_lengthu64Original file size in bytes
media_typeMediaTypeImage, Video, Audio, Document, Sticker, …

DownloadParams::encrypted

Convenience constructor for encrypted (E2EE) media — fills media_key and file_enc_sha256 as Some(...).
pub fn encrypted(
    direct_path: impl Into<String>,
    media_key: &[u8],
    file_sha256: &[u8],
    file_enc_sha256: &[u8],
    file_length: u64,
    media_type: MediaType,
) -> Self
DownloadParams implements Downloadable, so it works with download, download_to_writer, download_from_params, and download_from_params_to_writer.

fetch_sticker_pack

Fetch first-party sticker pack metadata (and the per-sticker download handles) from the WhatsApp CDN.
pub async fn fetch_sticker_pack(
    &self,
    pack_id: &str,
    locale: &str,
) -> Result<wacore::sticker_pack::StickerPack>
pack_id
&str
required
The first-party sticker pack ID (typically extracted from a received sticker_pack_message).
locale
&str
required
BCP-47 locale tag for localized name / publisher strings. Pass "en" to match whatsmeow’s default.
pack
StickerPack
Pack metadata plus a Vec<StickerPackItem> of individual stickers. Each StickerPackItem implements Downloadable, so you can pass it straight to client.download(...).
Under the hood the client GETs https://static.whatsapp.net/sticker?lottie=1&cat=sticker_pack_data&id={pack_id}&lg={locale}, parses the JSON envelope, and constructs the StickerPack. The endpoint is unauthenticated — the call works whether or not you are paired.
use whatsapp_rust::Client;

let pack = client.fetch_sticker_pack("3F9Z2…", "en").await?;
println!("Pack '{}' by {}", pack.name, pack.publisher);
for sticker in &pack.stickers {
    let bytes = client.download(sticker).await?;
    std::fs::write(format!("{}.webp", sticker.file_name), bytes)?;
}

StickerPack

pub struct StickerPack {
    pub sticker_pack_id: String,
    pub name: String,
    pub publisher: String,
    pub description: Option<String>,
    pub tray_image_file_name: Option<String>,
    pub stickers: Vec<StickerPackItem>,
    // additional CDN metadata: animated, tray-icon colors, …
}

StickerPackItem

pub struct StickerPackItem {
    pub file_name: String,
    pub emojis: Vec<String>,
    pub accessibility_text: Option<String>,
    pub is_animated: bool,
    // plus all Downloadable fields:
    //   direct_path, media_key, file_enc_sha256, file_sha256, file_length
}

impl Downloadable for StickerPackItem { /* … */ }
The CDN response is a JSON envelope, not a protobuf message — StickerPack / StickerPackItem live in wacore::sticker_pack and are independent of waproto::whatsapp::StickerPackMessage (which represents the inline pack-bubble in a chat). The struct mirrors whatsmeow’s FirstPartyStickerPack.

Downloadable Trait

The Downloadable trait provides a generic interface for downloading media from any message type.
pub trait Downloadable: Sync + Send {
    fn direct_path(&self) -> Option<&str>;
    fn media_key(&self) -> Option<&[u8]>;
    fn file_enc_sha256(&self) -> Option<&[u8]>;
    fn file_sha256(&self) -> Option<&[u8]>;
    fn file_length(&self) -> Option<u64>;
    fn app_info(&self) -> MediaType;
    fn static_url(&self) -> Option<&str> { None }
    fn is_encrypted(&self) -> bool { self.media_key().is_some() }
}
direct_path
fn() -> Option<&str>
WhatsApp CDN path for the media file
media_key
fn() -> Option<&[u8]>
32-byte encryption key. Present for E2EE media, None for plaintext (newsletter/channel) media.
file_enc_sha256
fn() -> Option<&[u8]>
SHA-256 hash of the encrypted file. Used for encrypted media validation.
file_sha256
fn() -> Option<&[u8]>
SHA-256 hash of the decrypted file. Used for plaintext media validation.
file_length
fn() -> Option<u64>
Original file size in bytes
app_info
fn() -> MediaType
Media type for HKDF key derivation (Image, Video, Audio, Document, etc.)
static_url
fn() -> Option<&str>
default:"None"
Static CDN URL for direct download. Present on newsletter/channel media, bypasses host construction.
is_encrypted
fn() -> bool
default:"media_key().is_some()"
Returns true if media is encrypted (has media_key), false for plaintext media

Built-in Implementations

The Downloadable trait is automatically implemented for:
  • wa::message::ImageMessage
  • wa::message::VideoMessage
  • wa::message::AudioMessage
  • wa::message::DocumentMessage
  • wa::message::StickerMessage
  • wa::ExternalBlobReference (app state)
  • wa::message::HistorySyncNotification

MediaType

Media type enum for encryption/decryption.
pub enum MediaType {
    Image,
    Video,
    Audio,
    Document,
    History,
    AppState,
    Sticker,
    StickerPack,
    StickerPackThumbnail,
    LinkThumbnail,
    ProductCatalogImage,
}
Each media type has specific HKDF info strings used for key derivation:
  • Image / Sticker"WhatsApp Image Keys"
  • Video"WhatsApp Video Keys"
  • Audio"WhatsApp Audio Keys"
  • Document"WhatsApp Document Keys"
  • History"WhatsApp History Keys"
  • AppState"WhatsApp App State Keys"
  • StickerPack"WhatsApp Sticker Pack Keys"
  • StickerPackThumbnail"WhatsApp Sticker Pack Thumbnail Keys"
  • LinkThumbnail"WhatsApp Link Thumbnail Keys"

MediaType methods

MethodReturn typeDescription
app_info()&'static strHKDF info string for key derivation
mms_type()&'static strMedia type string for MMS path construction
upload_path()&'static strURL path prefix for upload/download
is_encrypted()boolWhether this media type uses E2E encryption

Upload paths

Media typeUpload path
Image / Sticker/mms/image
Video/mms/video
Audio/mms/audio
Document/mms/document
History/mms/md-msg-hist
AppState/mms/md-app-state
StickerPack/mms/sticker-pack
StickerPackThumbnail/mms/thumbnail-sticker-pack
LinkThumbnail/mms/thumbnail-link
ProductCatalogImage/product/image
ProductCatalogImage is unencryptedis_encrypted() returns false. This matches WhatsApp Web’s behavior where CreateMediaKeys.js skips encryption for product catalog images. Its upload path is /product/image (not under the /mms/ prefix like other media types).

Media Decryption

WhatsApp uses different handling for encrypted (E2EE) and plaintext media:

Encrypted Media (E2EE)

  1. Download encrypted bytes from CDN
  2. Verify HMAC-SHA256 (last 10 bytes)
  3. Decrypt using AES-256-CBC with keys derived from media_key via HKDF
  4. Return decrypted plaintext
The media_key is expanded using HKDF-SHA256 to derive:
  • 16-byte IV
  • 32-byte cipher key
  • 32-byte MAC key

Plaintext media (newsletter/channel)

  1. Download plaintext bytes from CDN (often via static_url)
  2. Verify SHA-256 hash matches file_sha256
  3. Return plaintext (no decryption needed)
Newsletter and channel media is not encrypted. The library automatically detects this when media_key is absent and switches to plaintext validation.

Example: detect media type

if downloadable.is_encrypted() {
    println!("E2EE media - will decrypt");
} else {
    println!("Plaintext media - no decryption needed");
}

DownloadUtils

Low-level static methods for media decryption and validation. These are re-exported from wacore::download and useful when you need fine-grained control over the download pipeline.
use whatsapp_rust::download::DownloadUtils;

Key methods

MethodDescription
verify_and_decrypt(encrypted_payload, media_key, media_type)Verifies HMAC and decrypts AES-256-CBC in one call
decrypt_stream(reader, media_key, media_type)Streaming decryption from a reader (convenience wrapper around decrypt_stream_to_writer)
decrypt_stream_to_writer(reader, media_key, media_type, writer)Streaming decryption directly into a writer with constant memory usage
validate_plaintext_sha256(data, expected_sha256)Validates SHA-256 hash of plaintext media (in-memory)
copy_and_validate_plaintext_to_writer(reader, expected_sha256, writer)Streams plaintext media to a writer while validating SHA-256 hash
prepare_download_requests(downloadable, media_conn)Builds CDN request URLs with host failover
get_media_keys(media_key, app_info)Derives IV, cipher key, and MAC key via HKDF