open-wa
DocsMessages and message events

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, document, ptt, location, buttons_response, or list_response.
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.
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 declares sendText as Promise<string | false>. The WAPI can return status strings such as Not a contact or Not able to send message to broadcast; the in-process Client turns Not a contact into a rejected Promise because sending to an unknown number requires a Restricted or Premium license. The Easy API schema also allows a boolean or a serialized ID object. A string is safe to pass to a follow-up method only when it has the message ID shape. Handle both returned values and rejected Promises.

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',
  'Receipt attached'
);

Put a real receipt.png in the process working directory before running this example. The embedded sendImage method accepts base64 data or a data URL; the snippet encodes the actual file instead of a placeholder payload. Use sendText for plain conversation updates. The current embedded surface marks sendFile unsupported and does not declare sendAudio, sendPtt, or sendVideoAsGif; those names belong to the generated Easy API schema, so check their method references and runtime support before using them. Captions belong to the attachment message, not a separate text message.

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'],
  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. Poll updates arrive through WhatsApp message events when supported by the session. Handle unsupported or rejected sends explicitly.

Forward messages

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

Read receipts and typing state

client.onAck((ack) => {
  console.log(ack);
});

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.

try {
  const result = await client.sendText(chatId, 'Hello');

  const messageId = typeof result === 'string'
    ? result
    : typeof result === 'object' && result !== null
      ? result._serialized
      : null;

  if (!messageId || !/^(true|false)_.+_.+$/.test(messageId)) {
    console.warn('Message send did not return a usable message ID', { result });
  }
} catch (error) {
  console.error('Could not send message', error);
}

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

This example creates the embedded runtime, wraps it in the Client facade, starts the session, and replies to an incoming command. It uses the same connected-client lifecycle as the custom-code guide.

import { Client, createClient } from '@open-wa/wa-automate';
import { PuppeteerDriver } from '@open-wa/driver-puppeteer';

async function main(): Promise<void> {
  const runtime = await createClient({
    sessionId: 'messages-guide',
    driver: new PuppeteerDriver(),
    headless: false,
  });

  const client = new Client({
    client: runtime,
    transport: runtime.getTransport(),
  });

  await client.start();
  const readiness = runtime.getReadiness();
  if (!readiness.ready) {
    throw new Error(`Session is not ready: ${readiness.pending.join(', ') || 'unknown blocker'}`);
  }

  console.log(`Session ${runtime.sessionId} is ready`);

  client.onMessage(async (message) => {
    try {
      if (message.body !== '!help') return;

      const result = await client.reply(
        message.from,
        'I received your request.',
        message.id
      );

      if (result === false) {
        console.warn('WhatsApp did not return a reply message ID');
      } else {
        console.log('Reply result:', result);
      }
    } catch (error) {
      console.error('Message handler failed', error);
    }
  });

  let stopping = false;
  const shutdown = async (signal: string): Promise<void> => {
    if (stopping) return;
    stopping = true;
    console.log(`Stopping after ${signal}...`);
    await client.stop(signal);
  };

  process.once('SIGINT', () => void shutdown('SIGINT'));
  process.once('SIGTERM', () => void shutdown('SIGTERM'));
}

main().catch((error: unknown) => {
  console.error('open-wa failed:', error);
  process.exitCode = 1;
});

Was this helpful?

Your answer includes the page path and docs version.

On this page