A powerful, production-ready interactive button plugin for the Baileys WhatsApp library.
Send interactive buttons, quick replies, URL buttons, copy buttons, call buttons, and single-select menus β with optional image headers β without touching Baileys core files.
By default, WhiskeySockets/Baileys cannot send interactive buttons. The root cause is that Baileys lacks the required binary node wrappers (biz, interactive, native_flow) that WhatsApp expects for interactive messages.
malvin-btns fills the gap by:
- Detecting button messages using WhatsApp's expected format
- Converting simple button definitions to the correct protobuf structure
- Injecting missing binary nodes (
biz,interactive,native_flow,bot) viaadditionalNodes - Automatically handling private vs. group chat requirements
- Processing images - automatically downloads, encrypts, and uploads images to WhatsApp's CDN
No core file edits. No monkey-patching. Just drop it in and go.
| Feature | Status |
|---|---|
| No modifications to Baileys core | β |
| Automatic binary node injection | β |
Private chat support (bot node with biz_bot: '1') |
β |
Group chat support (biz node only) |
β |
| Quick reply buttons | β |
| URL, Copy, Call CTA buttons | β |
| Single-select menus | β |
| Send location button | β |
| Image header support (auto-upload & encryption) | β |
| Input validation with detailed errors | β |
| Works with multiple Baileys forks | β |
| Regular messages pass through unchanged | β |
npm install malvin-btnsYou also need a Baileys package:
# pick one
npm install mrxd-baileys
npm install @whiskeysockets/baileysconst { makeWASocket } = require('mrxd-baileys');
const { sendButtons } = require('malvin-btns');
const sock = makeWASocket({ /* your config */ });
await sendButtons(sock, jid, {
title: 'Hello there!',
text: 'Pick an option below',
footer: 'Powered by malvin-btns',
buttons: [
{ id: 'help', text: 'π Help' },
{ id: 'about', text: 'βΉοΈ About' },
{
name: 'cta_url',
buttonParamsJson: JSON.stringify({
display_text: 'π Visit Website',
url: 'https://host.malvintech.sbs'
})
}
]
});Add an image above your buttons by passing an image object. The image is automatically downloaded, encrypted, and uploaded to WhatsApp's CDN β you just provide the URL.
await sendButtons(sock, jid, {
title: 'Product Showcase',
text: 'Check out our latest collection',
image: { url: 'https://example.com/product.jpg' },
footer: 'Limited time offer',
buttons: [
{ id: 'shop', text: 'π Shop Now' },
{ id: 'details', text: 'π Details' }
]
});You can also use a local file path or buffer:
// Local file
image: { path: '/path/to/image.jpg' }
// Buffer
image: { buffer: bufferData }When a user taps a button, WhatsApp sends back the button's id as a regular message:
sock.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0];
const text =
msg.message?.conversation ||
msg.message?.extendedTextMessage?.text || '';
if (text === 'help') {
await sock.sendMessage(msg.key.remoteJid, { text: 'How can I help you?' });
}
if (text === 'about') {
await sock.sendMessage(msg.key.remoteJid, { text: 'Made with malvin-btns π' });
}
});| Name | Purpose | Required buttonParamsJson keys |
|---|---|---|
quick_reply |
Simple reply that sends its id back | { display_text, id } |
single_select |
In-button picker list | { title, sections: [{ title?, rows: [{ id, title, description?, header? }] }] } |
cta_url |
Open a URL | { display_text, url, merchant_url? } |
cta_copy |
Copy text to clipboard | { display_text, copy_code } |
cta_call |
Tap to dial | { display_text, phone_number } |
cta_catalog |
Open business catalog | { display_text? } |
send_location |
Request user location | { display_text? } |
For full control over all button types in one message, use sendInteractiveMessage:
const { sendInteractiveMessage } = require('malvin-btns');
await sendInteractiveMessage(sock, jid, {
text: 'Advanced button demo',
footer: 'malvin-btns',
image: { url: 'https://example.com/header.jpg' },
interactiveButtons: [
{
name: 'quick_reply',
buttonParamsJson: JSON.stringify({ display_text: 'Reply A', id: 'reply_a' })
},
{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: 'Pick One',
sections: [{
title: 'Options',
rows: [
{ id: 'opt_hello', title: 'Hello', description: 'Say hi' },
{ id: 'opt_bye', title: 'Bye', description: 'Say bye' }
]
}]
})
}
]
});await sendInteractiveMessage(sock, jid, {
text: 'Contact actions',
interactiveButtons: [
{
name: 'cta_url',
buttonParamsJson: JSON.stringify({ display_text: 'π Docs', url: 'https://host.malvintech.sbs/doc' })
},
{
name: 'cta_copy',
buttonParamsJson: JSON.stringify({ display_text: 'π Copy Code', copy_code: 'MALVIN-2025' })
},
{
name: 'cta_call',
buttonParamsJson: JSON.stringify({ display_text: 'π Call Support', phone_number: '+1234567890' })
}
]
});await sendInteractiveMessage(sock, jid, {
text: 'Choose a plan',
interactiveButtons: [
{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: 'Plans',
sections: [{
title: 'Available Plans',
rows: [
{ id: 'plan_free', title: 'Free', description: '4,000 requests/day' },
{ id: 'plan_pro', title: 'Premium', description: 'Unlimited β one-time payment' }
]
}]
})
}
]
});const { sendButtons, InteractiveValidationError } = require('malvin-btns');
try {
await sendButtons(sock, jid, { text: 'Hi', buttons: [] });
} catch (err) {
if (err instanceof InteractiveValidationError) {
console.error(err.formatDetailed());
}
}What You Write
{
text: 'Hello',
footer: 'Footer',
image: { url: 'https://example.com/img.jpg' },
interactiveButtons: [{ name, buttonParamsJson }, ...]
}What Gets Sent to WhatsApp
{
interactiveMessage: {
header: {
hasMediaAttachment: true,
imageMessage: { url: 'https://mmg.whatsapp.net/...', ... }
},
nativeFlowMessage: { buttons: [...] },
body: { text: 'Hello' },
footer: { text: 'Footer' }
}
}Binary Nodes Injected
| Chat Type | Nodes Added |
|---|---|
| Private | biz + interactive/native_flow + bot (biz_bot: '1') |
| Group | biz + interactive/native_flow only |
Simplified sending for quick replies and basic CTAs.
| Parameter | Type | Description |
|---|---|---|
sock |
WASocket | Baileys socket instance |
jid |
string | Recipient JID |
data.text |
string | Message body (required) |
data.title |
string | Header title (optional) |
data.footer |
string | Footer text (optional) |
data.image |
object | Image header - { url, path, or buffer } (optional) |
data.buttons |
array | Button objects |
Full control over all button types and structures.
| Parameter | Type | Description |
|---|---|---|
sock |
WASocket | Baileys socket instance |
jid |
string | Recipient JID |
content.text |
string | Message body (required) |
content.footer |
string | Footer text (optional) |
content.image |
object | Image header - { url, path, or buffer } (optional) |
content.interactiveButtons |
array | Full button definitions |
| Baileys Package | Compatible |
|---|---|
| mrxd-baileys | β Yes |
| @whiskeysockets/baileys | β Yes (7.0.0-rc.2+) |
| baileys | β Yes |
| @adiwajshing/baileys | β Yes |
- Node.js v20 or higher
- Baileys 7.0.0-rc.2+
mrxdking β @XdKing2
- πΊ YouTube / TikTok / Instagram: @malvintech
- π¬ Telegram: @malvintech
ISC Β© Malvin Tech