open-wa logoopen-wa
DocsGuides

Media

Send attachments, view-once media and circular video notes with typed options.

Media

sendFile(to, input, filename?, options?) and sendImage use the same native attachment pipeline. Both accept images, videos, audio and documents as a local path, URL, data URL, base64 string or Buffer. The filename is optional; it is inferred from a recognized path/URL or the resolved MIME type. Captions and quoted-message IDs belong in options.

await client.sendImage(chatId, './photo.jpg', 'photo.jpg', { caption: 'Hello' });
await client.sendFile(chatId, './clip.mp4', 'clip.mp4', { caption: 'Watch this' });
await client.sendFile(chatId, './report.pdf', 'report.pdf', { caption: 'Report' });
await client.sendAudio(chatId, './sound.mp3');
await client.sendPtt(chatId, './voice.ogg');
await client.sendVideoAsGif(chatId, './loop.mp4');

Choose presentation

Import MediaSendOptions from @open-wa/client or @open-wa/schema. The discriminator selects the options that apply to that media type; unknown fields and invalid combinations are rejected.

typeOptionsBehavior
Omitted or autocaption, quotedMsgId, viewOnceDetect MIME. View once requires an image or video; detected audio rejects captions.
imagecaption, quotedMsgId, viewOnceRequire image media.
videocaption, quotedMsgId, viewOnce, isGif, videoNoteRequire video. GIF and circular notes require MP4.
audioquotedMsgId, ptt, viewOnceRequire audio. View once requires ptt: true.
documentcaption, quotedMsgIdForce document presentation, including for an image/video/audio file.

These methods prepare and upload the supplied media through WhatsApp Web; they do not transcode it or install a media-processing sidecar. WhatsApp's upload constraints and recipient support still apply. A returned message ID or boolean confirms submission, not delivery; observe onAck for later acknowledgements and inspect the chat before retrying an uncertain send.

View once

await client.sendFile(chatId, './photo.jpg', 'photo.jpg', {
  type: 'image', viewOnce: true, quotedMsgId: messageId,
});
await client.sendFile(chatId, './clip.mp4', 'clip.mp4', {
  type: 'video', viewOnce: true,
});
await client.sendPtt(chatId, './voice.ogg', { viewOnce: true });

For the generic method, the voice-note equivalent is { type: 'audio', ptt: true, viewOnce: true }. Documents and ordinary audio attachments cannot be view once. Quoting preserves the view-once flag.

Circular video notes and GIFs

await client.sendFile(chatId, './square-note.mp4', 'note.mp4', {
  type: 'video', videoNote: true,
});
await client.sendFile(chatId, './square-note.mp4', 'note.mp4', {
  type: 'video', videoNote: true, viewOnce: true,
});
await client.sendFile(chatId, './loop.mp4', 'loop.mp4', {
  type: 'video', isGif: true, caption: 'Looping clip',
});

Circular notes use WhatsApp's PTV message type. Supply an already prepared square MP4 with a positive duration of at most 60 seconds. This API does not crop or shorten it. Video notes have no caption and cannot also be GIFs. GIF-style messages carry MP4 video with a playback flag; GIFs cannot be view once. Audio voice notes should use suitable prepared audio such as Ogg Opus.

Send media as a document

await client.sendFile(chatId, './photo.jpg', 'original.jpg', {
  type: 'document', caption: 'Original file', quotedMsgId: messageId,
});

Document presentation retains the file MIME type and filename. Its caption is separate from the filename.

Remote URL auth

sendFileFromUrl accepts the same presentation options and a separate headers object for the download. The URL must return the media bytes.

await client.sendFileFromUrl(
  chatId,
  'https://files.example.com/invoices/123.pdf',
  'invoice-123.pdf',
  { type: 'document', caption: 'Invoice 123' },
  { Authorization: `Bearer ${process.env.FILE_API_TOKEN}` },
);

Data URLs and MIME

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', { viewOnce: true });

Use the actual media MIME type. For application/octet-stream, the client uses a recognized filename extension or file signature to identify the media. An unknown file defaults to a document. Base64 increases transfer size by about one third, and decoding/uploading also consumes browser and host memory.

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.

On this page