Messages and message events
Send, receive, reply to, and monitor WhatsApp messages.
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);
});| Field | Description |
|---|---|
id | The message ID. Pass this to reply, forwardMessages, or lookup methods. |
body | Text content for text messages. For media messages, this can be empty. |
type | WhatsApp message type, such as chat, image, video, document, ptt, location, buttons_response, or list_response. |
from | The chat that sent the message. In direct chats this is the contact ID. In groups this is the group chat ID. |
to | The chat or account that received the message. |
chatId | The chat ID for the conversation. |
sender | Contact details for the sender. |
senderId | Sender contact ID when available. Useful in groups. |
author | The group participant that sent the message. Present on many group messages. |
fromMe | true when the current session sent the message. |
self | Direction marker, usually in or out. |
timestamp / t | Unix timestamp values from WhatsApp. |
isGroupMsg | true when the message came from a group. |
isMedia / isMMS | Media flags. Use these before downloading or processing attachments. |
caption | Caption for image, video, or document messages. |
mentionedJidList | Contact IDs mentioned in the message. |
ack | Delivery state for outgoing messages. Read onAck for updates. |
quotedMsg / quotedMsgObj | Quoted message payload when WhatsApp makes it available. |
isQuotedMsgAvailable | Whether 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 case | What to check |
|---|---|
| Invalid chat ID | Confirm the value matches a contact, group, or LID format. See ChatId primer. |
| Session not ready | Wait for authentication and connection before sending. |
| Media rejected | Check the MIME type, base64 data URL, filename, and file size. |
| Message not delivered | Watch onAck; retry only from one bounded owner after reviewing the failure. |
| Provider restriction | Pause 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.
