open-wa logoopen-wa
DocsGuides

Messages and message events

Send, receive, reply to, and monitor WhatsApp messages.

Messages and message events

Use this guide for the common message flows: receiving messages, sending replies, attaching media, and reacting to message metadata.

For chat ID formats such as 447700000000@c.us, group IDs, and LID IDs, read ChatId primer first.

Receive messages

client.onMessage((message) => {
  console.log(message.body);
});

client.onAnyMessage((message) => {
  console.log(message.body);
});

Use onMessage for incoming traffic only. Use onAnyMessage when you need incoming and outgoing traffic.

Message object shape

Incoming message objects include the message text, sender details, chat details, media flags, and quote metadata. WhatsApp can add extra fields, so treat the object as extensible.

client.onMessage((message) => {
  console.log(message.id);
  console.log(message.from);
  console.log(message.body);
  console.log(message.type);
});
FieldDescription
idThe message ID. Pass this to reply, forwardMessages, or lookup methods.
bodyText content for text messages. For media messages, this can be empty.
typeWhatsApp message type, such as chat, image, video, album, document, ptt, location, buttons_response, or list_response.
eventStructured event details for an event_creation message. See WhatsApp events and RSVPs.
fromThe chat that sent the message. In direct chats this is the contact ID. In groups this is the group chat ID.
toThe chat or account that received the message.
chatIdThe chat ID for the conversation.
senderContact details for the sender.
senderIdSender contact ID when available. Useful in groups.
authorThe group participant that sent the message. Present on many group messages.
fromMetrue when the current session sent the message.
selfDirection marker, usually in or out.
timestamp / tUnix timestamp values from WhatsApp.
isGroupMsgtrue when the message came from a group.
isMedia / isMMSMedia flags. Use these before downloading or processing attachments.
captionCaption for image, video, or document messages.
expectedImageCount / expectedVideoCountMedia counts declared by an album container. They don't confirm that all children have arrived. GIF videos contribute to the video count.
parentMsgKey / associationTypeAssociated media's parent message ID and association type. Album children use MEDIA_ALBUM.
isGifWhether a video message plays as a WhatsApp GIF.
mentionedJidListContact IDs mentioned in the message.
ackDelivery state for outgoing messages. Read onAck for updates.
quotedMsg / quotedMsgObjQuoted message payload when WhatsApp makes it available.
isQuotedMsgAvailableWhether quote data can be read from the payload.

Send messages

await client.sendText(chatId, 'Hello');
await client.reply(chatId, 'Hello', messageId);

The minimum call is sendText(chatId, text). The generated sendText reference lists an optional options value retained for request compatibility, but the current Client implementation ignores it. Use reply with a message ID for a quoted reply. For mentions, use the generated Easy API method sendTextWithMentions with contact IDs such as 447700900000@c.us; the current embedded Client does not declare that method.

The in-process Client returns a validated serialized MessageId from sendText, or throws a typed SendTextError. A returned ID confirms submission, not delivery or reading. Subscribe to onAck before sending to observe later acknowledgements. Starting a chat with a new number requires an applied Restricted or Premium license; an existing chat is enough for this tutorial.

An error with outcome: 'unknown' means the message may already have been sent. Check the conversation before retrying. open-wa never retries sendText automatically.

Mention helpers are generated Easy API methods with their own parameter contract; they are not methods declared by the current embedded Client surface. When using that API surface, the call shape is:

await client.sendTextWithMentions(
  groupId,
  'Hello @447700000000',
  false,
  ['447700000000@c.us']
);
await client.sendReplyWithMentions(
  groupId,
  'Hello @447700000000',
  messageId,
  false,
  ['447700000000@c.us']
);

Formatting

WhatsApp uses inline formatting markers in text messages. Send the markers as part of the message body.

await client.sendText(chatId, '*bold*');
await client.sendText(chatId, '_italic_');
await client.sendText(chatId, '~strikethrough~');
await client.sendText(chatId, '```monospace```');

You can combine formatting with normal text.

await client.sendText(chatId, 'Order *confirmed* for _today_.');

Attachments vs text

Text messages only need a chat ID and a string body. Attachment messages include a file payload, a filename, and often a caption.

import { readFile } from 'node:fs/promises';

await client.sendText(chatId, 'Here is the receipt.');

