Skip to main content
Media is sent through the same 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.
For usable media, install @zapo-js/media-utils and wire a processor through the media client option. Media still uploads without it, but without a processor it has no thumbnail/preview, dimensions, or waveform — so it may arrive as a plain attachment.

Mimetype resolution

mimetype is optional. The builder resolves it in this order:
  1. The mimetype you pass on the content object wins.
  2. If a WaMediaProcessor with detectMimetype is configured, the builder calls it (sniffing magic bytes). @zapo-js/media-utils implements this on top of file-type ^19 — install file-type to enable detection.
  3. Otherwise the builder throws for image/video/audio/document/ptv messages.
Stickers default to image/webp when no mimetype is set. Readable stream inputs with no mimetype are staged to a temp file before detection runs.

Media input

The media field accepts several input types:
Prefer a file path (string) or a Readable stream over a Buffer/Uint8Array. zapo streams media through the pipeline without buffering the whole file in memory — passing a path or stream keeps memory flat regardless of file size. Reading a large file into a Buffer first defeats that and is discouraged. (Buffer is also avoided internally in favor of Uint8Array.)

Images

Video

For a round push-to-video (PTV) message, use type: 'ptv' with the same shape.

Audio & voice notes

Voice notes render best as Opus in an OGG container. Enable media processing to auto-generate waveforms and normalize voice notes.

Documents

Stickers

For a full sticker pack, use 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:

Downloading incoming media

The message coordinator decrypts and downloads media from an incoming event. Three flavors are available — prefer the streaming ones:
download() / downloadToFile() stream the media and keep memory flat regardless of size. downloadBytes() materializes the whole file in memory — reach for it only on small media, and cap it with maxBytes.
All three accept either a 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.
Accepts a 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:
Returns 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.
payload.mediaKey is the AES/MAC seed for the encrypted blob — treat it like a secret. Don’t log it, don’t put it in error messages, and don’t ship it to a third-party service unless that’s the whole point of your pipeline.

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.
Last modified on June 3, 2026