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

# Media Handling

> Learn how to upload and download media with encryption in whatsapp-rust

## Overview

This guide covers uploading and downloading media (images, videos, audio, documents, stickers) with automatic encryption and decryption.

## Downloading Media

### From message objects

Download media directly from message types that implement `Downloadable`:

```rust theme={null}
use wacore::download::Downloadable;
use whatsapp_rust::client::Client;
use wacore::types::events::InboundMessage;

match event {
    Event::Messages(batch) => {
        for InboundMessage { message, info, .. } in batch.iter() {
            // Image
            if let Some(img) = message.image_message.as_option() {
                let data = client.download(img).await?;
                std::fs::write("image.jpg", data)?;
            }

            // Video
            if let Some(video) = message.video_message.as_option() {
                let data = client.download(video).await?;
                std::fs::write("video.mp4", data)?;
            }

            // Audio
            if let Some(audio) = message.audio_message.as_option() {
                let data = client.download(audio).await?;
                let ext = if audio.ptt() { "ogg" } else { "mp3" };
                std::fs::write(format!("audio.{}", ext), data)?;
            }

            // Document
            if let Some(doc) = message.document_message.as_option() {
                let data = client.download(doc).await?;
                let filename = doc.file_name.as_deref().unwrap_or("document");
                std::fs::write(filename, data)?;
            }

            // Sticker
            if let Some(sticker) = message.sticker_message.as_option() {
                let data = client.download(sticker).await?;
                std::fs::write("sticker.webp", data)?;
            }
        }
    }
    _ => {}
}
```

See [Download API reference](/api/download) for full details.

### Streaming Download

For large files, use streaming to avoid memory overhead:

```rust theme={null}
use std::fs::File;

if let Some(video) = message.video_message.as_option() {
    let file = File::create("video.mp4")?;
    let file = client.download_to_writer(video, file).await?;
    println!("Video downloaded with constant memory usage!");
}
```