const image = await readFile('./receipt.png');
const imageDataUrl = `data:image/png;base64,${image.toString('base64')}`;
await client.sendImage(
  chatId,
  imageDataUrl,
  'receipt.png',
  { caption: 'Receipt attached' }
);

Media methods accept a local path, URL, data URL, base64 string or Buffer. sendFile and sendImage both use the native attachment pipeline and detect image, video, audio or document presentation. Captions, quote IDs and typed presentation settings go in their options object. The embedded Client also provides sendAudio, sendPtt and sendVideoAsGif.

See Media for view-once images/videos/voice notes, circular video notes, document presentation and the allowed combinations.

Albums

The next release adds album receiving and sending. An album arrives with type: 'album', declared image/video counts, and an empty body. The container has no downloadable media; its images and videos are separate messages. Album children carry associationType: 'MEDIA_ALBUM' and a serialized parentMsgKey matching the container's ID. Captions belong to those children.

client.onMessage(async (message) => {
  if (message.type === 'album') {
    console.log('Album:', message.id,
      message.expectedImageCount, message.expectedVideoCount);
    const loaded = await client.getAlbumMessages(message.id);
    if (loaded !== false) console.log('Loaded items:', loaded.length);
  }
  if (message.associationType === 'MEDIA_ALBUM' && message.parentMsgKey) {
    console.log('Album item:', message.parentMsgKey, message.caption);
    const dataUrl = await client.decryptMedia(message);
    // Process this image or video in your application.
    console.log('Downloaded bytes as a data URL:', dataUrl.length);
  }
});

getAlbumMessages(albumId) returns the currently loaded image/video children, including GIF videos, or false if the container isn't loaded or the ID isn't an album. Its array can be empty or incomplete when the parent arrives before its children. Process child events as they arrive, or call the lookup again later; the lookup doesn't wait for every declared item. Pass each child to decryptMedia or downloadMedia.

Send an album from real media files:

import { readFile } from 'node:fs/promises';

const encode = async (path: string, mime: string) =>
  `data:${mime};base64,${(await readFile(path)).toString('base64')}`;

const result = await client.sendAlbum(chatId, [
  { file: await encode('./photo.jpg', 'image/jpeg'), filename: 'photo.jpg' },
  { file: await encode('./clip.mp4', 'video/mp4'), filename: 'clip.mp4' },
  { file: await encode('./loop.mp4', 'video/mp4'), filename: 'loop.mp4', isGif: true },
  { file: await encode('./photo-2.jpg', 'image/jpeg'), filename: 'photo-2.jpg' },
], 'One caption for the album');

console.log(result.albumId, result.messageIds);

Images and videos can be mixed. For a WhatsApp GIF, provide an MP4 video with isGif: true; this API doesn't convert GIF files to MP4. Documents and audio are rejected. WhatsApp controls album availability, its minimum item count and upload limits. Phone-number IDs (number@c.us) are resolved to LIDs using WhatsApp's contact mapping, querying it when needed. LID, group and channel IDs are used directly. The destination chat must already be loaded and allow sending. The optional fourth argument quotes a loaded message in that chat on the first album item; the caption is also attached to that first item.

The method prepares every item before submitting the container. It returns the parent ID and submitted media IDs in input order. These IDs confirm submission, not delivery or recipient rendering. If submission fails after the parent is created, the error includes the parent ID and submitted child IDs when the browser can report them. A lost connection can leave the outcome unknown, and the album may be partially sent, so inspect the chat before retrying. The method doesn't retry automatically.

Quoted replies

Use reply when you want the response to quote an existing message. Pass the destination chat, reply body, and original message ID.

client.onMessage(async (message) => {
  if (message.body === '!status') {
    await client.reply(message.from, 'Still working on it.', message.id);
  }
});

For replies that also mention users, use the generated Easy API method sendReplyWithMentions.

await client.sendReplyWithMentions(
  groupId,
  'Thanks @447700000000',
  messageId,
  false,
  ['447700000000@c.us']
);

Mentions

Mentions work in group messages when the body contains @ text and the mentioned contact IDs are passed to the send method.

await client.sendTextWithMentions(
  groupId,
  'Hello @447700000000, can you check this?',
  false,
  ['447700000000@c.us']
);

Set hideTags to true when you want WhatsApp to notify the users without showing the mention tags in the rendered text.

Buttons, lists, and polls

