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

# upload

> Upload and encrypt media for sending in messages

## upload

Upload media to WhatsApp's CDN with automatic encryption.

<Tip>
  Only use `upload` for new or modified media. To forward existing media unchanged, reuse the original message's CDN fields directly — no upload required. See [media forwarding via CDN reuse](/guides/media-handling#forwarding-media-via-cdn-reuse).
</Tip>

```rust theme={null}
pub async fn upload(
    &self,
    data: Vec<u8>,
    media_type: MediaType,
    options: UploadOptions,
) -> Result<UploadResponse, anyhow::Error>
```

<ParamField path="data" type="Vec<u8>" required>
  Raw media bytes to upload. The data is automatically encrypted using AES-256-CBC before uploading.
</ParamField>

<ParamField path="media_type" type="MediaType" required>
  Type of media being uploaded. Determines the CDN endpoint and encryption keys:

  * `MediaType::Image` - Images and stickers
  * `MediaType::Video` - Video files
  * `MediaType::Audio` - Audio files and voice notes
  * `MediaType::Document` - Documents and other files
  * `MediaType::Sticker` - Sticker images
  * `MediaType::StickerPack` - Sticker pack ZIP files
  * `MediaType::StickerPackThumbnail` - Sticker pack thumbnail images
  * `MediaType::LinkThumbnail` - Link preview thumbnails
  * `MediaType::ProductCatalogImage` - Product catalog images (unencrypted)
</ParamField>

