> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# download

> Download and decrypt media from WhatsApp messages

## download

Download and decrypt media from a message.

<Tip>
  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](/guides/media-handling#forwarding-media-via-cdn-reuse).
</Tip>

```rust theme={null}
pub async fn download(
    &self,
    downloadable: &dyn Downloadable
) -> Result<Vec<u8>, anyhow::Error>
```

<ParamField path="downloadable" type="&dyn Downloadable" required>
  Any message type that implements the `Downloadable` trait. Includes:

  * `ImageMessage`
  * `VideoMessage`
  * `AudioMessage`
  * `DocumentMessage`
  * `StickerMessage`
  * `ExternalBlobReference` (app state)
  * `HistorySyncNotification`
</ParamField>

<ResponseField name="bytes" type="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.
</ResponseField>

### Example: download image

```rust theme={null}
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

```rust theme={null}
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 (`download_to_writer`), the writer is seeked back to position 0 before retrying so partial writes are overwritten. For in-memory downloads (`download()`), each retry gets a fresh buffer rather than reusing the previous one, so a failed host that wrote a longer body can't leave a stale tail behind a shorter successful retry.

***

## 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.

```rust theme={null}
pub async fn download_to_writer<W: Write + Seek + Send + 'static>(
    &self,
    downloadable: &dyn Downloadable,
    writer: W,
) -> Result<W, anyhow::Error>
```

<ParamField path="downloadable" type="&dyn Downloadable" required>
  Message containing downloadable media
</ParamField>

<ParamField path="writer" type="W: Write + Seek + Send + 'static" required>
  Writer for streaming output. Must be Send + 'static for use in blocking task.
</ParamField>

<ResponseField name="writer" type="W">
  Returns the writer after successful download, seeked back to position 0.
</ResponseField>

### Example: streaming download

```rust theme={null}
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
```

<Note>
  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.
</Note>

***

## download\_from\_params

Download and decrypt media from raw CDN parameters without the original message. The parameters are bundled into a [`DownloadParams`](#downloadparams) struct.

```rust theme={null}
pub async fn download_from_params(
    &self,
    params: &DownloadParams,
) -> Result<Vec<u8>, anyhow::Error>
```

<ParamField path="params" type="&DownloadParams" required>
  The CDN/crypto fields needed to fetch and decrypt the media. Build one with [`DownloadParams::encrypted`](#downloadparams).
</ParamField>

<ResponseField name="bytes" type="Vec<u8>">
  Decrypted media bytes
</ResponseField>

### Example: download from stored metadata

```rust theme={null}
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?;
```

<Tip>
  `DownloadParams` implements [`Downloadable`](#downloadable-trait), so you can also pass it straight to [`download`](#download): `client.download(&params).await?`.
</Tip>

***

## download\_from\_params\_to\_writer

Streaming variant of `download_from_params` that writes to a writer.

```rust theme={null}
pub async fn download_from_params_to_writer<W: Write + Seek + Send + 'static>(
    &self,
    params: &DownloadParams,
    writer: W,
) -> Result<W, anyhow::Error>
```

<ParamField path="params" type="&DownloadParams" required>
  The CDN/crypto fields needed to fetch and decrypt the media. See [`DownloadParams`](#downloadparams).
</ParamField>

<ParamField path="writer" type="W" required>
  Writer for streaming output
</ParamField>

<ResponseField name="writer" type="W">
  Returns the writer after successful download
</ResponseField>

***

## DownloadParams

A `Downloadable` built from raw CDN fields, for re-downloading media without the original message.

```rust theme={null}
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,
}
```

| Field             | Type              | Description                                                        |
| ----------------- | ----------------- | ------------------------------------------------------------------ |
| `direct_path`     | `String`          | WhatsApp CDN path (e.g. `/v/t62.7118-24/12345_67890`)              |
| `media_key`       | `Option<Vec<u8>>` | 32-byte media key. `None` for plaintext (newsletter/channel) media |
| `file_sha256`     | `Vec<u8>`         | SHA-256 of the decrypted file                                      |
| `file_enc_sha256` | `Option<Vec<u8>>` | SHA-256 of the encrypted file (encrypted media only)               |
| `file_length`     | `u64`             | Original file size in bytes                                        |
| `media_type`      | `MediaType`       | `Image`, `Video`, `Audio`, `Document`, `Sticker`, …                |

### DownloadParams::encrypted

Convenience constructor for encrypted (E2EE) media — fills `media_key` and `file_enc_sha256` as `Some(...)`.

```rust theme={null}
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`](#downloadable-trait), 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.

```rust theme={null}
pub async fn fetch_sticker_pack(
    &self,
    pack_id: &str,
    locale: &str,
) -> Result<wacore::sticker_pack::StickerPack>
```

<ParamField path="pack_id" type="&str" required>
  The first-party sticker pack ID (typically extracted from a received `sticker_pack_message`).
</ParamField>

<ParamField path="locale" type="&str" required>
  BCP-47 locale tag for localized name / publisher strings. Pass `"en"` to match whatsmeow's default.
</ParamField>

<ResponseField name="pack" type="StickerPack">
  Pack metadata plus a `Vec<StickerPackItem>` of individual stickers. Each `StickerPackItem` implements [`Downloadable`](#downloadable-trait), so you can pass it straight to `client.download(...)`.
</ResponseField>

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.

```rust theme={null}
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

```rust theme={null}
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

```rust theme={null}
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 { /* … */ }
```

<Note>
  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`.
</Note>

***

## Downloadable Trait

The `Downloadable` trait provides a generic interface for downloading media from any message type.

```rust theme={null}
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() }
}
```

<ParamField path="direct_path" type="fn() -> Option<&str>">
  WhatsApp CDN path for the media file
</ParamField>

<ParamField path="media_key" type="fn() -> Option<&[u8]>">
  32-byte encryption key. Present for E2EE media, `None` for plaintext (newsletter/channel) media.
</ParamField>

<ParamField path="file_enc_sha256" type="fn() -> Option<&[u8]>">
  SHA-256 hash of the encrypted file. Used for encrypted media validation.
</ParamField>

<ParamField path="file_sha256" type="fn() -> Option<&[u8]>">
  SHA-256 hash of the decrypted file. Used for plaintext media validation.
</ParamField>

<ParamField path="file_length" type="fn() -> Option<u64>">
  Original file size in bytes
</ParamField>

<ParamField path="app_info" type="fn() -> MediaType">
  Media type for HKDF key derivation (`Image`, `Video`, `Audio`, `Document`, etc.)
</ParamField>

<ParamField path="static_url" type="fn() -> Option<&str>" default="None">
  Static CDN URL for direct download. Present on newsletter/channel media, bypasses host construction.
</ParamField>

<ParamField path="is_encrypted" type="fn() -> bool" default="media_key().is_some()">
  Returns `true` if media is encrypted (has `media_key`), `false` for plaintext media
</ParamField>

### 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.

```rust theme={null}
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

| Method           | Return type    | Description                                 |
| ---------------- | -------------- | ------------------------------------------- |
| `app_info()`     | `&'static str` | HKDF info string for key derivation         |
| `mms_type()`     | `&'static str` | Media type string for MMS path construction |
| `upload_path()`  | `&'static str` | URL path prefix for upload/download         |
| `is_encrypted()` | `bool`         | Whether this media type uses E2E encryption |

### Upload paths

| Media type             | Upload 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`              |

<Note>
  `ProductCatalogImage` is **unencrypted** — `is_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).
</Note>

***

## 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)

<Note>
  Newsletter and channel media is **not encrypted**. The library automatically detects this when `media_key` is absent and switches to plaintext validation.
</Note>

### Example: detect media type

```rust theme={null}
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.

```rust theme={null}
use whatsapp_rust::download::DownloadUtils;
```

### Key methods

| Method                                                                   | Description                                                                                                                                                                                                                                                                    |
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `verify_and_decrypt(encrypted_payload, media_key, media_type)`           | Verifies HMAC and decrypts AES-256-CBC in one call                                                                                                                                                                                                                             |
| `verify_and_decrypt_in_place(encrypted_payload, media_key, media_type)`  | In-place counterpart to `verify_and_decrypt` — authenticates and decrypts into the same buffer, truncating the trailing MAC/padding, instead of allocating a separate output. Used internally by non-streaming downloads to avoid keeping two file-sized buffers alive at once |
| `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                                                                                                                                                                                                                                   |
