
How to Send WhatsApp Messages from TypeScript
whatsmeow-node gives you typed async methods for every kind of WhatsApp message — text, replies, mentions, media, polls, and reactions. This guide covers each one with TypeScript examples.
Prerequisites
- whatsmeow-node installed (Installation guide)
- A paired session (How to Pair WhatsApp)
Set Up the Client
import { createClient } from "@whatsmeow-node/whatsmeow-node";
const client = createClient({ store: "session.db" });
async function main() {
const { jid: myJid } = await client.init();
if (!myJid) {
console.error("Not paired — run the pairing flow first");
process.exit(1);
}
await client.connect();
await client.waitForConnection();
const recipient = "5512345678@s.whatsapp.net";
// ... send messages here
}
main().catch(console.error);
Send a Text Message
The simplest way — pass a conversation string:
const resp = await client.sendMessage(recipient, {
conversation: "Hello from TypeScript!",
});
console.log(`Sent with ID: ${resp.id}`);
sendMessage is the high-level method. It takes a JID and a MessageContent object, and returns a SendResponse with the message id and timestamp.
Reply with a Quote
To show the original message in a quote bubble, use sendRawMessage with contextInfo:
// Assume we received a message and have its info
client.on("message", async ({ info, message }) => {
if (info.isFromMe) return;
const text =
(message.conversation as string) ??
(message.extendedTextMessage as { text?: string } | undefined)?.text;
if (!text) return;
await client.sendRawMessage(info.chat, {
extendedTextMessage: {
text: `You said: "${text}"`,
contextInfo: {
stanzaId: info.id,
participant: info.sender,
quotedMessage: { conversation: text },
},
},
});
});
@Mention Users
Include JIDs in mentionedJid and use @<number> in the text:
await client.sendRawMessage(groupJid, {
extendedTextMessage: {
text: `Hey @${memberJid.split("@")[0]}, check this out!`,
contextInfo: {
mentionedJid: [memberJid],
},
},
});
To mention everyone in a group:
const group = await client.getGroupInfo(groupJid);
const jids = group.participants.map((p) => p.jid);
const mentions = jids.map((j) => `@${j.split("@")[0]}`).join(" ");
await client.sendRawMessage(groupJid, {
extendedTextMessage: {
text: `Attention: ${mentions}`,
contextInfo: { mentionedJid: jids },
},
});
Send Media
Upload first, then send the metadata with a raw message:
// Upload an image
const media = await client.uploadMedia("/path/to/photo.jpg", "image");
await client.sendRawMessage(recipient, {
imageMessage: {
URL: media.URL,
directPath: media.directPath,
mediaKey: media.mediaKey,
fileEncSHA256: media.fileEncSHA256,
fileSHA256: media.fileSHA256,
fileLength: String(media.fileLength),
mimetype: "image/jpeg",
caption: "Check this out!",
},
});
Other media types follow the same pattern — videoMessage, audioMessage, documentMessage, stickerMessage.
Upload response fields use exact protobuf casing: URL, fileSHA256, fileEncSHA256 — not url, fileSha256. Wrong casing silently fails.
Send a Poll
const resp = await client.sendPollCreation(
groupJid,
"Where should we eat?", // question
["Pizza", "Sushi", "Tacos"], // options
1, // max selectable
);
React to a Message
// Add a reaction
await client.sendReaction(chat, senderJid, messageId, "🔥");
// Remove a reaction (empty string)
await client.sendReaction(chat, senderJid, messageId, "");
Edit a Sent Message
const sent = await client.sendMessage(recipient, {
conversation: "Hello!",
});
// Edit it — only works on your own messages
await client.editMessage(recipient, sent.id, {
conversation: "Hello! (edited)",
});
Delete a Message
// Revoke for everyone — only your own messages, within the time limit
await client.revokeMessage(chat, myJid, sent.id);
Mark as Read
await client.markRead([info.id], info.chat, info.sender);
Complete Example
A bot that echoes text messages back as quoted replies:
import { createClient } from "@whatsmeow-node/whatsmeow-node";
import qrcode from "qrcode-terminal";
const client = createClient({ store: "session.db" });
client.on("error", (err) => console.error("Error:", err));
client.on("message", async ({ info, message }) => {
if (info.isFromMe) return;
const text =
(message.conversation as string) ??
(message.extendedTextMessage as { text?: string } | undefined)?.text;
if (!text) return;
await client.markRead([info.id], info.chat, info.sender);
await client.sendChatPresence(info.chat, "composing");
await client.sendRawMessage(info.chat, {
extendedTextMessage: {
text: `Echo: ${text}`,
contextInfo: {
stanzaId: info.id,
participant: info.sender,
quotedMessage: { conversation: text },
},
},
});
});
async function main() {
const { jid } = await client.init();
if (!jid) {
client.on("qr", ({ code }) => qrcode.generate(code, { small: true }));
await client.getQRChannel();
}
await client.connect();
console.log("Listening for messages...");
process.on("SIGINT", async () => {
await client.sendPresence("unavailable");
await client.disconnect();
client.close();
process.exit(0);
});
}
main().catch(console.error);
Common Pitfalls
sendMessage vs sendRawMessagesendMessage is for simple text. For anything structured (quotes, mentions, media), use sendRawMessage with the full protobuf shape.
info.chatIn groups, always send to info.chat (the group JID), not info.sender (the individual). Sending to info.sender starts a private conversation.
WhatsApp rate-limits sending. Space out messages, especially in groups. See Rate Limiting for details.







