> ## Documentation Index
> Fetch the complete documentation index at: https://personal-9eca1d6c-claude-eager-cerf-0xyrbp.mintlify.site/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, 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.

```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                                         |
| `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                                               |