<ParamField path="options" type="UploadOptions" required>
  Upload options. Use `Default::default()` or `UploadOptions::new()` for default behavior. See [UploadOptions](#uploadoptions) below.
</ParamField>

<ResponseField name="UploadResponse" type="struct">
  Contains all metadata needed to include the media in a message:

  ```rust theme={null}
  #[non_exhaustive]
  pub struct UploadResponse {
      pub url: String,
      pub direct_path: String,
      pub media_key: [u8; 32],
      pub file_enc_sha256: [u8; 32],
      pub file_sha256: [u8; 32],
      pub file_length: u64,
      pub media_key_timestamp: i64,
      /// Per-64-KiB HMAC sidecar for progressive playback/seek (audio/video
      /// only). Pass to `AudioMessage`/`VideoMessage.streaming_sidecar`.
      pub streaming_sidecar: Option<Vec<u8>>,
  }
  ```

  Helper methods are provided for protobuf message construction, which requires `Vec<u8>`:

  ```rust theme={null}
  impl UploadResponse {
      pub fn media_key_vec(&self) -> Vec<u8> { ... }
      pub fn file_sha256_vec(&self) -> Vec<u8> { ... }
      pub fn file_enc_sha256_vec(&self) -> Vec<u8> { ... }
  }
  ```

  <Note>
    `UploadResponse` is `#[non_exhaustive]`. Field reads are unaffected; only exhaustive struct destructuring from outside the crate requires adding `..`.
  </Note>

  <ParamField path="url" type="String">
    Full CDN URL where the encrypted file was uploaded
  </ParamField>

  <ParamField path="direct_path" type="String">
    CDN path component (e.g., `/v/t62.7118-24/12345_67890`). Used for downloads.
  </ParamField>

  <ParamField path="media_key" type="[u8; 32]">
    32-byte encryption key. Required for the recipient to decrypt the media. Use `media_key_vec()` when building protobuf messages.
  </ParamField>

  <ParamField path="file_enc_sha256" type="[u8; 32]">
    SHA-256 hash of the encrypted file. Used for integrity verification during download. Use `file_enc_sha256_vec()` when building protobuf messages.
  </ParamField>

  <ParamField path="file_sha256" type="[u8; 32]">
    SHA-256 hash of the original (decrypted) file. Used for final validation after decryption. Use `file_sha256_vec()` when building protobuf messages.
  </ParamField>

  <ParamField path="file_length" type="u64">
    Original file size in bytes (before encryption)
  </ParamField>

  <ParamField path="media_key_timestamp" type="i64">
    Unix timestamp (seconds) when the media key was generated. Set automatically by `upload()` to the current time.
  </ParamField>

  <ParamField path="streaming_sidecar" type="Option<Vec<u8>>">
    Per-64-KiB HMAC-SHA256 table over the ciphertext that lets recipients seek and stream audio/video progressively without downloading the whole file. Generated automatically for audio and video uploads (`None` otherwise). Assign it to `AudioMessage.streaming_sidecar` / `VideoMessage.streaming_sidecar` when building the message. Opt in/out explicitly with `UploadOptions::with_streaming_sidecar(bool)`.
  </ParamField>
</ResponseField>

### Example: upload and send image

```rust theme={null}
use wacore::download::MediaType;
use waproto::whatsapp as wa;
use std::fs;

// Read image file
let image_bytes = fs::read("photo.jpg")?;

// Upload image to WhatsApp CDN
let upload = client.upload(image_bytes, MediaType::Image, Default::default()).await?;

// Create image message with upload metadata
let message = wa::Message {
    image_message: buffa::MessageField::some(wa::message::ImageMessage {
        url: Some(upload.url),
        direct_path: Some(upload.direct_path),
        media_key: Some(upload.media_key_vec()),
        file_enc_sha256: Some(upload.file_enc_sha256_vec()),
        file_sha256: Some(upload.file_sha256_vec()),
        file_length: Some(upload.file_length),
        media_key_timestamp: Some(upload.media_key_timestamp),
        caption: Some("Check out this photo!".to_string()),
        mimetype: Some("image/jpeg".to_string()),
        ..Default::default()
    }),
    ..Default::default()
};

// Send the message
let result = client.send_message(chat_jid, message).await?;
println!("Image sent: {}", result.message_id);
```

### Example: upload video with progress

```rust theme={null}
use wacore::download::MediaType;
use std::fs;

let video_bytes = fs::read("video.mp4")?;
println!("Uploading {} bytes...", video_bytes.len());

let upload = client.upload(video_bytes, MediaType::Video, Default::default()).await?;
println!("Upload complete!");
println!("  URL: {}", upload.url);
println!("  Path: {}", upload.direct_path);

// Use upload metadata in VideoMessage...
```

### Example: upload document

```rust theme={null}
use wacore::download::MediaType;
use waproto::whatsapp as wa;
use std::fs;
use std::path::Path;

let doc_path = Path::new("report.pdf");
let doc_bytes = fs::read(doc_path)?;
let filename = doc_path.file_name()
    .and_then(|n| n.to_str())
    .unwrap_or("document.pdf");

let upload = client.upload(doc_bytes, MediaType::Document, Default::default()).await?;

let message = wa::Message {
    document_message: buffa::MessageField::some(wa::message::DocumentMessage {
        url: Some(upload.url),
        direct_path: Some(upload.direct_path),
        media_key: Some(upload.media_key_vec()),
        file_enc_sha256: Some(upload.file_enc_sha256_vec()),
        file_sha256: Some(upload.file_sha256_vec()),
        file_length: Some(upload.file_length),
        media_key_timestamp: Some(upload.media_key_timestamp),
        file_name: Some(filename.to_string()),
        mimetype: Some("application/pdf".to_string()),
        ..Default::default()
    }),
    ..Default::default()
};

let result = client.send_message(chat_jid, message).await?;
```

### Example: upload audio (voice note)

```rust theme={null}
use wacore::download::MediaType;
use waproto::whatsapp as wa;

let audio_bytes = fs::read("voice.ogg")?;
let upload = client.upload(audio_bytes, MediaType::Audio, Default::default()).await?;

let message = wa::Message {
    audio_message: buffa::MessageField::some(wa::message::AudioMessage {
        url: Some(upload.url),
        direct_path: Some(upload.direct_path),
        media_key: Some(upload.media_key_vec()),
        file_enc_sha256: Some(upload.file_enc_sha256_vec()),
        file_sha256: Some(upload.file_sha256_vec()),
        file_length: Some(upload.file_length),
        media_key_timestamp: Some(upload.media_key_timestamp),
        mimetype: Some("audio/ogg; codecs=opus".to_string()),
        ptt: Some(true), // Mark as Push-To-Talk (voice note)
        ..Default::default()
    }),
    ..Default::default()
};

let result = client.send_message(chat_jid, message).await?;
```

***

## High-level message builders

The `whatsapp_rust::media` module turns an [`UploadResponse`](#upload) into a ready-to-send `wa::Message`, so you don't hand-assemble the CDN/crypto fields (url, direct\_path, media\_key, file\_sha256, file\_enc\_sha256, file\_length, media\_key\_timestamp, streaming\_sidecar) on every send. Each builder takes the upload result plus a typed options struct with sensible MIME defaults.

```rust theme={null}
use wacore::download::MediaType;
use whatsapp_rust::media::{self, ImageOptions, VideoOptions, AudioOptions, DocumentOptions};
use std::fs;

let chat_jid: Jid = "15551234567@s.whatsapp.net".parse()?;
let image_bytes = fs::read("photo.jpg")?;

let upload = client.upload(image_bytes, MediaType::Image, Default::default()).await?;
let msg = media::image_message(upload, ImageOptions {
    caption: Some("Check out this photo!".to_string()),
    ..Default::default()
});
client.send_message(chat_jid, msg).await?;
```

| Builder                                 | Options struct    | Notable fields (all optional)                                               | MIME default               |
| --------------------------------------- | ----------------- | --------------------------------------------------------------------------- | -------------------------- |
| `media::image_message(upload, opts)`    | `ImageOptions`    | `caption`, `mimetype`, `jpeg_thumbnail`                                     | `image/jpeg`               |
| `media::video_message(upload, opts)`    | `VideoOptions`    | `caption`, `mimetype`, `jpeg_thumbnail`, `duration_seconds`, `gif_playback` | `video/mp4`                |
| `media::document_message(upload, opts)` | `DocumentOptions` | `mimetype`, `file_name`, `title`, `caption`, `page_count`, `jpeg_thumbnail` | `application/octet-stream` |
| `media::audio_message(upload, opts)`    | `AudioOptions`    | `mimetype`, `duration_seconds`, `ptt`, `waveform`                           | `audio/ogg; codecs=opus`   |

The `video_message` and `audio_message` builders carry the upload's `streaming_sidecar` (progressive-playback HMAC table) when present. For fields the options structs don't expose (e.g. explicit image `width`/`height`), assemble the proto by hand as shown above.

```rust theme={null}
use wacore::download::MediaType;
use whatsapp_rust::media::{self, AudioOptions};
use std::fs;

// Voice note (push-to-talk) with a waveform preview
let opus_bytes = fs::read("voice.ogg")?;
let waveform_bytes: Vec<u8> = vec![]; // optional PCM waveform preview

let upload = client.upload(opus_bytes, MediaType::Audio, Default::default()).await?;
let msg = media::audio_message(upload, AudioOptions {
    ptt: Some(true),
    duration_seconds: Some(8),
    waveform: Some(waveform_bytes),
    ..Default::default()
});
client.send_message(chat_jid, msg).await?;
```

***

## UploadOptions

Options for customizing upload behavior.

```rust theme={null}
#[non_exhaustive]
pub struct UploadOptions {
    /// Reuse an existing media key instead of generating a fresh one.
    pub media_key: Option<[u8; 32]>,
    /// Override streaming-sidecar generation; `None` selects by media type.
    pub streaming_sidecar: Option<bool>,
}
```

`UploadOptions` is `#[non_exhaustive]`, so it cannot be constructed using struct literal syntax from outside the crate. Use `UploadOptions::default()` or `UploadOptions::new()` with builder methods instead.

<ParamField path="media_key" type="Option<[u8; 32]>" default="None">
  When set, reuses the provided 32-byte media key instead of generating a new one. This is required when uploading a sticker pack thumbnail, which must share the same `media_key` as the sticker pack ZIP.
</ParamField>

<ParamField path="streaming_sidecar" type="Option<bool>" default="None">
  Override streaming-sidecar generation. `None` (default) generates the per-64-KiB HMAC-SHA256 `streaming_sidecar` only for audio and video; `Some(true)` forces it on; `Some(false)` forces it off. When produced, assign the resulting `UploadResponse.streaming_sidecar` to `AudioMessage.streaming_sidecar` / `VideoMessage.streaming_sidecar` so recipients can seek and stream progressively.
</ParamField>

### Creating options

```rust theme={null}
use whatsapp_rust::upload::UploadOptions;

// Default options (generates a new media key)
let options = UploadOptions::default();

// Reuse an existing media key
let options = UploadOptions::new().with_media_key(existing_key.clone());

// Force the streaming sidecar on (e.g. for a media type it skips by default)
let options = UploadOptions::new().with_streaming_sidecar(true);
```

### Example: Upload sticker pack thumbnail with shared key

```rust theme={null}
use whatsapp_rust::upload::UploadOptions;
use wacore::download::MediaType;

// Upload the sticker pack ZIP first
let zip_upload = client.upload(
    zip_bytes,
    MediaType::StickerPack,
    UploadOptions::default(),
).await?;

// Upload the thumbnail with the same media_key
let thumb_upload = client.upload(
    thumbnail_jpeg,
    MediaType::StickerPackThumbnail,
    UploadOptions::new().with_media_key(zip_upload.media_key),
).await?;
```

***

## MediaEncryptor

Chunk-based AES-256-CBC media encryptor. Processes plaintext incrementally without requiring a sync `Read`, enabling use with async streams, network sources, or any chunk-at-a-time producer.

Two output modes with zero duplicated crypto logic:

* `update()` / `finalize()` — append encrypted blocks to a `Vec<u8>`
* `update_to_writer()` / `finalize_to_writer()` — write directly to any `Write` implementor, zero intermediate buffer

```rust theme={null}
pub struct MediaEncryptor { /* ... */ }
```

### Creating an encryptor

```rust theme={null}
use wacore::upload::MediaEncryptor;
use wacore::download::MediaType;

// Initialize with a random media key
let enc = MediaEncryptor::new(MediaType::Image)?;

// Or initialize with a caller-supplied key (must be 32 cryptographically random bytes)
let enc = MediaEncryptor::with_key(media_key, MediaType::Image)?;
```

<ParamField path="media_type" type="MediaType" required>
  Type of media being encrypted. Determines the HKDF info string for key derivation.
</ParamField>

<ParamField path="media_key" type="[u8; 32]">
  Caller-supplied 32-byte key for `with_key()`. Must be cryptographically random — reusing keys breaks confidentiality.
</ParamField>

### Feeding plaintext

Feed plaintext in arbitrarily sized chunks. Encrypted AES blocks are emitted as soon as a full 16-byte block is available; partial blocks are buffered internally (at most 15 bytes).

```rust theme={null}
// Append encrypted blocks to a Vec
let mut encrypted = Vec::new();
enc.update(plaintext_chunk, &mut encrypted);

// Or write encrypted blocks directly to a writer
enc.update_to_writer(plaintext_chunk, &mut writer)?;
```

<ParamField path="plaintext" type="&[u8]" required>
  Plaintext bytes to encrypt. Can be any size — from a single byte to the entire file.
</ParamField>

<Note>
  On `update_to_writer` I/O error, the encryptor state is unspecified — discard it.
</Note>

### Finalizing

Finalize applies PKCS7 padding to the remaining plaintext, encrypts the final block(s), and appends a 10-byte truncated HMAC-SHA256 MAC.

```rust theme={null}
// Finalize to Vec
let info = enc.finalize(&mut encrypted)?;

// Or finalize to writer
let info = enc.finalize_to_writer(&mut writer)?;
```

<ResponseField name="EncryptedMediaInfo" type="struct">
  Encryption metadata (keys and hashes). The encrypted data was already written via `update`/`finalize` calls.

  ```rust theme={null}
  pub struct EncryptedMediaInfo {
      pub media_key: [u8; 32],
      pub file_sha256: [u8; 32],
      pub file_enc_sha256: [u8; 32],
      pub file_length: u64,
  }
  ```

  <ParamField path="media_key" type="[u8; 32]">
    32-byte encryption key for the recipient to decrypt the media
  </ParamField>

  <ParamField path="file_sha256" type="[u8; 32]">
    SHA-256 hash of the original plaintext, computed on the fly during encryption
  </ParamField>

  <ParamField path="file_enc_sha256" type="[u8; 32]">
    SHA-256 hash of the encrypted output (ciphertext + MAC)
  </ParamField>

  <ParamField path="file_length" type="u64">
    Original file size in bytes (before encryption)
  </ParamField>
</ResponseField>

### Example: Chunk-based encryption to Vec

```rust theme={null}
use wacore::upload::MediaEncryptor;
use wacore::download::MediaType;

let mut enc = MediaEncryptor::new(MediaType::Image)?;
let mut encrypted = Vec::new();

// Feed plaintext in arbitrary chunks
for chunk in plaintext_data.chunks(4096) {
    enc.update(chunk, &mut encrypted);
}

let info = enc.finalize(&mut encrypted)?;
println!("Media key: {:?}", info.media_key);
println!("Original size: {} bytes", info.file_length);
```

### Example: Streaming encryption to a writer

```rust theme={null}
use wacore::upload::MediaEncryptor;
use wacore::download::MediaType;
use std::fs::File;

let mut enc = MediaEncryptor::new(MediaType::Video)?;
let mut writer = File::create("large_video.enc")?;

// Feed chunks from any source (async stream, network, etc.)
for chunk in reader_chunks {
    enc.update_to_writer(&chunk, &mut writer)?;
}

let info = enc.finalize_to_writer(&mut writer)?;
```

<Note>
  Memory usage is constant regardless of file size — only the crypto state and at most 15 bytes of remainder are buffered. Unlike `encrypt_media_streaming`, `MediaEncryptor` does not require a sync `Read` source, making it suitable for async contexts.
</Note>

***

## Streaming encryption

For sync `Read` sources, `encrypt_media_streaming` provides a convenience wrapper around `MediaEncryptor` that reads in 8KB chunks:

```rust theme={null}
pub fn encrypt_media_streaming<R: Read, W: Write>(
    reader: R,
    writer: W,
    media_type: MediaType,
) -> Result<EncryptedMediaInfo>
```

<ParamField path="reader" type="R: Read" required>
  Source of plaintext media bytes. Can be a `File`, `Cursor<Vec<u8>>`, or any `Read` implementor.
</ParamField>

<ParamField path="writer" type="W: Write" required>
  Destination for encrypted output. Receives ciphertext + 10-byte HMAC-SHA256 MAC — the exact bytes to upload to WhatsApp CDN.
</ParamField>

<ParamField path="media_type" type="MediaType" required>
  Type of media being encrypted. Determines the HKDF info string for key derivation.
</ParamField>

### Example: Encrypt from file to file

```rust theme={null}
use wacore::upload::encrypt_media_streaming;
use wacore::download::MediaType;
use std::fs::File;

let reader = File::open("large_video.mp4")?;
let writer = File::create("large_video.enc")?;

let info = encrypt_media_streaming(reader, writer, MediaType::Video)?;
println!("Media key: {:?}", info.media_key);
println!("Original size: {} bytes", info.file_length);
```

<Note>
  `encrypt_media_streaming` uses \~40KB of memory regardless of file size (8KB read buffer + crypto state). Internally it creates a `MediaEncryptor` and feeds 8KB chunks from the reader.
</Note>

### encrypt\_media\_streaming\_with\_key

```rust theme={null}
pub fn encrypt_media_streaming_with_key<R: Read, W: Write>(
    reader: R,
    writer: W,
    media_type: MediaType,
    media_key: Option<&[u8; 32]>,
    sidecar: Option<bool>,
) -> Result<EncryptedMediaInfo>
```

Lower-level variant of `encrypt_media_streaming`. It returns the same `EncryptedMediaInfo` (including the optional `streaming_sidecar`) but lets you control two things the convenience wrapper decides for you:

* **`media_key`** — pass `Some(&key)` to encrypt with a pre-existing 32-byte key (e.g. when re-encrypting for a shared-key sticker pack), or `None` to generate a fresh random key like `encrypt_media_streaming` does.
* **`sidecar`** — pass `Some(true)`/`Some(false)` to force the streaming sidecar on or off, or `None` to use the per-media-type default (generated for audio/video).

Reach for this when you need a deterministic key or want to override sidecar generation; otherwise use `encrypt_media_streaming`, which calls this with `None, None`.

### Relationship to `encrypt_media`

The in-memory `encrypt_media` function is a convenience wrapper around `encrypt_media_streaming`:

```rust theme={null}
pub fn encrypt_media(plaintext: &[u8], media_type: MediaType) -> Result<EncryptedMedia> {
    let mut data_to_upload = Vec::new();
    let info = encrypt_media_streaming(Cursor::new(plaintext), &mut data_to_upload, media_type)?;
    Ok(EncryptedMedia {
        data_to_upload,
        media_key: info.media_key,
        file_sha256: info.file_sha256,
        file_enc_sha256: info.file_enc_sha256,
    })
}
```

### When to use each API

| API                       | Use when                                                                        |
| ------------------------- | ------------------------------------------------------------------------------- |
| `MediaEncryptor`          | You have an async stream, network source, or need full control over chunk sizes |
| `encrypt_media_streaming` | You have a sync `Read` source (file, cursor)                                    |
| `encrypt_media`           | The entire plaintext fits in memory                                             |

***

## upload\_stream (constant-memory)

`upload()` encrypts the whole plaintext into a `Vec<u8>` in memory before sending. For large files, `upload_stream` keeps memory constant by uploading **already-encrypted** ciphertext from any backing store (temp file, memory-mapped blob, …) without re-buffering it.

```rust theme={null}
pub async fn upload_stream<S>(
    &self,
    source: S,
    info: wacore::upload::EncryptedMediaInfo,
    media_type: MediaType,
) -> Result<UploadResponse>
where
    S: wacore::upload::UploadSource + 'static,
```

The flow is two steps you own:

1. Encrypt the plaintext into storage of your choice with [`encrypt_media_streaming`](#streaming-encryption) (or `encrypt_media_streaming_with_key`). It returns an `EncryptedMediaInfo` carrying the crypto metadata and the optional `streaming_sidecar`.
2. Pass that storage (as an `UploadSource`) plus the `EncryptedMediaInfo` to `upload_stream`. The method never touches disk itself.

```rust theme={null}
use wacore::upload::{encrypt_media_streaming, UploadSource};
use wacore::download::MediaType;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use tempfile::NamedTempFile;

// A file-backed UploadSource. It owns the temp file (so the data lives as
// long as the source) and reopens it by path on every attempt, giving each
// reader its own independent OS file offset — safe for retry / resume /
// host-failover, which re-invoke reader_from.
struct FileSource {
    file: NamedTempFile,
    len: u64,
}

impl UploadSource for FileSource {
    fn len(&self) -> u64 {
        self.len
    }
    fn reader_from(&self, offset: u64) -> std::io::Result<Box<dyn Read + Send>> {
        let mut f = File::open(self.file.path())?; // fresh fd, independent cursor
        f.seek(SeekFrom::Start(offset))?;
        Ok(Box::new(f))
    }
}

// 1. Encrypt big_video.mp4 into a named temp file (constant memory). `&File`
//    implements Write, so the ciphertext is written straight to disk.
let cipher_file = NamedTempFile::new()?;
let info = encrypt_media_streaming(
    File::open("big_video.mp4")?,
    cipher_file.as_file(),
    MediaType::Video,
)?;
let len = cipher_file.as_file().metadata()?.len(); // ciphertext size

// 2. Stream the ciphertext straight from the temp file to the CDN — the
//    blob is never buffered in memory.
let source = FileSource { file: cipher_file, len };
let upload = client.upload_stream(source, info, MediaType::Video).await?;

// upload.streaming_sidecar is propagated from the EncryptedMediaInfo.
```

<Warning>
  Don't back a multi-attempt `UploadSource` with `File::try_clone()` — on Unix the cloned descriptor shares the original's file offset, so a retry or resume that calls `reader_from` again would move the cursor out from under any concurrent read. Reopen by path (as above) or use positional reads so each reader has an independent offset.
</Warning>

<Note>
  If the ciphertext already fits comfortably in memory, skip the custom source and pass it directly — `bytes::Bytes` implements `UploadSource`, so `client.upload_stream(bytes::Bytes::from(ciphertext_vec), info, media_type)` works for the small-file case. The file-backed source above is what keeps memory constant for large media.
</Note>

### UploadSource trait

`upload_stream` accepts anything implementing `UploadSource`:

```rust theme={null}
pub trait UploadSource: Send + Sync {
    /// Total length of the encrypted blob in bytes.
    fn len(&self) -> u64;
    fn is_empty(&self) -> bool { self.len() == 0 }
    /// A reader positioned at `offset`. Called once per upload attempt
    /// (re-invoked on retry / resume).
    fn reader_from(&self, offset: u64) -> std::io::Result<Box<dyn Read + Send>>;
}
```

Built-in implementations:

| Type                   | Notes                                                                                                                                            |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `std::sync::Arc<[u8]>` | In-memory shared buffer                                                                                                                          |
| `bytes::Bytes`         | **O(1) to construct** from an owned `Vec<u8>` (adopts the buffer instead of copying). Prefer this when the ciphertext was produced into a `Vec`. |

Implement the trait yourself for a file-backed or network-backed source — `reader_from(offset)` is what makes resumable uploads work without re-encrypting.

### encrypted\_len

```rust theme={null}
pub const fn encrypted_len(plaintext_len: usize) -> usize
```

Computes the exact ciphertext size for a given plaintext length (plaintext rounded up to the next 16-byte AES block, plus the 10-byte media MAC). Use it to pre-allocate an exact-fit `Vec` before streaming encryption, avoiding reallocation as the ciphertext grows.

<Note>
  `upload()` is unchanged and still the right choice when the plaintext fits in memory — both paths share the same retry/resumable-upload machinery. Reach for `upload_stream` only when you want to bound memory for large media.
</Note>

***

## Resumable uploads

For files 5 MiB (5,242,880 bytes) or larger, the client automatically probes the CDN to check for a previous partial or complete upload of the same file before sending the full payload.

### Resume check flow

1. A `POST` request is sent to the upload URL with `?resume=1` appended
2. The server responds with one of three states:
   * **Complete** — the file already exists on the CDN. The existing `url` and `direct_path` are returned immediately with no upload
   * **Resume** — a partial upload exists. The client resumes from the given `byte_offset`, appending `&file_offset={offset}` to the URL and sending only the remaining bytes
   * **Not found** — no previous upload exists. A full upload proceeds normally

<Note>
  The resume check is non-fatal. If the probe request itself fails (network error, unexpected response), the client silently falls back to a full upload. No special handling is needed in your code.
</Note>

### Resume check endpoint

```
POST https://{media_host}/mms/{type}/{token}?auth={auth}&token={token}&resume=1
Origin: https://web.whatsapp.com
```

### Resumed upload endpoint

When a partial upload is detected at byte offset `N`:

```
POST https://{media_host}/mms/{type}/{token}?auth={auth}&token={token}&file_offset=N
Content-Type: application/octet-stream
Body: [encrypted bytes from offset N onward]
```

***

## Media Encryption

All media uploaded to WhatsApp is end-to-end encrypted before transmission:

### Encryption Process

1. **Generate keys**: Create random 32-byte `media_key`
2. **Derive keys**: Use HKDF-SHA256 with media type info string to derive:
   * 16-byte IV (initialization vector)
   * 32-byte cipher key
   * 32-byte MAC key
3. **Encrypt**: Apply AES-256-CBC with PKCS7 padding
4. **Compute MAC**: HMAC-SHA256 over IV + ciphertext, append first 10 bytes
5. **Upload**: POST encrypted bytes to WhatsApp CDN
6. **Return metadata**: `media_key`, hashes, and CDN path for message

### Key Derivation

Each media type uses a specific HKDF info string:

```rust theme={null}
MediaType::Image              → "WhatsApp Image Keys"
MediaType::Video              → "WhatsApp Video Keys"
MediaType::Audio              → "WhatsApp Audio Keys"
MediaType::Document           → "WhatsApp Document Keys"
MediaType::Sticker            → "WhatsApp Image Keys"
MediaType::StickerPack        → "WhatsApp Sticker Pack Keys"
MediaType::StickerPackThumbnail → "WhatsApp Sticker Pack Thumbnail Keys"
MediaType::LinkThumbnail      → "WhatsApp Link Thumbnail Keys"
MediaType::History            → "WhatsApp History Keys"
MediaType::AppState           → "WhatsApp App State Keys"
MediaType::ProductCatalogImage → N/A (unencrypted)
```

The `media_key` is shared with recipients through the encrypted message, allowing them to decrypt the media. `ProductCatalogImage` is the exception — it is uploaded without encryption and has no media key.

### Encryption vs Decryption

The encryption process is the inverse of download decryption:

| Upload (Encryption)            | Download (Decryption)             |
| ------------------------------ | --------------------------------- |
| Generate `media_key`           | Receive `media_key` in message    |
| Derive IV, cipher key, MAC key | Derive same keys from `media_key` |
| Encrypt with AES-256-CBC       | Decrypt with AES-256-CBC          |
| Append HMAC-SHA256 (10 bytes)  | Verify HMAC-SHA256                |
| Upload to CDN                  | Download from CDN                 |

***

## MediaType

Specifies the type of media for encryption and CDN routing.

```rust theme={null}
pub enum MediaType {
    Image,
    Video,
    Audio,
    Document,
    History,
    AppState,
    Sticker,
    StickerPack,
    StickerPackThumbnail,
    LinkThumbnail,
    ProductCatalogImage,
}
```

<ParamField path="Image" type="variant">
  JPEG, PNG, or other image formats. Uses `/mms/image` endpoint and `"WhatsApp Image Keys"` for HKDF.
</ParamField>

<ParamField path="Video" type="variant">
  MP4 or other video formats. Uses `/mms/video` endpoint and `"WhatsApp Video Keys"` for HKDF.
</ParamField>

<ParamField path="Audio" type="variant">
  Audio files and voice notes. Uses `/mms/audio` endpoint and `"WhatsApp Audio Keys"` for HKDF.
</ParamField>

<ParamField path="Document" type="variant">
  PDF, DOCX, ZIP, and other document formats. Uses `/mms/document` endpoint and `"WhatsApp Document Keys"` for HKDF.
</ParamField>

<ParamField path="Sticker" type="variant">
  Sticker images (WebP format). Uses `/mms/image` endpoint and `"WhatsApp Image Keys"` for HKDF (same as images).
</ParamField>

<ParamField path="History" type="variant">
  History sync data. Uses `/mms/md-msg-hist` endpoint and `"WhatsApp History Keys"` for HKDF.
</ParamField>

<ParamField path="AppState" type="variant">
  App state sync data. Uses `/mms/md-app-state` endpoint and `"WhatsApp App State Keys"` for HKDF.
</ParamField>

<ParamField path="StickerPack" type="variant">
  Sticker pack ZIP files. Uses `/mms/sticker-pack` endpoint and `"WhatsApp Sticker Pack Keys"` for HKDF.
</ParamField>

<ParamField path="StickerPackThumbnail" type="variant">
  Sticker pack thumbnail images (JPEG). Uses `/mms/thumbnail-sticker-pack` endpoint and `"WhatsApp Sticker Pack Thumbnail Keys"` for HKDF. Must be uploaded with the same `media_key` as the corresponding sticker pack ZIP — use `UploadOptions::new().with_media_key(...)`.
</ParamField>

<ParamField path="LinkThumbnail" type="variant">
  Link preview thumbnails. Uses `/mms/thumbnail-link` endpoint and `"WhatsApp Link Thumbnail Keys"` for HKDF.
</ParamField>

<ParamField path="ProductCatalogImage" type="variant">
  Product catalog images for WhatsApp Business. **Unencrypted** — `is_encrypted()` returns `false`. Uses `/product/image` endpoint (not under the `/mms/` prefix). Matches WhatsApp Web's `CreateMediaKeys.js` behavior which skips encryption for this type.
</ParamField>

***

## Upload Endpoint

The upload endpoint is constructed as:

```
https://{media_host}{upload_path}/{token}?auth={auth}&token={token}
```

Where:

* `{media_host}` — CDN hostname from media connection (primary hosts tried first)
* `{upload_path}` — Media type path from `MediaType::upload_path()` (e.g., `/mms/image`, `/mms/video`, `/product/image`)
* `{token}` — Base64url-encoded `file_enc_sha256`
* `{auth}` — Media connection auth token

<Note>
  Most media types use the `/mms/{type}` prefix, but `ProductCatalogImage` uses `/product/image` directly. The `upload_path()` method returns the correct path for each media type.
</Note>

Optional query parameters (added automatically for resumable uploads):

* `resume=1` — probe for existing upload (resume check request)
* `file_offset={N}` — resume upload from byte offset `N`

The request uses:

* **Method**: `POST`
* **Content-Type**: `application/octet-stream`
* **Origin**: `https://web.whatsapp.com`
* **Body**: Encrypted media bytes (or remaining bytes when resuming)

<Note>
  The upload automatically handles media connection refresh. If the connection is expired, it's renewed before uploading. If the CDN returns HTTP 401 or 403, the client invalidates the cached credentials, fetches a fresh auth token, and retries the upload once.
</Note>

***

## Error Handling

### Automatic retry on auth errors

The upload method automatically retries when WhatsApp's CDN returns HTTP 401 or 403. On an auth error, the client invalidates the cached media connection, fetches fresh credentials from WhatsApp's servers, and retries the upload once. If the retry also fails with an auth error, the error is returned to the caller.

For non-auth HTTP errors (such as 500), the client tries the next available CDN host without refreshing credentials. Hosts are tried in priority order — primary hosts first, then fallback hosts.

### Common upload errors

<ParamField path="HTTP 4xx/5xx" type="error">
  CDN returned error status. Auth errors (401/403) are retried automatically with fresh credentials. Other errors are tried against alternate CDN hosts.

  ```rust theme={null}
  match client.upload(data, media_type, Default::default()).await {
      Err(e) if e.to_string().contains("Upload failed") => {
          eprintln!("CDN error: {}", e);
          // Retry or use fallback
      }
      Err(e) => eprintln!("Other error: {}", e),
      Ok(upload) => { /* Success */ }
  }
  ```
</ParamField>

<ParamField path="No media hosts" type="error">
  Media connection has no available CDN hosts.

  ```rust theme={null}
  Err(anyhow!("No media hosts"))
  ```
</ParamField>

<ParamField path="Encryption failure" type="error">
  Failed to encrypt media (rare, usually indicates invalid input).

  ```rust theme={null}
  match client.upload(data, media_type, Default::default()).await {
      Err(e) if e.to_string().contains("encrypt") => {
          eprintln!("Encryption error: {}", e);
      }
      _ => {}
  }
  ```
</ParamField>

### Example: Upload with additional retry

Auth errors (401/403) are retried automatically. For other transient failures, you can add your own retry logic:

```rust theme={null}
use std::time::Duration;
use tokio::time::sleep;

let mut attempts = 0;
let max_attempts = 3;

let upload = loop {
    attempts += 1;
    
    match client.upload(data.clone(), MediaType::Image, Default::default()).await {
        Ok(upload) => break upload,
        Err(e) if attempts < max_attempts => {
            eprintln!("Upload attempt {} failed: {}", attempts, e);
            sleep(Duration::from_secs(2)).await;
            continue;
        }
        Err(e) => return Err(e),
    }
};

println!("Upload succeeded after {} attempt(s)", attempts);
```
