Every builder follows the same flow. Understanding it helps when debugging payloads or building a custom delivery path.
setter/adder → internal state → build()/toCard() → WAMessage → relayMessage()
│
├─ resolve media
├─ await nested promises
└─ assemble Baileys structure
build()
build(jid, options) assembles a WAMessage without sending it. Media is resolved, promises are awaited, and the final structure is created with generateWAMessageFromContent().
const msg = await new Button(sock)
.setBody('Hello')
.addReply('OK', 'ok')
.build(jid, { messageId: 'custom-id' });
console.log(msg.key.id);
Use build() to:
- inspect the payload before sending;
- store a draft;
- deliver through your own mechanism;
- construct an edit message (
AIRich.buildEdit).
send()
send() calls build() and then relayMessage(). For Button, ButtonV2, and Carousel, the library adds the following XMPP node:
biz
└── interactive (type=native_flow, v=1)
└── native_flow (v=9, name=mixed)
This node is required for buttons to render. If you send a build() result manually, add the same node.
await sock.relayMessage(jid, msg.message, {
messageId: msg.key.id,
additionalNodes: [
{
tag: 'biz',
attrs: {},
content: [
{
tag: 'interactive',
attrs: { type: 'native_flow', v: '1' },
content: [{ tag: 'native_flow', attrs: { v: '9', name: 'mixed' } }],
},
],
},
],
});
The AIRich flow
AIRich sends in two stages:
- The main message contains
botForwardedMessage.message.richResponseMessage, including the base64unifiedResponseand fallbacksubmessages. - When
includesUnifiedResponseandbypassDownloadare enabled,send()callssendEdit()with the same payload.
The second stage makes clients synchronize the rich payload immediately instead of waiting for an additional download. _lastMessageKey is updated, so calling sendEdit() without arguments edits the most recent message.
const rich = new AIRich(sock).addText('Hello from AIRich');
await rich.send(jid);
// Edit the last message
rich.addText('Replacement text', { replace: 'old-block' });
await rich.sendEdit();
Editing messages
buildEdit() wraps the payload in a type 14 protocolMessage:
const edit = await rich.buildEdit(jid, messageId, {
msg: originalMessage.message,
});
sendEdit(jid, id) builds and relays the edit. When jid or id is omitted, the library uses the last message it sent.
Message IDs
By default, IDs are generated with generateMessageIDV2(). You can provide your own through messageId:
await button.send(jid, { messageId: 'message-' + Date.now() });
Never reuse the same ID for two different messages. When editing, use the target message’s ID, not a new one.
Common errors
| Symptom | Cause |
|---|---|
Socket is required |
The builder was created without a socket. |
interactiveMessage not found |
loadFrom() received the wrong message. |
| Buttons do not render | The native flow node is missing from a custom delivery path. |
Message id is required |
sendEdit() was called without an ID and there is no recent message. |
ITEM_NOT_FOUND |
The target ID for replace, insertAt, or delete does not exist. |
Production recommendations
- Store
msg.keyaftersend()for edits or auditing. - Wrap sends in try/catch and log
err.codeforAIRicherrors. - Do not reuse the same builder instance for multiple recipients.
- Test on real devices; rendering can differ between WhatsApp versions.