Skip to main content

upload

Upload media to WhatsApp’s CDN with automatic encryption.
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.
pub async fn upload(
    &self,
    data: Vec<u8>,
    media_type: MediaType,
    options: UploadOptions,
) -> Result<UploadResponse, anyhow::Error>
data
Vec<u8>
required
Raw media bytes to upload. The data is automatically encrypted using AES-256-CBC before uploading.
media_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)
options
UploadOptions
required
Upload options. Use Default::default() or UploadOptions::new() for default behavior. See UploadOptions below.
UploadResponse
struct
Contains all metadata needed to include the media in a message:
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,
}
Helper methods are provided for protobuf message construction, which requires Vec<u8>:
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> { ... }
}
url
String
Full CDN URL where the encrypted file was uploaded
direct_path
String
CDN path component (e.g., /v/t62.7118-24/12345_67890). Used for downloads.
media_key
[u8; 32]
32-byte encryption key. Required for the recipient to decrypt the media. Use media_key_vec() when building protobuf messages.
file_enc_sha256
[u8; 32]
SHA-256 hash of the encrypted file. Used for integrity verification during download. Use file_enc_sha256_vec() when building protobuf messages.
file_sha256
[u8; 32]
SHA-256 hash of the original (decrypted) file. Used for final validation after decryption. Use file_sha256_vec() when building protobuf messages.
file_length
u64
Original file size in bytes (before encryption)
media_key_timestamp
i64
Unix timestamp (seconds) when the media key was generated. Set automatically by upload() to the current time.

Example: Upload and Send Image

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: Some(Box::new(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

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

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: Some(Box::new(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)

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: Some(Box::new(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?;

UploadOptions

Options for customizing upload behavior.
#[non_exhaustive]
pub struct UploadOptions {
    /// Reuse an existing media key instead of generating a fresh one.
    pub media_key: Option<[u8; 32]>,
}
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.
media_key
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.

Creating options

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());

Example: Upload sticker pack thumbnail with shared key

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
pub struct MediaEncryptor { /* ... */ }

Creating an encryptor

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)?;
media_type
MediaType
required
Type of media being encrypted. Determines the HKDF info string for key derivation.
media_key
[u8; 32]
Caller-supplied 32-byte key for with_key(). Must be cryptographically random — reusing keys breaks confidentiality.

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).
// 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)?;
plaintext
&[u8]
required
Plaintext bytes to encrypt. Can be any size — from a single byte to the entire file.
On update_to_writer I/O error, the encryptor state is unspecified — discard it.

Finalizing

Finalize applies PKCS7 padding to the remaining plaintext, encrypts the final block(s), and appends a 10-byte truncated HMAC-SHA256 MAC.
// Finalize to Vec
let info = enc.finalize(&mut encrypted)?;

// Or finalize to writer
let info = enc.finalize_to_writer(&mut writer)?;
EncryptedMediaInfo
struct
Encryption metadata (keys and hashes). The encrypted data was already written via update/finalize calls.
pub struct EncryptedMediaInfo {
    pub media_key: [u8; 32],
    pub file_sha256: [u8; 32],
    pub file_enc_sha256: [u8; 32],
    pub file_length: u64,
}
media_key
[u8; 32]
32-byte encryption key for the recipient to decrypt the media
file_sha256
[u8; 32]
SHA-256 hash of the original plaintext, computed on the fly during encryption
file_enc_sha256
[u8; 32]
SHA-256 hash of the encrypted output (ciphertext + MAC)
file_length
u64
Original file size in bytes (before encryption)

Example: Chunk-based encryption to Vec

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

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)?;
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.

Streaming encryption

For sync Read sources, encrypt_media_streaming provides a convenience wrapper around MediaEncryptor that reads in 8KB chunks:
pub fn encrypt_media_streaming<R: Read, W: Write>(
    reader: R,
    writer: W,
    media_type: MediaType,
) -> Result<EncryptedMediaInfo>
reader
R: Read
required
Source of plaintext media bytes. Can be a File, Cursor<Vec<u8>>, or any Read implementor.
writer
W: Write
required
Destination for encrypted output. Receives ciphertext + 10-byte HMAC-SHA256 MAC — the exact bytes to upload to WhatsApp CDN.
media_type
MediaType
required
Type of media being encrypted. Determines the HKDF info string for key derivation.

Example: Encrypt from file to file

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);
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.

Relationship to encrypt_media

The in-memory encrypt_media function is a convenience wrapper around encrypt_media_streaming:
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

APIUse when
MediaEncryptorYou have an async stream, network source, or need full control over chunk sizes
encrypt_media_streamingYou have a sync Read source (file, cursor)
encrypt_mediaThe entire plaintext fits in memory

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

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:
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::ProductCatalogImageN/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_keyReceive media_key in message
Derive IV, cipher key, MAC keyDerive same keys from media_key
Encrypt with AES-256-CBCDecrypt with AES-256-CBC
Append HMAC-SHA256 (10 bytes)Verify HMAC-SHA256
Upload to CDNDownload from CDN

MediaType

Specifies the type of media for encryption and CDN routing.
pub enum MediaType {
    Image,
    Video,
    Audio,
    Document,
    History,
    AppState,
    Sticker,
    StickerPack,
    StickerPackThumbnail,
    LinkThumbnail,
    ProductCatalogImage,
}
Image
variant
JPEG, PNG, or other image formats. Uses /mms/image endpoint and "WhatsApp Image Keys" for HKDF.
Video
variant
MP4 or other video formats. Uses /mms/video endpoint and "WhatsApp Video Keys" for HKDF.
Audio
variant
Audio files and voice notes. Uses /mms/audio endpoint and "WhatsApp Audio Keys" for HKDF.
Document
variant
PDF, DOCX, ZIP, and other document formats. Uses /mms/document endpoint and "WhatsApp Document Keys" for HKDF.
Sticker
variant
Sticker images (WebP format). Uses /mms/image endpoint and "WhatsApp Image Keys" for HKDF (same as images).
History
variant
History sync data. Uses /mms/md-msg-hist endpoint and "WhatsApp History Keys" for HKDF.
AppState
variant
App state sync data. Uses /mms/md-app-state endpoint and "WhatsApp App State Keys" for HKDF.
StickerPack
variant
Sticker pack ZIP files. Uses /mms/sticker-pack endpoint and "WhatsApp Sticker Pack Keys" for HKDF.
StickerPackThumbnail
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(...).
Link preview thumbnails. Uses /mms/thumbnail-link endpoint and "WhatsApp Link Thumbnail Keys" for HKDF.
ProductCatalogImage
variant
Product catalog images for WhatsApp Business. Unencryptedis_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.

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

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

HTTP 4xx/5xx
error
CDN returned error status. Auth errors (401/403) are retried automatically with fresh credentials. Other errors are tried against alternate CDN hosts.
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 */ }
}
No media hosts
error
Media connection has no available CDN hosts.
Err(anyhow!("No media hosts"))
Encryption failure
error
Failed to encrypt media (rare, usually indicates invalid input).
match client.upload(data, media_type, Default::default()).await {
    Err(e) if e.to_string().contains("encrypt") => {
        eprintln!("Encryption error: {}", e);
    }
    _ => {}
}

Example: Upload with additional retry

Auth errors (401/403) are retried automatically. For other transient failures, you can add your own retry logic:
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);