See [Download API reference](/api/download#download_to_writer) for streaming details.

<Note>
  When the HTTP client supports streaming (like the default `UreqHttpClient`), downloads use \~40KB of memory regardless of file size (8KB buffer + decryption state). If the HTTP client doesn't support streaming, `download_to_writer` automatically falls back to a buffered download — the file is fetched into memory first, then decrypted and written. This fallback ensures the method works with any `HttpClient` implementation.
</Note>

### From raw parameters

Download media when you have the raw encryption parameters:

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

let params = DownloadParams::encrypted(
    "/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
);

let data = client.download_from_params(&params).await?;

std::fs::write("downloaded.jpg", data)?;
```

See [Download API reference](/api/download#download_from_params) for raw parameter downloads.

### Streaming from raw parameters

```rust theme={null}
let params = DownloadParams::encrypted(
    direct_path,
    &media_key,
    &file_sha256,
    &file_enc_sha256,
    file_length,
    MediaType::Video,
);

let file = File::create("large_video.mp4")?;
let file = client.download_from_params_to_writer(&params, file).await?;
```

See [Download API reference](/api/download#download_from_params_to_writer) for streaming from raw parameters.

## Thumbnails

Thumbnails are **not generated by the library** — you provide them as raw JPEG bytes when constructing media messages. WhatsApp clients use these thumbnails as low-resolution previews while the full media is downloading.

### Setting thumbnails on media messages

Set the `jpeg_thumbnail` field on image, video, or document messages:

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

let upload = client.upload(image_data, MediaType::Image, Default::default()).await?;

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_sha256: Some(upload.file_sha256_vec()),
        file_enc_sha256: Some(upload.file_enc_sha256_vec()),
        file_length: Some(upload.file_length),
        media_key_timestamp: Some(upload.media_key_timestamp),
        mimetype: Some("image/jpeg".to_string()),
        jpeg_thumbnail: Some(thumbnail_bytes), // raw JPEG bytes
        width: Some(1920),
        height: Some(1080),
        ..Default::default()
    }),
    ..Default::default()
};
```

### Thumbnail fields by message type

| Message type      | Inline thumbnail field | Server-hosted thumbnail fields                                      |
| ----------------- | ---------------------- | ------------------------------------------------------------------- |
| `ImageMessage`    | `jpeg_thumbnail`       | `thumbnail_direct_path`, `thumbnail_sha256`, `thumbnail_enc_sha256` |
| `VideoMessage`    | `jpeg_thumbnail`       | `thumbnail_direct_path`, `thumbnail_sha256`, `thumbnail_enc_sha256` |
| `DocumentMessage` | `jpeg_thumbnail`       | `thumbnail_direct_path`, `thumbnail_sha256`, `thumbnail_enc_sha256` |
| `StickerMessage`  | `png_thumbnail`        | —                                                                   |

<Note>
  Inline thumbnails (`jpeg_thumbnail` / `png_thumbnail`) are embedded directly in the encrypted message payload. Server-hosted thumbnail fields (`thumbnail_direct_path`, etc.) reference a separately uploaded thumbnail that clients can download independently. For most use cases, inline thumbnails are sufficient.
</Note>

### Reusing thumbnails from incoming messages

When re-uploading media (for example, after processing or transcoding), you can clone the thumbnail from the original message:

```rust theme={null}
if let Some(img) = message.image_message.as_option() {
    let data = client.download(img).await?;
    let upload = client.upload(data, MediaType::Image, Default::default()).await?;

    let reply = 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_sha256: Some(upload.file_sha256_vec()),
            file_enc_sha256: Some(upload.file_enc_sha256_vec()),
            file_length: Some(upload.file_length),
            media_key_timestamp: Some(upload.media_key_timestamp),
            mimetype: img.mimetype.clone(),
            jpeg_thumbnail: img.jpeg_thumbnail.clone(), // reuse from original
            height: img.height,
            width: img.width,
            ..Default::default()
        }),
        ..Default::default()
    };
}
```

<Tip>
  If you don't need to modify the media, use [CDN reuse](#forwarding-media-via-cdn-reuse) instead — it forwards the entire message (including thumbnails) without downloading or re-uploading anything.
</Tip>

### Downloading link thumbnails

The `LinkThumbnail` variant in `MediaType` supports downloading thumbnails for URL previews in `ExtendedTextMessage`:

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

// LinkThumbnail uses HKDF info "WhatsApp Link Thumbnail Keys"
let params = DownloadParams::encrypted(
    thumbnail_direct_path,
    &thumbnail_media_key,
    &thumbnail_sha256,
    &thumbnail_enc_sha256,
    thumbnail_length,
    MediaType::LinkThumbnail,
);

let thumbnail = client.download_from_params(&params).await?;
```

## Forwarding media via CDN reuse

When you receive a media message (image, video, audio, document, sticker), you can "forward" it by reusing the original CDN fields directly — no download or re-upload needed. This is instant regardless of file size because no media bytes are transferred.

The technique works because every media message contains all the CDN metadata (`url`, `direct_path`, `media_key`, `file_sha256`, etc.) needed for recipients to fetch the blob. You simply clone those fields into a new `wa::Message` and call `send_message`:

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

/// Reuses the original CDN blob, only swaps the caption.
/// Instant regardless of file size — no download or re-upload.
fn forward_image(original: &wa::message::ImageMessage) -> wa::Message {
    wa::Message {
        image_message: buffa::MessageField::some(wa::message::ImageMessage {
            caption: Some("Forwarded image".to_string()),
            ..original.clone() // reuses url, direct_path, media_key, hashes, thumbnail, etc.
        }),
        ..Default::default()
    }
}

// In your message handler:
if let Some(img) = message.image_message.as_option() {
    let forwarded = forward_image(img);
    client.send_message(destination_jid, forwarded).await?;
}
```

The same pattern works for any media type:

```rust theme={null}
// Forward a video, keeping all CDN fields intact
if let Some(vid) = message.video_message.as_option() {
    let forwarded = wa::Message {
        video_message: buffa::MessageField::some(wa::message::VideoMessage {
            caption: Some("Forwarded video".to_string()),
            ..vid.clone()
        }),
        ..Default::default()
    };
    client.send_message(destination_jid, forwarded).await?;
}
```

<Tip>
  Only use `download` + `upload` when you need the plaintext bytes (processing, transcoding, re-encoding). To forward existing media unchanged, reuse the original message's CDN fields directly.
</Tip>

### When CDN reuse won't work

CDN URLs expire after some time. If the original media URL has expired, recipients won't be able to download it. In that case, use [media reupload](/api/client#media_reupload) to request the server to provide a fresh download path, or fall back to downloading and re-uploading the media.

## Uploading Media

### Basic Upload

```rust theme={null}
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, Default::default()).await?;

println!("URL: {}", upload.url);
println!("Direct path: {}", upload.direct_path);
println!("File SHA256: {:?}", upload.file_sha256);
```

See [Upload API reference](/api/upload) for full details.

### Using upload response in messages

The `whatsapp_rust::media` module turns an `UploadResponse` into a ready-to-send `wa::Message`, so you don't hand-assemble the CDN/crypto fields (url, direct\_path, media\_key, the two SHA-256 hashes, file\_length, media\_key\_timestamp, streaming\_sidecar):

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

let upload = client.upload(image_data, MediaType::Image, Default::default()).await?;

let message = media::image_message(upload, ImageOptions {
    caption: Some("Check out this image!".to_string()),
    ..Default::default()
});

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

`media::video_message`, `media::document_message`, and `media::audio_message` work the same way with their own options structs (and carry the streaming sidecar for video/audio). See [media message builders](/api/upload#high-level-message-builders).

<Accordion title="Building the message by hand">
  You can still assemble the proto yourself when you need a field the options structs don't expose (e.g. explicit `width`/`height`):

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

  let upload = client.upload(image_data, MediaType::Image, Default::default()).await?;

  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_sha256: Some(upload.file_sha256_vec()),
          file_enc_sha256: Some(upload.file_enc_sha256_vec()),
          file_length: Some(upload.file_length),
          media_key_timestamp: Some(upload.media_key_timestamp),
          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 result = client.send_message(to, message).await?;
  ```
</Accordion>

### Sending media as an album

You can group multiple images and/or videos into an album. See [Sending Messages - Album messages](/guides/sending-messages#album-messages) for a full walkthrough of the parent/child album pattern using `AlbumMessage` and `wrap_as_album_child`.

### Resumable uploads

For files 5 MiB or larger, the client automatically checks whether a previous upload of the same file exists on the CDN before uploading the full payload. This avoids re-uploading data that the server already has.

The resume check flow works as follows:

1. The client sends a `?resume=1` probe to the CDN for each host
2. If the server responds with **complete**, the existing URL is returned immediately — no upload occurs
3. If the server responds with a **byte offset**, only the remaining bytes are uploaded using the `file_offset` query parameter
4. If the server responds with **not found** (or the check fails), a full upload proceeds

<Note>
  Resumable upload detection is fully transparent. You call `client.upload()` the same way regardless of file size. The resume check is non-fatal — if it fails for any reason, the client falls back to a full upload.
</Note>

```rust theme={null}
// Large files automatically use resumable upload logic
let video_bytes = std::fs::read("large_video.mp4")?; // e.g., 50 MiB
let upload = client.upload(video_bytes, MediaType::Video, Default::default()).await?;
// If a partial upload existed, only the remaining bytes were sent
```

## Media Types

### Available media types

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

### Media type properties

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

// Get MMS type for upload URLs
let mms_type = MediaType::Image.mms_type();
// Returns: "image", "video", "audio", "document"

// Get app info string for HKDF key derivation
let info = MediaType::Video.app_info();
// Returns: "WhatsApp Video Keys"

// Get upload path for CDN routing
let path = MediaType::Image.upload_path();
// Returns: "/mms/image"

// Check if media type uses E2E encryption
let encrypted = MediaType::ProductCatalogImage.is_encrypted();
// Returns: false (product catalog images are unencrypted)
```

<Note>
  `ProductCatalogImage` is the only unencrypted media type. It uploads to `/product/image` instead of the `/mms/` prefix used by other types. This matches WhatsApp Web's behavior where `CreateMediaKeys.js` skips encryption for product catalog images.
</Note>

## Encryption Details

### Upload Encryption

Media is automatically encrypted before upload:

1. Generate random 32-byte `media_key`
2. Derive encryption keys using HKDF-SHA256:
   * IV: 16 bytes
   * Cipher key: 32 bytes
   * MAC key: 32 bytes
3. Encrypt with AES-256-CBC
4. Append HMAC-SHA256 MAC (10 bytes)
5. Compute SHA256 hashes of plaintext and ciphertext

```rust theme={null}
// Encryption happens automatically in upload()
let data = std::fs::read("file.pdf")?;
let upload = client.upload(data, MediaType::Document, Default::default()).await?;

// The UploadResponse contains all encryption metadata
// media_key, file_sha256, file_enc_sha256 are [u8; 32] arrays
println!("Media key: {:?}", upload.media_key);
println!("File length: {} bytes", upload.file_length);
```

### Chunk-based encryption with MediaEncryptor

`MediaEncryptor` provides a chunk-based API for encrypting media incrementally. Unlike `encrypt_media_streaming`, it doesn't require a sync `Read` source — you can feed plaintext in arbitrarily sized chunks, making it suitable for async streams and network sources:

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

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

// Feed plaintext in any chunk size
for chunk in video_chunks {
    enc.update(&chunk, &mut encrypted);
}

// Finalize: PKCS7 pad + append 10-byte HMAC-SHA256 MAC
let info = enc.finalize(&mut encrypted)?;
// info.media_key, info.file_sha256, info.file_enc_sha256, info.file_length
```

You can also write directly to a file or writer instead of buffering in memory:

```rust theme={null}
let mut enc = MediaEncryptor::new(MediaType::Video)?;
let mut writer = File::create("large_video.enc")?;

for chunk in video_chunks {
    enc.update_to_writer(&chunk, &mut writer)?;
}

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

See [Upload API reference](/api/upload#mediaencryptor) for full details.

### Streaming encryption from a reader

For sync `Read` sources, `encrypt_media_streaming` wraps `MediaEncryptor` and reads in 8KB chunks:

```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)?;
// info.media_key, info.file_sha256, info.file_enc_sha256, info.file_length
```

See [Upload API reference](/api/upload#streaming-encryption) for full details.

### Download Decryption

Decryption is automatic when downloading:

1. Fetch encrypted data from WhatsApp CDN
2. Verify MAC (last 10 bytes)
3. Decrypt with AES-256-CBC
4. Verify plaintext SHA256
5. Return decrypted data

```rust theme={null}
// Decryption happens automatically in download()
let data = client.download(image_message).await?;
// `data` is already decrypted and verified
```

<Warning>
  Never skip SHA256 verification! The library automatically verifies both encrypted and decrypted hashes to ensure data integrity.
</Warning>

## Error Handling

### Download Errors

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

```rust theme={null}
async fn safe_upload(
    client: &Client,
    data: Vec<u8>,
    media_type: MediaType,
) -> Result<UploadResponse> {
    match client.upload(data, media_type, Default::default()).await {
        Ok(upload) => {
            println!("✅ Uploaded to: {}", upload.url);
            Ok(upload)
        }
        Err(e) => {
            eprintln!("❌ Upload failed: {:?}", e);
            Err(e)
        }
    }
}
```

## Advanced Usage

### Custom media processing

```rust theme={null}
use image::DynamicImage;

// Download and process image
if let Some(img) = message.image_message.as_option() {
    let data = client.download(img).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

```rust theme={null}
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, progress_writer).await?;
```

### Automatic retry and failover

Both `download` and `upload` methods automatically retry when WhatsApp's CDN returns certain HTTP errors. The client handles three categories of errors:

**Auth errors (401/403):** The client invalidates the cached `MediaConn`, fetches fresh credentials from WhatsApp's servers, and retries the operation once with new auth tokens.

**Media not found (404/410):** When a download URL has expired or the media has been moved, the CDN returns 404 or 410. The client treats this the same as an auth error — it invalidates the cached media 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. Media connections include multiple hosts sorted by priority (primary hosts first, then fallback hosts), and the client iterates through them sequentially.

<Note>
  This retry behavior is fully transparent — no changes are needed in your code. Both `download` and `upload` handle auth refresh, URL re-derivation, and host failover automatically, with a maximum of one credential refresh per operation.
</Note>

For streaming downloads (`download_to_writer`), the writer is automatically seeked back to position 0 before each retry so partial writes don't corrupt the output.

### Additional retry logic

For transient errors beyond auth failures, you can implement your own retry wrapper:

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

## Media connection

### Automatic management

The client automatically manages media connection tokens, including refreshing them when they expire or when the CDN rejects them with 401/403/404/410:

```rust theme={null}
// Automatically refreshed when expired or rejected
let upload = client.upload(data, MediaType::Image, Default::default()).await?;
let download = client.download(downloadable).await?;

// No manual token management needed!
```

### Host types and failover order

Each media connection includes multiple CDN hosts with a `host_type` field indicating whether the host is `"primary"` or `"fallback"`. The client sorts hosts with primary hosts first, so uploads and downloads always try the fastest hosts before falling back to alternatives. Each host may also include a `fallback_hostname` for additional resilience.

### Token expiration

Media connections have two TTL values:

* **`ttl`** — how long the route/host information is valid
* **`auth_ttl`** — how long the auth token specifically is valid (may be shorter than `ttl`)

The client uses the minimum of these two values to determine when the cached connection has expired. When expired, the next media operation automatically fetches fresh credentials.

### Internal refresh

Media connection refresh and invalidation are managed internally by the client. When the CDN rejects a request (401/403/404/410), the client automatically invalidates the cached connection and fetches fresh credentials before retrying. You don't need to manage this manually.

## Best Practices

<Steps>
  ### Use streaming for large files

  ```rust theme={null}
  // ❌ Bad: Loads entire file into memory
  let data = client.download(video).await?;
  std::fs::write("video.mp4", data)?;

  // ✅ Good: Constant memory usage (with streaming HTTP client)
  let file = File::create("video.mp4")?;
  let _file = client.download_to_writer(video, file).await?;
  ```

  <Note>
    `download_to_writer` works with any HTTP client. With streaming-capable clients (like `UreqHttpClient`), memory usage is constant \~40KB. With non-streaming clients, it falls back to buffered downloads automatically.
  </Note>

  ### Verify media type

  Check `mimetype` before processing:

  ```rust theme={null}
  if let Some(img) = message.image_message.as_option() {
      if let Some(mimetype) = &img.mimetype {
          match mimetype.as_str() {
              "image/jpeg" | "image/png" => {
                  // Process image
              }
              _ => {
                  eprintln!("Unsupported image type: {}", mimetype);
              }
          }
      }
  }
  ```

  ### Handle missing fields

  Media messages may have optional fields:

  ```rust theme={null}
  if let Some(doc) = message.document_message.as_option() {
      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);
  }
  ```

  ### Set proper dimensions

  For images and videos, include dimensions:

  ```rust theme={null}
  use image::GenericImageView;

  let img = image::open("photo.jpg")?;
  let (width, height) = img.dimensions();

  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_sha256: Some(upload.file_sha256_vec()),
          file_enc_sha256: Some(upload.file_enc_sha256_vec()),
          file_length: Some(upload.file_length),
          media_key_timestamp: Some(upload.media_key_timestamp),
          mimetype: Some("image/jpeg".to_string()),
          width: Some(width),
          height: Some(height),
          ..Default::default()
      }),
      ..Default::default()
  };
  ```
</Steps>

## Next Steps

* [Sending Messages](/guides/sending-messages) - Send media with captions and quotes
* [Receiving Messages](/guides/receiving-messages) - Handle incoming media
* [Custom Backends](/guides/custom-backends) - Implement custom HTTP clients for media operations
