Media
Send files and media, then decrypt incoming content when your flow needs access to the raw bytes.
Send files and media
await client.sendImage(chatId, dataUrl, 'photo.jpg', 'caption');
await client.sendPtt(chatId, dataUrl);
await client.sendFile(chatId, dataUrl, 'file.pdf', 'check this pdf');
await client.sendFileFromUrl(chatId, url);
await client.sendImageAsSticker(chatId, dataUrl);
await client.sendStickerfromUrl(chatId, url);For media helpers that expect content payloads, prefer a proper data URL or a remote URL that your runtime can actually fetch.
Data URL creation
A data URL gives the runtime the file bytes and the MIME type in one string. Use this shape when sending media from disk, a buffer, or another service:
data:<mime-type>;base64,<base64-data>import { readFile } from 'node:fs/promises';
const buffer = await readFile('./photo.jpg');
const dataUrl = `data:image/jpeg;base64,${buffer.toString('base64')}`;
await client.sendImage(chatId, dataUrl, 'photo.jpg', 'caption');Set the MIME type to the real file type whenever you know it. application/octet-stream can work for generic documents, but image, audio, video, and sticker helpers often need a more specific value.
MIME rules
WhatsApp decides how to render media from the helper you call, the filename, and the MIME type in the data URL or remote response. Common safe values are:
| Media | MIME types to prefer |
|---|---|
| Images | image/jpeg, image/png, image/webp |
| GIFs | image/gif |
| Video | video/mp4, video/quicktime |
| Voice notes | audio/ogg with Opus audio |
| Audio files | audio/mpeg, audio/mp4, audio/ogg |
| Documents | application/pdf, text/plain, or the real document MIME type |
If a send fails or has the incorrect type, examine the data URL prefix first. Common problems include a missing ;base64 value or an image/jpg value instead of image/jpeg. A WebP image can fail in a non-sticker helper. A remote URL can return an auth page with text/html instead of the file.
Remote URL auth
sendFileFromUrl is useful when the file already lives behind HTTP. The runtime must be able to fetch the URL directly, so private files need either a short-lived signed URL or headers that your server accepts.
await client.sendFileFromUrl(
chatId,
'https://files.example.com/invoices/123.pdf',
'invoice-123.pdf',
'Invoice 123',
{
Authorization: `Bearer ${process.env.FILE_API_TOKEN}`,
}
);Use signed URLs when possible. They do not put long-lived credentials in the browser or runtime layer. If you use headers, make sure that the endpoint returns the file bytes and a correct Content-Type header.
File size limits
Media limits are enforced by WhatsApp and can change without notice. Treat these as practical limits to test against, not a permanent contract:
| Media type | Practical limit |
|---|---|
| Images | Keep under 16 MB |
| Video and GIF-style sends | Keep under 16 MB for the most reliable delivery |
| Voice notes and audio | Keep under 16 MB unless you have tested larger files in your target runtime |
| Stickers | Keep under 1 MB |
| Documents | WhatsApp Web can accept larger files, but keep automation payloads under 100 MB unless you have tested your account, browser, and host memory |
Base64 data URLs are larger than the original file by roughly one third. A 12 MB image becomes about 16 MB of string data before it reaches the runtime, so leave headroom for memory and transport overhead.
Video and GIF caveat
Video-style delivery depends on the browser/runtime surface you launch with. If you rely on Chrome-specific behavior, configure the browser path explicitly and test against the environment you deploy.
Decrypt incoming media
import { decryptMedia } from '@open-wa/wa-automate';
client.onMessage(async (message) => {
if (!message.mimetype) return;
const mediaData = await decryptMedia(message);
// persist, inspect, or re-send it from here
});DecryptMedia output
The standalone decryptMedia helper from the decrypt package returns a Node.js Buffer containing the raw decrypted bytes:
import { writeFile } from 'node:fs/promises';
import { decryptMedia } from '@open-wa/decrypt';
const buffer = await decryptMedia(message);
await writeFile('./incoming-media', buffer);The higher-level client method in the current client surface returns a data URL so it can be passed straight back into send helpers:
const dataUrl = await client.decryptMedia(message);
const base64 = dataUrl.split(',')[1] ?? '';
const buffer = Buffer.from(base64, 'base64');Use message.mimetype to choose the file extension or upload metadata. Decryption needs the media fields WhatsApp provides on the message, including the media URL, media key, type, MIME type, hash, and usually size.
Storage example
To save decrypted media to disk, convert the decrypted output into a buffer and write it with the right extension:
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import mime from 'mime-types';
import { decryptMedia } from '@open-wa/decrypt';
client.onMessage(async (message) => {
if (!message.mimetype) return;
const buffer = await decryptMedia(message);
const extension = mime.extension(message.mimetype) || 'bin';
const filename = `${message.id ?? Date.now()}.${extension}`;
const outputPath = path.join('media', filename);
await mkdir('media', { recursive: true });
await writeFile(outputPath, buffer);
});For S3, upload the same bytes and keep the MIME type as object metadata:
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { decryptMedia } from '@open-wa/decrypt';
const s3 = new S3Client({ region: 'us-east-1' });
client.onMessage(async (message) => {
if (!message.mimetype) return;
const buffer = await decryptMedia(message);
const key = `whatsapp/${message.id ?? Date.now()}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.MEDIA_BUCKET,
Key: key,
Body: buffer,
ContentType: message.mimetype,
}));
});If you want this handled automatically, use the S3 media integration instead of writing upload logic in every message handler.
Recovery helpers
Some older helpers such as stale-media recovery or sticker-specific decryption can depend on the runtime surface you are targeting. Validate those helpers in your deployment before baking them into a production flow.
Stale media recovery
Incoming media points to encrypted blobs on WhatsApp servers. Those URLs can expire or become unavailable, especially for older messages or messages that were not downloaded while the session was active.
When media is stale, decryption usually fails during the download step because the encrypted blob can no longer be fetched. Recovery helpers can sometimes ask the runtime to refresh the media reference, but success depends on WhatsApp still making the media available to that session.
For production flows, decrypt and store media when you receive it. If the media is necessary for compliance, support, or analytics, save it before you complete the workflow. Use disk, object storage, or the S3 integration.
Runtime limitations
Media handling depends on more than the open-wa method call. The browser driver, host memory, network access, Chrome feature support, and WhatsApp Web changes can all affect send and decrypt behavior.
Known limitations to plan around:
- remote URLs must be reachable from the runtime host, not just from your app server
- data URLs keep the full file in memory as a string, so large files can pressure Node.js and the browser
- video, GIF, sticker, and voice-note behavior is more runtime-sensitive than plain document sending
- stale media is not always recoverable after WhatsApp removes or expires the encrypted blob
- MIME type mismatches can make media send as a document, fail validation, or decrypt without a useful extension
- headless browser differences can affect media capture, conversion, and upload flows
Test the exact media types, sizes, and runtime driver you plan to deploy. Small local examples are not a guarantee that high-volume or large-file production traffic will behave the same way.
Was this helpful?
Your answer includes the page path and docs version.
