A feature-enhanced WhatsApp Web API library for Node.js
Maintained by BF667-IDLE · Based on Baileys by Adhiraj Singh
- About
- Changelog / What's New
- Detailed Changelog
- Installation
- Quick Start
- Configuration
- Features
- Auto WhatsApp Version Fetch
- Group Messages Fix (Issue #58)
- AI Message Icon (Sparkle Badge)
- Button Messages
- Interactive Messages (Native Flow)
- List / Row Messages
- Handling Button & Menu Responses
- Album Messages
- Newsletter (Channel) Management
- Audio & Voice Messages in Channels
- Custom Pairing Codes
- Connection Optimization
- Full Example
- API Reference
- Troubleshooting
- Credits & License
Baileys Premod is an enhanced fork of the Baileys WhatsApp Web library. It brings a collection of new features, critical bug fixes, and quality-of-life improvements that make it easier than ever to build WhatsApp bots, automation tools, and integrations.
The original Baileys library was created by Adhiraj Singh and later maintained by WhiskeySockets. This premod version builds on that solid foundation by addressing long-standing community issues and introducing powerful new capabilities that the upstream library lacks.
- Works with the latest WhatsApp Web versions via automatic version detection
- Buttons, interactive messages, and list menus now render correctly in groups (fixes issue #58)
- AI sparkle icon support for messages in both private chats and groups
- Full newsletter (channel) management API
- Album/media group message sending
- Custom pairing codes for streamlined device linking
- Zero external WhatsApp API dependency — uses the official WhatsApp Web protocol directly
| Change | Details |
|---|---|
nativeFlowResponseMessage direct extraction |
Handles unwrapped flow responses from newer WhatsApp clients |
carouselCardResponseMessage extraction |
Detects carousel card taps via card index and row ID |
interactiveResponseMessage fallback |
Parses paramsJson when body.text is missing |
meMsg.messageContextInfo safer default |
Prevents proto serialization issues on newer WhatsApp — defaults to {} instead of undefined |
| Duplicate comment cleanup | Removed 2x duplicate AI icon comments from compiled JS |
| See Changelog.md for full details |
| Change | Details |
|---|---|
| WhatsApp version | Updated to 2.3000.1036833994 with auto-fetch on startup |
| Group messages fix | Buttons, interactive messages, template messages, and list messages now display correctly in group chats |
| AI Message Icon | New ai parameter adds the sparkle/bot badge to messages — works in groups and private chats (based on nstar-y/bail) |
| Connection optimization | New optimizeConnection and maxMediaUploadRetryCount config options |
| Package rename | Published as baileys-premod to distinguish from the upstream package |
| Debug mode | New debugMessageSend option for verbose message-sending logs |
| Button response fix | extractMessageContent now properly handles interactiveResponseMessage, buttonsResponseMessage, listResponseMessage, and templateButtonReplyMessage |
| Carousel & shop extraction | extractMessageContent now extracts text from carouselMessage and shopStorefrontMessage interactive sub-types |
| Shop biz node fix | Shop messages (shopStorefrontMessage) now correctly receive the <biz> node for proper rendering |
| AdditionalNodes merge fix | ai: true no longer overrides user-provided additionalNodes — they are now merged together |
| botMessageSecret for newer WhatsApp | ai: true now generates a random 32-byte botMessageSecret in Message.messageContextInfo for compatibility with newer WhatsApp versions |
| ViewOnce wrapper detection | Biz node detection now covers listMessage and interactiveResponseMessage wrapped in viewOnce messages |
Add to your package.json:
{
"dependencies": {
"baileys": "github:BF667-IDLE/baileys-premod"
}
}Then run:
npm installES Modules:
import makeWASocket, { useMultiFileAuthState, DisconnectReason } from 'baileys'CommonJS:
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('baileys')- Node.js >= 20.0.0
- No WhatsApp account, phone number, or API key is required — everything runs through the WhatsApp Web protocol
Here's a minimal example to get a WhatsApp bot up and running with QR code authentication:
import { Boom } from '@hapi/boom'
import makeWASocket, { useMultiFileAuthState } from 'baileys'
async function start() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info')
const sock = makeWASocket({
auth: state,
printQRInTerminal: true, // print QR code to terminal
// autoFetchVersion is enabled by default in baileys-premod
})
sock.ev.on('creds.update', saveCreds)
sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update
if (connection === 'close') {
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode !==
DisconnectReason.loggedOut
if (shouldReconnect) {
start() // auto-reconnect
}
}
if (connection === 'open') {
console.log('Connected successfully!')
}
})
sock.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
// Simple echo bot with AI icon
await sock.sendMessage(
msg.key.remoteJid,
{
text: `You said: _"${msg.message.conversation || ''}"_`,
ai: true // shows the AI sparkle icon on the message
},
{ quoted: msg }
)
})
}
start()If you prefer linking via a pairing code instead of scanning a QR code:
import makeWASocket, { useMultiFileAuthState } from 'baileys'
import readline from 'readline'
const question = (text: string) =>
new Promise<string>((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question(text, (answer) => {
rl.close()
resolve(answer)
})
})
async function startWithPairingCode() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info')
const sock = makeWASocket({
auth: state,
printQRInTerminal: false,
})
sock.ev.on('creds.update', saveCreds)
if (!sock.authState.creds.registered) {
const phoneNumber = await question('Enter your phone number (with country code): ')
const code = await sock.requestPairingCode(phoneNumber)
console.log(`Pairing Code: ${code?.match(/.{1,4}/g)?.join('-')}`)
}
}
startWithPairingCode()Baileys Premod supports all standard Baileys configuration options, plus several new ones:
| Option | Type | Default | Description |
|---|---|---|---|
auth |
AuthenticationState |
required | Authentication state object (use useMultiFileAuthState) |
printQRInTerminal |
boolean |
false |
Print QR code in the terminal for scanning |
browser |
[string, string, string] |
Ubuntu Chrome | Browser description for connection |
waWebSocketUrl |
string |
wss://web.whatsapp.com/ws/chat |
WebSocket URL |
connectTimeoutMs |
number |
20000 |
Connection timeout in milliseconds |
keepAliveIntervalMs |
number |
30000 |
Keep-alive ping interval |
defaultQueryTimeoutMs |
number |
60000 |
Default timeout for IQ queries |
emitOwnEvents |
boolean |
true |
Emit events for own actions |
syncFullHistory |
boolean |
false |
Request full chat history from phone |
markOnlineOnConnect |
boolean |
true |
Set online status on connect |
generateHighQualityLinkPreview |
boolean |
false |
Generate high-quality link preview thumbnails |
logger |
ILogger |
pino logger | Custom logger instance |
options |
AxiosRequestConfig |
{} |
Axios options for HTTP requests |
patchMessageBeforeSending |
function |
identity | Hook to modify messages before sending |
getMessage |
function |
no-op | Fetch message from store for retry |
cachedGroupMetadata |
function |
no-op | Cache group metadata to speed up sending |
| Option | Type | Default | Description |
|---|---|---|---|
autoFetchVersion |
boolean |
true |
Automatically fetch the latest WhatsApp Web version on startup. Fetches from web.whatsapp.com/sw.js and falls back to the bundled version if it fails |
optimizeConnection |
boolean |
true |
Enable connection optimization features including improved retry logic and keep-alive tuning |
maxMediaUploadRetryCount |
number |
3 |
Maximum number of retry attempts for media upload operations. Increase this value on unstable networks |
debugMessageSend |
boolean |
false |
Enable verbose debug logging for message sending operations. Useful for troubleshooting message delivery issues |
const sock = makeWASocket({
auth: state,
printQRInTerminal: true,
// baileys-premod specific options:
autoFetchVersion: true,
optimizeConnection: true,
maxMediaUploadRetryCount: 5,
debugMessageSend: false,
// standard options:
logger: pino({ level: 'debug' }),
browser: ['My Bot', 'Chrome', '120.0.0'],
connectTimeoutMs: 30_000,
keepAliveIntervalMs: 25_000,
})One of the most common issues with WhatsApp Web libraries is version mismatches that lead to disconnections or banned sessions. Baileys Premod solves this by automatically fetching the latest WhatsApp Web version every time your bot starts up.
When autoFetchVersion is enabled (which it is by default), the library fetches the service worker file from web.whatsapp.com/sw.js, extracts the client_revision value, and constructs the proper version tuple [2, 3000, <revision>]. If the fetch fails for any reason (network error, blocked request, etc.), it silently falls back to the bundled version, so your bot will always start without crashing.
// This is enabled by default — no extra code needed!
const sock = makeWASocket({
auth: state,
// autoFetchVersion: true // default behavior
})You can also use the utility function directly if you need the version at a custom time:
import { fetchLatestWaWebVersion } from 'baileys-premod'
const { version, isLatest } = await fetchLatestWaWebVersion({})
console.log('Latest WA version:', version) // e.g. [2, 3000, 1036833994]This is a critical bug fix. In the original Baileys library, button messages, interactive messages, template messages, and list messages would not render correctly when sent to WhatsApp groups. The buttons and interactive elements would simply not appear, or the message would fail to deliver entirely.
The root cause was in the bizNode construction logic within messages-send.ts. The original code only checked the top-level contentType of the message, completely missing messages that were wrapped inside viewOnceMessage, viewOnceMessageV2, or viewOnceMessageV2Extension wrappers. Additionally, templateMessage was not handled at all.
Baileys Premod fixes this by performing a comprehensive deep check across all possible message wrappers and message types:
interactiveMessage(direct and wrapped)buttonsMessage(direct and wrapped)templateMessage(direct and wrapped)listMessage(now works in groups, not just private chats)interactiveResponseMessage(flow responses)
All these message types now work correctly in both group chats and private chats without any special handling required from your code.
// These all now work perfectly in groups:
await sock.sendMessage(groupId, {
text: "Pick an option:",
buttons: [
{ buttonId: 'opt1', buttonText: { displayText: 'Option 1' }, type: 1 },
{ buttonId: 'opt2', buttonText: { displayText: 'Option 2' }, type: 1 },
],
footer: 'Please select',
headerType: 1
})
await sock.sendMessage(groupId, {
text: "Choose a section:",
interactiveButtons: [
{
name: "single_select",
buttonParamsJson: JSON.stringify({
title: "Menu",
sections: [
{
title: "Category",
rows: [
{ title: "Item 1", id: "item1" },
{ title: "Item 2", id: "item2" }
]
}
]
})
}
]
})Based on nstar-y/bail — the AI sparkle badge implementation was originally developed in the Bail fork and adapted for Baileys Premod.
WhatsApp displays a small sparkle ✨ icon next to messages that are identified as AI-generated (similar to how Meta AI messages appear). Baileys Premod lets you add this badge to any message type with a single ai: true parameter.
The implementation works via a dual mechanism:
- Binary node layer: A
<bot biz_bot="1"/>XML node is appended to the message stanza, signaling WhatsApp to render the sparkle icon - Proto layer:
botMetadata(field 7) andbotMessageSecret(field 6) are set inMessage.messageContextInfo(field 35) — thebotMessageSecretis a random 32-byte cryptographic secret required by newer WhatsApp versions to properly authenticate bot messages
How it works internally: When
ai: trueis set, the library generates a randombotMessageSecret(32 bytes), setsbotMetadatawith a defaultpersonaId, and attaches a<bot biz_bot="1"/>binary node to the message stanza. This combination ensures compatibility across all WhatsApp versions — older clients use the binary node, while newer clients validate the proto-levelmessageContextInfo.
The AI icon works in both private chats and group chats, and it can be combined with any message type — text, image, video, buttons, interactive messages, etc.
// Simple text with AI icon
await sock.sendMessage(jid, { text: "Hello from AI!", ai: true })// Image with AI icon
await sock.sendMessage(jid, {
image: { url: "https://example.com/photo.jpg" },
caption: "Generated by AI",
ai: true
})
// Video with AI icon
await sock.sendMessage(jid, {
video: { url: "https://example.com/demo.mp4" },
caption: "AI-powered video",
ai: true
})// Button message with AI icon
await sock.sendMessage(jid, {
text: "Choose your language:",
buttons: [
{ buttonId: 'en', buttonText: { displayText: 'English' }, type: 1 },
{ buttonId: 'es', buttonText: { displayText: 'Spanish' }, type: 1 },
{ buttonId: 'fr', buttonText: { displayText: 'French' }, type: 1 },
],
footer: "AI Assistant",
headerType: 1,
ai: true
})// Interactive message with AI icon
await sock.sendMessage(jid, {
text: "What would you like to do?",
title: "AI Assistant",
footer: "Powered by baileys-premod",
interactiveButtons: [
{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "Generate Image",
id: "gen_img"
})
},
{
name: "cta_url",
buttonParamsJson: JSON.stringify({
display_text: "Visit Website",
url: "https://example.com"
})
}
],
ai: true
})For more control over how the AI badge appears, you can pass a full BotMetadata object instead of true:
import { proto } from 'baileys-premod'
await sock.sendMessage(jid, {
text: "Custom AI persona message",
ai: {
personaId: 'my-custom-bot',
// Add additional bot metadata fields as needed
} as proto.IBotMetadata
})The AI icon works identically in group chats. There is no special handling needed — just include ai: true in your message object:
const groupJid = "123456789@g.us"
await sock.sendMessage(groupJid, {
text: "Hello group! I'm an AI bot 🤖",
ai: true
})Note:
buttons(classic buttons) are a legacy/deprecated format by WhatsApp. For new bots, preferinteractiveButtonswhich support more button types and are actively maintained by WhatsApp.
Button messages display up to 3 action buttons below a text message. They are perfect for simple menus, confirmations, and quick user interactions.
const buttons = [
{ buttonId: 'id1', buttonText: { displayText: 'Button 1' }, type: 1 },
{ buttonId: 'id2', buttonText: { displayText: 'Button 2' }, type: 1 },
{ buttonId: 'id3', buttonText: { displayText: 'Button 3' }, type: 1 }
]
const buttonMessage = {
text: "Hi it's button message",
footer: 'Hello World',
buttons,
headerType: 1
}
await sock.sendMessage(jid, buttonMessage)const buttons = [
{ buttonId: 'buy', buttonText: { displayText: 'Buy Now' }, type: 1 },
{ buttonId: 'info', buttonText: { displayText: 'More Info' }, type: 1 }
]
const buttonMessage = {
image: { url: "https://example.com/product.jpg" },
caption: "Check out this product!",
footer: "Limited Offer",
buttons,
headerType: 4 // IMAGE header type
}
await sock.sendMessage(jid, buttonMessage)const buttons = [
{ buttonId: 'subscribe', buttonText: { displayText: 'Subscribe' }, type: 1 },
{ buttonId: 'skip', buttonText: { displayText: 'Skip' }, type: 1 }
]
const buttonMessage = {
video: { url: "https://example.com/preview.mp4" },
caption: "Watch the full video",
footer: "New Episode",
buttons,
headerType: 5 // VIDEO header type
}
await sock.sendMessage(jid, buttonMessage)When a user taps a button, you'll receive a buttonsResponseMessage in the incoming message event:
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
const response = msg.message?.buttonsResponseMessage
if (response) {
console.log('Button clicked:', response.selectedButtonId)
console.log('Display text:', response.selectedDisplayText)
}
})Interactive messages are the current and recommended WhatsApp message format for buttons. They support rich button types including quick replies, URL buttons, and copy-code buttons. They use the native flow protocol and are actively maintained by WhatsApp.
const interactiveButtons = [
{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "Quick Reply",
id: "ID"
})
},
{
name: "cta_url",
buttonParamsJson: JSON.stringify({
display_text: "Open Website",
url: "https://www.example.com/"
})
},
{
name: "cta_copy",
buttonParamsJson: JSON.stringify({
display_text: "Copy Promo Code",
id: "promo_2024",
copy_code: "SAVE20"
})
}
]
const interactiveMessage = {
text: "Hello World!",
title: "this is the title",
footer: "this is the footer",
interactiveButtons
}
await sock.sendMessage(jid, interactiveMessage)const interactiveButtons = [
{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "Approve",
id: "approve"
})
},
{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "Reject",
id: "reject"
})
}
]
const interactiveMessage = {
image: { url: "https://example.com/photo.jpg" },
caption: "Review this image",
title: "Image Review",
footer: "AI Moderation Bot",
interactiveButtons,
ai: true // combine with AI icon
}
await sock.sendMessage(jid, interactiveMessage)const interactiveButtons = [
{
name: "cta_url",
buttonParamsJson: JSON.stringify({
display_text: "Watch Full Video",
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
})
},
{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "Remind Me Later",
id: "remind"
})
}
]
const interactiveMessage = {
video: { url: "https://example.com/clip.mp4" },
caption: "New video is out!",
title: "Video Alert",
footer: "Subscribe for more",
interactiveButtons
}
await sock.sendMessage(jid, interactiveMessage)List messages (also called single-select or row messages) display a button that opens a menu with multiple sections and rows. They are ideal for displaying a large number of options in a compact format.
const interactiveButtons = [
{
name: "single_select",
buttonParamsJson: JSON.stringify({
title: "Choose a Category",
sections: [
{
title: "Technology",
highlight_label: "Popular",
rows: [
{
header: "📱",
title: "Smartphones",
description: "Latest phone reviews and news",
id: "tech_phones"
},
{
header: "💻",
title: "Laptops",
description: "Best laptops of the year",
id: "tech_laptops"
}
]
},
{
title: "Entertainment",
rows: [
{
header: "🎬",
title: "Movies",
description: "Movie recommendations",
id: "ent_movies"
},
{
header: "🎮",
title: "Gaming",
description: "Game reviews and news",
id: "ent_gaming"
}
]
}
]
})
}
]
const interactiveMessage = {
text: "What are you interested in?",
title: "Content Preferences",
footer: "Customize your feed",
interactiveButtons
}
await sock.sendMessage(jid, interactiveMessage)sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
const listResponse = msg.message?.listResponseMessage
if (listResponse) {
console.log('Selected row:', listResponse.title)
console.log('Row ID:', listResponse.singleSelectReply?.selectedRowId)
}
})When users interact with your bot's buttons, interactive menus, or list messages, WhatsApp sends back response messages. Baileys Premod now properly extracts text from all response types via the extractMessageContent utility, so your bot can detect and route button clicks without any manual parsing.
This section covers how to handle every type of button and menu response that WhatsApp can send back to your bot.
| Message Type | WhatsApp Field | When It's Sent | Key Data |
|---|---|---|---|
buttonsResponseMessage |
buttonsResponseMessage |
User taps a native button (up to 3) | selectedButtonId, selectedDisplayText |
interactiveResponseMessage |
interactiveResponseMessage |
User taps a native flow / quick reply button | nativeFlowResponseMessage.paramsJson, body.text |
listResponseMessage |
listResponseMessage |
User selects a row from a list menu | title, singleSelectReply.selectedRowId |
templateButtonReplyMessage |
templateButtonReplyMessage |
User taps a template button | selectedDisplayText, selectedId |
Every incoming message has a message object. You can use getContentType to determine which type of response you received:
import { getContentType } from 'baileys'
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const type = getContentType(msg.message)
console.log('Received message type:', type)
// type === 'interactiveResponseMessage' | 'buttonsResponseMessage' | 'listResponseMessage' | ...
})When a user taps a native flow button (the newer interactive message format), WhatsApp sends an interactiveResponseMessage. This is the most common response type when using interactiveButtons.
The response contains two important pieces of data: body.text which is the display text shown to the user (e.g. the button label or category title), and nativeFlowResponseMessage.paramsJson which contains the raw button data you originally set in buttonParamsJson.
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const response = msg.message.interactiveResponseMessage
if (response) {
// The display text the user saw (button label or category title)
console.log('Display text:', response.body?.text)
// Parse the button params to get the ID and any custom data
if (response.nativeFlowResponseMessage?.paramsJson) {
const params = JSON.parse(response.nativeFlowResponseMessage.paramsJson)
console.log('Button ID:', params.id)
console.log('Description:', params.description)
// Route based on the button ID
switch (params.id) {
case 'cat_owner':
// Show owner commands
break
case 'cat_downloader':
// Show downloader commands
break
case 'cat_group':
// Show group management commands
break
}
}
}
})Tip: The
paramsJsonfield contains the exact JSON object you passed inbuttonParamsJsonwhen creating the button. This means you can include custom fields likeid,description, or any other data to help identify which button was clicked and what action to take.
Classic buttons (sent with the buttons parameter) produce a buttonsResponseMessage:
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const response = msg.message.buttonsResponseMessage
if (response) {
console.log('Button ID:', response.selectedButtonId)
console.log('Display text:', response.selectedDisplayText)
// Route based on button ID
switch (response.selectedButtonId) {
case 'red':
await sock.sendMessage(msg.key.remoteJid, { text: 'You chose Red!' })
break
case 'green':
await sock.sendMessage(msg.key.remoteJid, { text: 'You chose Green!' })
break
}
}
})When a user selects a row from a list message (created with single_select), WhatsApp sends a listResponseMessage:
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const listResponse = msg.message.listResponseMessage
if (listResponse) {
console.log('Selected title:', listResponse.title)
console.log('Row ID:', listResponse.singleSelectReply?.selectedRowId)
console.log('Description:', listResponse.description)
// Route based on the row ID
switch (listResponse.singleSelectReply?.selectedRowId) {
case 'tech_phones':
await sock.sendMessage(msg.key.remoteJid, {
text: 'Latest smartphone reviews coming up!'
})
break
case 'ent_movies':
await sock.sendMessage(msg.key.remoteJid, {
text: 'Here are today\'s movie recommendations!'
})
break
}
}
})Template buttons (sent with templateButtons) produce a templateButtonReplyMessage:
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const tmplResponse = msg.message.templateButtonReplyMessage
if (tmplResponse) {
console.log('Selected ID:', tmplResponse.selectedId)
console.log('Display text:', tmplResponse.selectedDisplayText)
}
})Baileys Premod provides the extractMessageContent utility which automatically extracts the display text from any message type, including all button and menu responses. This is the simplest way to get the user-facing text from any interaction:
import { extractMessageContent } from 'baileys'
sock.ev.on('messages.upsert', ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const extracted = extractMessageContent(msg.message)
if (extracted?.conversation) {
console.log('User interacted with:', extracted.conversation)
// e.g. "👑 Owner" for an interactive response
// e.g. "Option 1" for a button response
// e.g. "Smartphones" for a list response
}
})The following table shows what extractMessageContent returns for each response type:
| Input Message Type | Extracted Result | Source Field |
|---|---|---|
interactiveResponseMessage |
{ conversation: body.text } |
response.body.text |
interactiveResponseMessage (fallback) |
{ conversation: params.id } |
nativeFlowResponseMessage.paramsJson.id |
nativeFlowResponseMessage (direct) |
{ conversation: params.id } |
nativeFlowResponseMessage.paramsJson.id |
carouselCardResponseMessage |
{ conversation: rowId } or { conversation: "card:<index>" } |
carouselCardResponseMessage.rowId / .cardIndex |
buttonsResponseMessage |
{ conversation: selectedDisplayText } |
response.selectedDisplayText |
listResponseMessage |
{ conversation: title } |
response.title |
templateButtonReplyMessage |
{ conversation: selectedDisplayText } |
response.selectedDisplayText |
interactiveMessage |
{ conversation: body.text } |
interactive.body.text |
buttonsMessage |
{ conversation: contentText } |
buttons.contentText |
Here is a full working example that demonstrates sending buttons, receiving responses, and routing actions based on what the user clicked:
import makeWASocket, { useMultiFileAuthState, getContentType } from 'baileys'
async function start() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_info')
const sock = makeWASocket({ auth: state, printQRInTerminal: true })
sock.ev.on('creds.update', saveCreds)
// Send a menu when the user types .menu
sock.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const from = msg.key.remoteJid
const type = getContentType(msg.message)
// User typed a text command
if (msg.message.conversation === '.menu') {
await sock.sendMessage(from, {
text: 'Choose a category:',
title: 'Bot Menu',
footer: 'Powered by baileys-premod',
interactiveButtons: [
{
name: 'quick_reply',
buttonParamsJson: JSON.stringify({
display_text: '👑 Owner',
id: 'cat_owner',
description: 'self, public, setprefix...'
})
},
{
name: 'quick_reply',
buttonParamsJson: JSON.stringify({
display_text: '📥 Downloader',
id: 'cat_downloader',
description: 'yt, tiktok, ig...'
})
},
{
name: 'cta_url',
buttonParamsJson: JSON.stringify({
display_text: '🌐 Website',
url: 'https://example.com'
})
}
]
})
return
}
// User clicked an interactive / native flow button
if (type === 'interactiveResponseMessage') {
const response = msg.message.interactiveResponseMessage
const bodyText = response?.body?.text
const params = response?.nativeFlowResponseMessage?.paramsJson
? JSON.parse(response.nativeFlowResponseMessage.paramsJson)
: null
console.log(`Button clicked: ${bodyText} (id: ${params?.id})`)
if (params?.id === 'cat_owner') {
await sock.sendMessage(from, {
text: '👑 *Owner Commands*\n\n.setpublic\n.self\n.setprefix'
}, { quoted: msg })
} else if (params?.id === 'cat_downloader') {
await sock.sendMessage(from, {
text: '📥 *Downloader Commands*\n\n.yt <url>\n.tiktok <url>\n.ig <url>'
}, { quoted: msg })
} else {
await sock.sendMessage(from, {
text: `You selected: ${bodyText}`
}, { quoted: msg })
}
return
}
// User clicked a classic button
if (type === 'buttonsResponseMessage') {
const response = msg.message.buttonsResponseMessage
console.log(`Button: ${response?.selectedButtonId}`)
await sock.sendMessage(from, {
text: `You clicked: ${response?.selectedDisplayText}`
}, { quoted: msg })
return
}
// User selected a list row
if (type === 'listResponseMessage') {
const response = msg.message.listResponseMessage
console.log(`List: ${response?.title} (${response?.singleSelectReply?.selectedRowId})`)
await sock.sendMessage(from, {
text: `You selected: ${response?.title}`
}, { quoted: msg })
return
}
})
}
start()| How You Send It | Response Type User Sends Back |
|---|---|
buttons: [...] |
buttonsResponseMessage |
interactiveButtons: [{ name: 'quick_reply', ... }] |
interactiveResponseMessage |
interactiveButtons: [{ name: 'single_select', ... }] |
listResponseMessage |
interactiveButtons: [{ name: 'cta_url', ... }] |
(no response — opens URL directly) |
interactiveButtons: [{ name: 'cta_copy', ... }] |
interactiveResponseMessage |
templateButtons: [...] |
templateButtonReplyMessage |
| Carousel card tap | carouselCardResponseMessage |
Album messages allow you to send multiple images and videos as a grouped carousel in a single message. The recipient can swipe through the media items, similar to WhatsApp's native album feature.
// Send a mixed album of images and videos
const media = [
{
image: { url: "https://example.com/photo1.jpg" }
},
{
image: { url: "https://example.com/photo2.jpg" }
},
{
image: await fs.readFile("./local_photo.jpg")
},
{
video: { url: "https://example.com/video.mp4" }
}
]
await sock.sendMessage(jid, {
album: media,
caption: "Check out these photos and videos!",
ai: true // optional: add AI icon
})Note: Album messages support both images and videos. The
captionparameter is optional and applies to the entire album. Each media item can be specified as a URL ({ url: "..." }), a local file path ({ url: "/path/to/file" }), or aBuffer.
Baileys Premod provides a full API for creating, managing, and sending messages to WhatsApp Channels (newsletters).
const metadata = await sock.newsletterCreate("My Channel Name")
console.log(metadata)// By invite code
const metadata = await sock.newsletterMetadata("invite", "xxxxx")
// By JID
const metadata = await sock.newsletterMetadata("jid", "abcd@newsletter")
console.log(metadata)// Text message
await sock.sendMessage("abcd@newsletter", {
text: "Hello channel subscribers!"
})
// Image message
await sock.sendMessage("abcd@newsletter", {
image: { url: "https://example.com/banner.jpg" },
caption: "New announcement"
})Baileys Premod fully supports sending both voice notes (PTT) and regular audio (music/files) to WhatsApp Channels. Audio messages in channels are uploaded unencrypted to the WhatsApp newsletter media endpoint and relayed via the plaintext protocol.
You can use the standard sendMessage with @newsletter JIDs — audio is automatically routed through the correct newsletter media upload path:
// Voice note (PTT) — displays as a playable voice message in the channel
await sock.sendMessage("abcd@newsletter", {
audio: { url: "./voice_note.ogg" },
ptt: true, // push-to-talk = voice note
mimetype: "audio/ogg",
})
// Music/audio file — displays as a downloadable audio in the channel
await sock.sendMessage("abcd@newsletter", {
audio: { url: "./podcast_episode.mp3" },
ptt: false, // regular audio (not a voice note)
mimetype: "audio/mpeg",
caption: "Episode 42: Building WhatsApp Bots",
})
// Voice note from URL
await sock.sendMessage("abcd@newsletter", {
audio: { url: "https://example.com/announcement.mp3" },
ptt: true,
})
// Voice note from Buffer
import fs from 'fs'
const audioBuffer = fs.readFileSync("./recording.ogg")
await sock.sendMessage("abcd@newsletter", {
audio: audioBuffer,
ptt: true,
mimetype: "audio/ogg",
})For a simpler and more ergonomic API, use the dedicated newsletterSendAudio function which validates the JID and applies sensible defaults:
// Voice note to channel (PTT is true by default)
await sock.newsletterSendAudio("abcd@newsletter", {
url: "./voice_note.ogg"
})
// Music file with caption (set ptt to false)
await sock.newsletterSendAudio("abcd@newsletter", {
url: "./song.mp3"
}, {
ptt: false,
caption: "Now playing: Chill Vibes",
mimetype: "audio/mpeg",
})
// Voice note with custom waveform color
await sock.newsletterSendAudio("abcd@newsletter", {
url: "./announcement.ogg"
}, {
ptt: true,
backgroundColor: "#25D366", // green waveform
mimetype: "audio/ogg",
})
// Pre-computed duration and waveform (skip auto-detection)
await sock.newsletterSendAudio("abcd@newsletter", {
url: "./clip.mp3"
}, {
ptt: true,
seconds: 45.2,
waveform: Buffer.from(/* 64-byte waveform data */),
})| Option | Type | Default | Description |
|---|---|---|---|
ptt |
boolean |
true |
true = voice note (playable inline), false = regular audio file (downloadable) |
mimetype |
string |
auto-detected | Audio MIME type. Common: audio/ogg, audio/mpeg, audio/mp4, audio/aac |
caption |
string |
none | Text caption (only shown for non-PTT audio, ignored for voice notes) |
backgroundColor |
string |
none | Hex color for the PTT waveform visualization (e.g. "#FF0000") |
seconds |
number |
auto-computed | Audio duration in seconds. Auto-detected via music-metadata if omitted |
waveform |
Buffer |
auto-computed | 64-byte waveform data for PTT visualization. Auto-generated via audio-decode if omitted |
WhatsApp Channels support the following audio formats for both voice notes and regular audio files:
- OGG (Opus) — Recommended for voice notes. Best quality-to-size ratio and native WhatsApp format.
- MP3 (MPEG) — Widely supported, works well for music and podcast distribution.
- AAC (M4A) — Good quality, commonly used for iOS recordings.
- MP4 Audio — Container format, works for both voice notes and music.
- Audio duration and waveform are automatically computed from the audio file when not explicitly provided. This requires the optional
music-metadataandaudio-decodepackages to be installed. - Voice notes (
ptt: true) display a playable inline player in the channel with a waveform visualization. Regular audio (ptt: false) displays as a downloadable file with a caption. - WhatsApp Channels do not support quoting/replying to messages. The
quotedoption is silently ignored for@newsletterJIDs. - Disappearing/ephemeral messages are not supported in channels and will be silently ignored.
- Audio files are uploaded unencrypted to WhatsApp's newsletter media endpoint (
/newsletter/newsletter-audio), unlike regular chat messages which use AES-256-CBC encryption.
// Update name
await sock.newsletterUpdateName("abcd@newsletter", "New Channel Name")
// Update description
await sock.newsletterUpdateDescription("abcd@newsletter", "Updated description text")
// Update profile picture (pass a Buffer)
await sock.newsletterUpdatePicture("abcd@newsletter", imageBuffer)
// Remove profile picture
await sock.newsletterRemovePicture("abcd@newsletter")await sock.newsletterFollow("abcd@newsletter")
await sock.newsletterUnfollow("abcd@newsletter")await sock.newsletterMute("abcd@newsletter")
await sock.newsletterUnmute("abcd@newsletter")// To find the message ID, copy the message URL from the channel
// Example: https://whatsapp.com/channel/xxxxx/175
// The last number (175) is the message ID
const messageId = "175"
await sock.newsletterReactMessage("abcd@newsletter", messageId, "🥳")await sock.newsletterDelete("abcd@newsletter")WhatsApp's default pairing codes are randomly generated 8-character alphanumeric strings. Baileys Premod allows you to specify a custom pairing code of your choice, making it easier for users to link their device (especially useful for bots and automated systems).
if (usePairingCode && !sock.authState.creds.registered) {
const phoneNumber = await question('Enter your phone number:\n')
// Set your own custom 8-character pairing code
const customCode = "MYBOT2024"
const code = await sock.requestPairingCode(phoneNumber, customCode)
console.log(`Pairing Code: ${code?.match(/.{1,4}/g)?.join('-') || code}`)
// Output: Pairing Code: MYBO-T202-4
}Important: The pairing code must be exactly 8 characters and alphanumeric (A-Z, 0-9). It will be automatically uppercased. The
requestPairingCodefunction accepts the phone number with or without the country code prefix.
When optimizeConnection is enabled (default: true), Baileys Premod applies several connection reliability improvements:
- Improved retry logic for failed WebSocket connections
- Tuned keep-alive intervals to reduce unnecessary disconnections
- Enhanced media upload retries controlled by
maxMediaUploadRetryCount
const sock = makeWASocket({
auth: state,
optimizeConnection: true, // enabled by default
maxMediaUploadRetryCount: 5, // default: 3
})Here's a more complete example that demonstrates authentication, message handling, multiple message types, and the AI icon feature:
import { Boom } from '@hapi/boom'
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
fetchLatestWaWebVersion
} from 'baileys-premod'
import pino from 'pino'
const logger = pino({ level: 'info' })
async function start() {
const { state, saveCreds } = await useMultiFileAuthState('./auth_session')
const sock = makeWASocket({
auth: state,
printQRInTerminal: true,
logger: logger.child({ class: 'bot' }),
// baileys-premod options
autoFetchVersion: true,
optimizeConnection: true,
maxMediaUploadRetryCount: 3,
})
// Save authentication credentials
sock.ev.on('creds.update', saveCreds)
// Handle connection events
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect } = update
if (connection === 'close') {
const statusCode = (lastDisconnect?.error as Boom)?.output?.statusCode
console.log('Connection closed, status:', statusCode)
if (statusCode !== DisconnectReason.loggedOut) {
console.log('Reconnecting...')
start()
}
}
if (connection === 'open') {
console.log('Bot is online and ready!')
// Fetch current version info
const { version } = await fetchLatestWaWebVersion({})
console.log('Using WhatsApp version:', version)
}
})
// Handle incoming messages
sock.ev.on('messages.upsert', async ({ messages, type }) => {
const msg = messages[0]
if (!msg.message || msg.key.fromMe) return
const from = msg.key.remoteJid
const isGroup = from?.endsWith('@g.us')
// Extract text from various message types
const text =
msg.message?.conversation ||
msg.message?.extendedTextMessage?.text ||
''
if (!text) return
console.log(`Message from ${from}: ${text}`)
// Route commands
if (text === '/menu') {
// Send interactive menu
await sock.sendMessage(from, {
text: "Welcome to the bot menu!",
title: "Bot Menu",
footer: "Choose an option below",
interactiveButtons: [
{
name: "quick_reply",
buttonParamsJson: JSON.stringify({
display_text: "Get Info",
id: "info"
})
},
{
name: "cta_url",
buttonParamsJson: JSON.stringify({
display_text: "Visit GitHub",
url: "https://github.com/BF667-IDLE"
})
},
{
name: "cta_copy",
buttonParamsJson: JSON.stringify({
display_text: "Copy My JID",
id: "copy_jid",
copy_code: from || "unknown"
})
}
],
ai: true // AI sparkle icon
}, { quoted: msg })
} else if (text === '/buttons') {
// Send button message
await sock.sendMessage(from, {
text: "Pick a color:",
footer: "Color Picker",
buttons: [
{ buttonId: 'red', buttonText: { displayText: '🔴 Red' }, type: 1 },
{ buttonId: 'green', buttonText: { displayText: '🟢 Green' }, type: 1 },
{ buttonId: 'blue', buttonText: { displayText: '🔵 Blue' }, type: 1 },
],
headerType: 1,
}, { quoted: msg })
} else {
// Echo with AI icon
await sock.sendMessage(from, {
text: `You said: _${text}_\n\n${isGroup ? '(sent in group)' : '(sent in private)'}`,
ai: true,
}, { quoted: msg })
}
})
}
start()Creates a new WhatsApp Web socket connection. Returns a socket object with all standard WhatsApp methods plus newsletter management.
| Method | Description |
|---|---|
sendMessage(jid, content, options?) |
Send a message to a chat. Supports text, media, buttons, interactive messages, albums, and more |
sendMessage(jid, content, options?) with ai: true |
Send a message with the AI sparkle icon |
requestPairingCode(phone, customCode?) |
Request a pairing code. Optionally pass a custom 8-char alphanumeric code |
groupMetadata(jid) |
Fetch group metadata |
groupCreate(name, participants) |
Create a new group |
groupLeave(jid) |
Leave a group |
newsletterCreate(name) |
Create a newsletter/channel |
newsletterMetadata(type, id) |
Get newsletter info |
newsletterUpdateName(jid, name) |
Update channel name |
newsletterUpdateDescription(jid, desc) |
Update channel description |
newsletterUpdatePicture(jid, buffer) |
Update channel profile picture |
newsletterRemovePicture(jid) |
Remove channel profile picture |
newsletterFollow(jid) |
Follow a channel |
newsletterUnfollow(jid) |
Unfollow a channel |
newsletterMute(jid) |
Mute channel notifications |
newsletterUnmute(jid) |
Unmute channel notifications |
newsletterReactMessage(jid, msgId, emoji) |
React to a channel message |
newsletterSendAudio(jid, audio, options?) |
Send voice note or audio file to a channel |
newsletterDelete(jid) |
Delete a newsletter |
readMessages(keys) |
Mark messages as read |
chatModify(mod, jid) |
Modify chat settings (mute, archive, etc.) |
profilePictureUrl(jid, type) |
Get profile picture URL |
fetchProfilePictureFromServer(jid) |
Fetch profile picture from server |
updateProfilePicture(jid, content) |
Update your profile picture |
updateProfileName(name) |
Update your display name |
onWhatsApp(jid) |
Check if a number is on WhatsApp |
presenceSubscribe(jid) |
Subscribe to presence updates |
sendPresenceUpdate(type) |
Update your presence (available, composing, etc.) |
| Event | Description |
|---|---|
connection.update |
Connection state changes (connecting, open, close, QR) |
creds.update |
Authentication credentials updated (must save these!) |
messages.upsert |
New messages received |
messages.update |
Message status updates (delivered, read, deleted) |
messages.delete |
Messages deleted |
messages.reaction |
Message reactions |
group-participants.update |
Group member changes (join, leave, promote, demote) |
groups.update |
Group metadata updates (name, description, etc.) |
presence.update |
Contact presence/typing updates |
Problem: Connection was lost or frequent disconnections
Solution: Enable auto version fetch and connection optimization:
const sock = makeWASocket({
auth: state,
autoFetchVersion: true,
optimizeConnection: true,
})Problem: QR code never appears or auth state is corrupted
Solution: Delete the auth_info (or your configured auth directory) and restart. The auth state files may have become corrupted:
rm -rf ./auth_infoProblem: Buttons or interactive messages appear as plain text in groups
Solution: Make sure you're using baileys-premod v7.0.0 or later. This issue was the core fix of issue #58 and is resolved in this version.
Problem: Your bot does not detect when a user taps a button or selects a menu item. The debug log shows type=interactiveResponseMessage but responseId=undefined or the message is marked as Unhandled.
Solution: This was caused by extractMessageContent not handling response message types. Update to the latest version which includes the fix for interactiveResponseMessage, buttonsResponseMessage, listResponseMessage, and templateButtonReplyMessage. Make sure you are detecting responses by checking getContentType(msg.message):
import { getContentType } from 'baileys'
const type = getContentType(msg.message)
if (type === 'interactiveResponseMessage') {
// Handle native flow button click
const params = JSON.parse(
msg.message.interactiveResponseMessage
.nativeFlowResponseMessage.paramsJson
)
console.log('Button ID:', params.id)
}Problem: Media upload failed or timeout errors
Solution: Increase the retry count and timeout:
const sock = makeWASocket({
auth: state,
maxMediaUploadRetryCount: 5,
defaultQueryTimeoutMs: 120_000,
})Problem: Excessive libsignal-related log output cluttering your console
Solution: This library includes cleaned-up libsignal logs. If you still see excessive output, adjust your logger level:
import pino from 'pino'
const logger = pino({ level: 'info' }) // or 'warn' to reduce output further
![]() Baileys Premod |
![]() Bail |
![]() Baileys |
![]() Baileys |
This project is licensed under the MIT License — see the LICENSE file for details.
This library is not affiliated with, authorized, maintained, sponsored, or endorsed by WhatsApp or any of its affiliates or subsidiaries. This is an independent and unofficial software library. Use at your own risk. The developers assume no liability and are not responsible for any misuse or damage caused by this program.
Made with ❤️ by BF667-IDLE
⭐ Star this repo if you find it useful!



