client.message.send method, using a typed media content object. The builder fills in the protocol-managed fields (encryption keys, SHA-256 digests, direct path, upload) for you — you provide the source and, optionally, a mimetype.
Mimetype resolution
mimetype is optional. The builder resolves it in this order:
- The
mimetypeyou pass on the content object wins. - If a
WaMediaProcessorwithdetectMimetypeis configured, the builder calls it (sniffing magic bytes).@zapo-js/media-utilsimplements this on top offile-type^19 — installfile-typeto enable detection. - Otherwise the builder throws for
image/video/audio/document/ptvmessages.
image/webp when no mimetype is set. Readable stream inputs with no mimetype are staged to a temp file before detection runs.
Media input
Themedia field accepts several input types:
Images
Video
type: 'ptv' with the same shape.
Audio & voice notes
Documents
Stickers
type: 'sticker-pack' with stickers, a trayIcon, and pack metadata (stickerPackId, name, publisher).
View-once
Wrap image/video/audio as view-once with the send option:Pre-upload and reuse
Sometimes you want to encrypt and upload media once and then reference the same descriptor across multiple sends — a broadcast asset, a stock reply, or building a raw proto payload yourself.client.message.upload(source, options) runs the encrypt / media_conn / CDN upload / parse flow the send path uses, but returns the descriptor without sending anything.
media_conn IQ). Uint8Array sources take a zero-temp-file fast path; a file path or Readable stream is staged to a temp file so the encrypt pass can hash it deterministically.
WaUploadMediaOptions
WaMediaUploadResult
Standalone crypto & transfer
For workflows that need to run encryption/decryption outside a session — a background worker crunching stored ciphertext, a custom relay —WaMediaCrypto and WaMediaTransferClient (plus their result and option types) are re-exported from the package root:
upload / download paths use; the coordinator method just wraps them with the session-bound media_conn handshake.
Downloading incoming media
The message coordinator decrypts and downloads media from an incoming event. Three flavors are available — prefer the streaming ones:WaIncomingMessageEvent or a raw Proto.IMessage, plus optional WaDownloadMediaOptions (for example maxBytes to cap downloadBytes).
Without a connected client
downloadMediaMessage is a free function that mirrors client.message.download but does not need a paired session. The encrypted-media metadata travels inside the (already decrypted) message itself, so you can re-download media from a persisted event long after the original socket is gone — useful for offline workers, archive replays, or anything that processes stored messages without spinning up a WaClient.
WaIncomingMessageEvent or a raw Proto.IMessage, returns a Readable you own (pipe it or .destroy() it — an unconsumed stream leaks the socket). MAC + SHA-256 verification runs as bytes are consumed, same semantics as the coordinator method. Throws when the message has no downloadable media.
For lower-level access — when you want to do the CDN fetch yourself, hand the keys to another process, or just inspect what’s downloadable —
resolveMediaPayload returns the keys + hashes without doing any I/O:
null when the message has no downloadable media, or when the proto carried no directPath / mediaKey. It unwraps ephemeralMessage, viewOnceMessage / viewOnceMessageV2, and documentWithCaptionMessage before resolving. Supported kinds: image, video (gif when gifPlayback), audio (ptt when ptt), document, sticker, ptv.
Requesting a reupload
The CDN drops old media blobs — adownload* on a message surfaced by history sync can answer 404/410 well after the original send. client.message.requestMediaReupload() runs the round-trip WhatsApp Web uses to recover: encrypt a server-error receipt with the message’s media key, send it as an ack, and await the mediaretry notification the sender’s primary device answers with. On success only the directPath changes — the media key, hashes, and length of the original message stay valid, so downloading with a spread of the new path works.
WaMediaRetryRequest ({ messageId, chatJid, mediaKey, fromMe, participant? }) when you hold the ids and the media key but not the decoded message — the event form pulls those out for you and rejects newsletter messages plus messages with no downloadable media. options.timeoutMs bounds the wait for the notification; the coordinator does not throw on not_found / general_error (the sender no longer holds the file, or the primary could not re-seal it) — check result.result for one of 'success' | 'not_found' | 'decryption_error' | 'general_error'.
Requests are deduplicated by
messageId: concurrent calls for the same message share one round-trip and one server-error receipt.Media processing
For proper media, use a media processor. Install@zapo-js/media-utils and pass one through the media client option — it probes and processes media (dimensions, duration, thumbnails, waveforms, voice-note normalization) before upload. Without it, media still uploads but lacks this processing:
@zapo-js/media-utils shells out to ffmpeg/ffprobe and uses sharp. Make sure those binaries are available in your environment.