For new v5 integrations, use sendInteractive for buttons, lists, forms, carousels, or bookings. It requires an applied Insiders or higher license for the sending account. The guide covers the message format, response listeners, HTTP route, and recipient-side caveats.

sendButtons and sendListMessage are deprecated legacy methods and remain Insiders-gated. sendAdvancedButtons is also an Insiders-gated legacy method. Check their exact contracts in the sendButtons reference and sendListMessage reference before maintaining existing integrations. An available route does not prove that the session is licensed. For a fixed-choice flow without an Insiders gate, use the active sendPoll method when poll semantics fit the product.

await client.sendPoll(
  chatId,
  'Which day works best?',
  ['Monday', 'Tuesday', 'Wednesday'],
  { selectableCount: 1 }
);

Legacy button and list replies arrive as message events with types such as buttons_response or list_response. Interactive-message response events use the structured shapes described in the linked guide. Use onPollVote for decrypted vote updates and getPollResults for synced totals. See Polls and message pins for closing times, hidden voter names, voting and message pinning.

Forward messages

await client.forwardMessages('1234567890@c.us', [messageId], true);

Acknowledgements

Subscribe before sending to observe delivery changes:

const subscription = client.onAck((message) => {
  console.log(message.id, message.ack, message.body);
  if (message.sender) console.log('Sender:', message.sender.id);
});

The next release after 5.4.0 restores the full available message snapshot. It preserves fields supplied by WhatsApp, including the body, sender, chat, and extra metadata, without fetching a later version of the message. Some details may be absent while WhatsApp is loading it. In early v5 releases through 5.4.0, this callback instead receives { messageId, ack }.

message.ackMeaning
-1Error
0Pending
1Accepted by the server
2Delivered to the recipient's device
3Read
4Played

Call subscription.off() to remove the handler. A returned send ID confirms submission; it does not mean that delivery or reading has occurred.

Typing state

await client.simulateTyping(chatId, true);
await client.simulateTyping(chatId, false);

Location messages

await client.sendLocation(chatId, latitude, longitude, 'London');

When receiving a location message, read fields such as lat, lng, and loc from the incoming message payload.

Errors

A message send can fail when the session is not ready or the chat ID is incorrect. It can also fail when the user is not available. WhatsApp can reject a large, malformed, or prohibited payload.

import { SendTextError } from '@open-wa/wa-automate';

try {
  const messageId = await client.sendText(chatId, 'Hello');
  console.log('Submitted:', messageId);
} catch (error) {
  if (error instanceof SendTextError) {
    console.error(error.code, error.message);
    if (error.outcome === 'unknown') {
      console.error('Check the receiving chat before sending again.');
    }
  } else {
    throw error;
  }
}

INVALID_ARGUMENT, LICENSE_REQUIRED, and SEND_REJECTED report outcome: 'not_sent'. SEND_FAILED and INVALID_SEND_RESULT report outcome: 'unknown'; their cause retains the underlying failure or response.

Common checks:

Error caseWhat to check
Invalid chat IDConfirm the value matches a contact, group, or LID format. See ChatId primer.
Session not readyWait for authentication and connection before sending.
Media rejectedCheck the MIME type, base64 data URL, filename, and file size.
Message not deliveredWatch onAck; retry only from one bounded owner after reviewing the failure.
Provider restrictionPause or slow the relevant queue and retain the provider error. Timing alone does not prove that the account is clear.

Rate limits

WhatsApp can limit or block accounts that send too many messages or send the same content again. It can also block accounts that contact users without consent. Send messages at a controlled rate. Do not send bulk messages. Do not use short retry loops.

Read Rate limits before running production automation.

Full bot example

Create a project with the initializer, then replace index.ts with this example. Run bun run start, link the sending account if prompted, and send !help to it from your test chat.

import { create } from '@open-wa/wa-automate';

const client = await create();

client.onMessage(async (message) => {
  if (message.body !== '!help') return;
  try {
    await client.sendText(message.from, 'I received your request.');
  } catch (error) {
    console.error('Reply failed. Check the chat before retrying:', error);
  }
});

let stopping = false;
const shutdown = async () => {
  if (stopping) return;
  stopping = true;
  await client.stop();
};
process.once('SIGINT', () => void shutdown());
process.once('SIGTERM', () => void shutdown());

The process stays connected to receive messages. Stop it with Ctrl+C when you finish; the saved connection remains available for the next run.

Was this helpful?

Your answer includes the page path and docs version.

On this page