Concepts Mapping
How Chat SDK concepts like Thread, Channel, and Message map to WhatsApp via this adapter
The Chat SDK uses a platform-agnostic model (Thread, Channel, Message, etc.) that adapters translate to and from each platform's native concepts. This page explains how those Chat SDK concepts map to WhatsApp's model when using this adapter.
Quickstart
Events and Lifecycle
Extensions
Thread IDs and Multi-Account
Formatting and Media
Error Handling
Migrating from v1 to v2
Thread → WhatsApp conversation (JID)
In the Chat SDK, a thread is a container for a sequence of messages, typically a chat room, DM, or topic thread. On WhatsApp there are only two kinds of conversations:
- Direct message (DM): a one-on-one chat with another user. JID format:
[email protected] - Group chat: a group conversation with multiple participants. JID format:
[email protected]
Each WhatsApp JID maps 1-to-1 to a Chat SDK thread. There is no concept of sub-threads or topics inside a WhatsApp conversation, so a thread and its channel are always the same conversation.
// When the bot receives a message, thread.id contains the encoded JID:
bot.onSubscribedMessage(async (thread, message) => {
console.log(thread.id); // e.g. "baileys:MTU1NTEyMzQ1NjdAcy53aGF0c2FwcC5uZXQ"
console.log(thread.isDM); // true for DMs, false for groups
});Channel → same WhatsApp conversation
The Chat SDK has a Channel abstraction for platforms that distinguish between a top-level channel and threads inside it (e.g. Slack channels with message threads). WhatsApp has no such distinction; every conversation is flat.
Because of this:
channelIdFromThreadId()returns the thread ID unchanged.postChannelMessage()delegates topostMessage(), so posting to the "channel" is the same as posting to the conversation.listThreads()returns an empty array because there are no sub-threads to list.
In practice you won't call these methods directly; they're used by the Chat SDK internally.
Message model mapping
When Baileys delivers a WAMessage, the adapter converts it into a Chat SDK Message object. Here's how the fields map:
| Chat SDK field | WhatsApp / Baileys source |
|---|---|
message.id | msg.key.id |
message.text | conversation, extendedTextMessage.text, image/video/document captions |
message.formatted | Parsed AST from message.text via BaileysFormatConverter |
message.attachments | Populated for image, video, audio, and document messages |
message.author.userId | Sender's JID |
message.author.userName | msg.pushName (the sender's display name) |
message.author.isMe | true only for messages sent by this adapter instance |
message.author.isBot | true only for messages sent by this adapter instance |
message.metadata.dateSent | msg.messageTimestamp converted to a Date |
message.metadata.edited | Detected from editedMessage or protocol message type 14 |
message.metadata.fromMe | msg.key.fromMe, the Baileys flag for messages from the paired WhatsApp account |
Baileys' msg.key.fromMe means "sent from the paired WhatsApp account." Chat SDK's message.author.isMe means "sent by this bot/adapter." In chat-adapter-baileys, personal-phone messages from the paired account keep message.metadata.fromMe === true but use message.author.isMe === false, so they still reach handlers like onNewMessage. Messages posted through the adapter are tracked by Baileys message ID and are marked isMe/isBot when WhatsApp echoes them back, letting Chat SDK's central self-filter avoid feedback loops.
Example: log full message info when the bot receives a mention:
bot.onNewMention(async (thread, message) => {
console.log("Message ID:", message.id);
console.log("Author:", message.author.userName, "(", message.author.userId, ")");
console.log("Text:", message.text);
console.log("Sent at:", message.metadata.dateSent);
console.log("Is edited:", message.metadata.edited);
console.log("From paired WhatsApp account:", message.metadata.fromMe);
console.log("Attachments:", message.attachments.length);
});Reactions
WhatsApp supports emoji reactions on individual messages. The adapter maps the Chat SDK reaction methods to Baileys' react payload:
message.addReaction("👍"): sends a reaction with the given emoji to the message.message.removeReaction("👍"): sends an empty-string reaction, which WhatsApp interprets as removing any existing reaction.
bot.onSubscribedMessage(async (thread, message) => {
if (message.author.isMe) return;
// Acknowledge every message with a checkmark
await message.addReaction("✅");
// Later, if you want to remove it:
// await message.removeReaction("✅");
});You can also observe reactions from other users using the Chat SDK's onReaction handler (the adapter converts WhatsApp reaction events and passes them to the SDK):
bot.onReaction(["👍", "👎"], async (event) => {
const action = event.added ? "reacted with" : "removed";
console.log(`${event.user.userName} ${action} ${event.emoji}`);
});bot.onReaction() is a Chat SDK method, not an adapter-specific method. The adapter normalizes WhatsApp's reactionMessage payloads and routes them through chat.processReaction().
Typing indicator
thread.startTyping() sends WhatsApp's "composing" presence update, which shows the "typing..." indicator to other participants. This is a best-effort no-op: if the socket isn't connected when called, it silently does nothing.
bot.onNewMention(async (thread, message) => {
await thread.startTyping(); // shows "typing..." to the group
// Simulate some processing time
const reply = await generateReply(message.text);
await thread.post(reply);
});WhatsApp presence updates are rate-limited. Sending them too frequently may be ignored by clients.
Webhook model
Baileys connects to WhatsApp by opening a persistent outbound WebSocket to WhatsApp's servers. It does not listen for inbound HTTP requests. This means:
- You do not expose a webhook URL for WhatsApp to call.
handleWebhook()always returns HTTP501 Not Implemented.- Incoming messages arrive via the
messages.upsertBaileys event, which the adapter subscribes to internally.
If you have HTTP server code that routes to adapter.handleWebhook(), nothing will break; it just returns 501. To actually receive messages in gateway mode, call await bot.initialize() and then adapter.connect().
What handleWebhook returns
When called, handleWebhook() returns a Response with:
- Status:
501 Not Implemented - Body:
{"error":"Baileys adapter does not use HTTP webhooks. Call adapter.connect() to start the WhatsApp WebSocket connection."} - Content-Type:
application/json
This is helpful if you're building a multi-adapter bot that handles both WebSocket adapters (WhatsApp) and HTTP webhook adapters (Slack, Teams):
// Express example, works for both webhook and WebSocket adapters
app.post("/webhook/:adapter", async (req, res) => {
const adapter = bot.adapters[req.params.adapter];
if (!adapter) return res.status(404).send("Unknown adapter");
const response = await adapter.handleWebhook(req);
// WhatsApp returns 501, which is expected
if (response.status === 501) {
return res.status(200).send("WebSocket adapter: use connect(), not webhooks");
}
// Webhook adapters return 200 on success
res.status(response.status).send(await response.text());
});The explicit error message helps developers understand they've mixed up connection models.
Message history
fetchMessages() and fetchChannelMessages() both return empty arrays. WhatsApp's unofficial API (Baileys) does not expose a REST endpoint for fetching historical messages.
If you need message history, build your own store:
import { Chat } from "chat";
import type { WAMessage } from "baileys";
// Simple in-memory store (replace with a database in production)
const messageStore = new Map<string, WAMessage[]>();
const bot = new Chat({ /* ... */ });
// Persist every incoming message
bot.onAnyMessage(async (thread, message) => {
const jid = thread.id;
const existing = messageStore.get(jid) ?? [];
existing.push(message.raw);
messageStore.set(jid, existing);
});
// Query the store whenever you need history
function getHistory(threadId: string): WAMessage[] {
return messageStore.get(threadId) ?? [];
}Opening DMs proactively
You can obtain a thread ID for a DM and post to it without waiting for the other person to message first:
// Get a thread ID for a phone number (E.164 format, no "+")
const threadId = await whatsapp.openDM("15551234567");
// Post a message to that DM
await bot.postTo(threadId, "Hello from the bot!");openDM accepts either a bare phone number or a full JID ([email protected]).