hiura-baileys
v1.5.12
Published
WhatsApp Web API — Hiura Baileys by Nimzz. Full LID+JID, all buttons, interactive messages, rich messages, album, latex, and more.
Maintainers
Readme
✨ Features
- 🔐 Full LID (Linked ID) support alongside standard JID
- 🔘 Every WhatsApp button & interactive message type — native flow, templates, carousels, product cards, and more
- 📊 Rich messages: tables, code blocks, LaTeX, link previews
- 📞 Built-in VoIP module for programmatic voice calls
- 🔓 Automatic decrypt handlers for polls, events, comments, and reactions
- 🌟 es module only
Getting Started
Sending Messages
- Text Messages
- Media Messages
- Album (Multi-Media)
- Sticker Pack
- Reaction, Delete, Pin, Keep
- Poll & Event
- Payment & Order
- Scheduled Call
Voice & Calls
Interactive & Rich Content
- List Reply & Button Reply
- Sections (List Message)
- Product List
- Buttons (Plain)
- Template Buttons
- Interactive Buttons (Native Flow)
- Shop, Collection, Cards
- Carousel (Raw)
- Interactive Messages (Hiura Engine)
- Rich Messages (Hiura Engine)
Groups & Newsletters
- Group Invite & Newsletter Admin Invite
- Share Phone & Limit Sharing
- Group Story / Group Status
- Status / WA Story
- Ephemeral / Disappearing Messages
- Group Management
- Newsletter (Channels)
Utilities & Internals
- JID Utilities
- Decrypt Handler
- In-Memory Store
- Auth State
- Handling Incoming Messages
- Typography & contextInfo
- DisconnectReason
- Browsers
Reference
Credits
| Project | Contribution | |---------|-----------| | @blckrose/baileys | Main base of this library | | @whiskeysockets/baileys | Original Baileys core |
Requirements
- Node.js >= 20.0.0
- npm or yarn
npm install hiura-baileysWhy does
require()now throw in CJS? As of v1.5.3, the CJS entry point (index.cjs) is a static, version-independent shim: callingrequire('hiura-baileys')throws immediately with a message pointing you toimport(ESM) orawait import('hiura-baileys')from CJS. Earlier versions (v1.5.1–v1.5.2) tried to bridge synchronously into the ESM build using Node's nativerequire(esm), which only worked on Node >=20.19.0 or=22.12.0 — that patch-version gate is gone now, which is also why the Node.js requirement dropped back down to
>=20.0.0. See the FAQ for details.
For media features (thumbnails, image processing):
npm install sharpFor logging:
npm install pinoHow to Import
ESM
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
Browsers,
makeCacheableSignalKeyStore,
makeInMemoryStore,
fetchLatestBaileysVersion,
proto
} from 'hiura-baileys';CJS (dynamic import)
// require('hiura-baileys') throws by design as of v1.5.3 — see FAQ.
// Use a dynamic import instead, inside an async function:
const {
makeWASocket,
useMultiFileAuthState,
DisconnectReason,
Browsers,
makeCacheableSignalKeyStore,
makeInMemoryStore,
fetchLatestBaileysVersion,
proto
} = await import('hiura-baileys');All 317 exports are available directly from the ESM build, and from the
CJS side via await import('hiura-baileys') — no await ready, no
loading a function first just to "unlock" the rest. Direct synchronous
require() is intentionally unsupported since v1.5.3 (see
Changelog).
Quick Start
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
makeCacheableSignalKeyStore,
makeInMemoryStore,
fetchLatestBaileysVersion
} from 'hiura-baileys';
import { Boom } from '@hapi/boom';
import pino from 'pino';
const logger = pino({ level: 'silent' });
const store = makeInMemoryStore({});
store.readFromFile('./data/store.json');
setInterval(() => store.writeToFile('./data/store.json'), 10_000);
async function start() {
const { state, saveCreds } = await useMultiFileAuthState('./sessions');
const { version } = await fetchLatestBaileysVersion();
const sock = makeWASocket({
version,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger)
},
printQRInTerminal: true,
logger,
syncFullHistory: false,
getMessage: async (key) => store.loadMessage(key.remoteJid, key.id)?.message,
cachedGroupMetadata: async (jid) => store.groupMetadata[jid],
});
store.bind(sock.ev);
sock.ev.on('creds.update', saveCreds);
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'close') {
const code = new Boom(lastDisconnect?.error)?.output?.statusCode;
if (code !== DisconnectReason.loggedOut) start();
else console.log('Logged out. Delete the sessions folder and restart.');
}
if (connection === 'open') console.log('Connected!');
});
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return;
const m = messages[0];
if (!m.message) return;
const jid = m.key.remoteJid;
const text = m.message?.conversation
|| m.message?.extendedTextMessage?.text
|| '';
if (text === '.ping') await sock.sendMessage(jid, { text: 'pong' }, { quoted: m });
});
}
start();Pairing Code (no QR)
const sock = makeWASocket({ printQRInTerminal: false, ...config });
sock.ev.on('connection.update', async ({ connection }) => {
if (connection === 'connecting' && !sock.authState.creds.registered) {
const code = await sock.requestPairingCode('6281234567890');
console.log('Code:', code?.match(/.{1,4}/g)?.join('-'));
}
});Text Messages
// plain text
await sock.sendMessage(jid, { text: 'hello' });
// with mention
await sock.sendMessage(jid, {
text: '@628111 hello',
mentions: ['[email protected]']
});
// with quoted reply
await sock.sendMessage(jid, { text: 'reply' }, { quoted: m });
// edit message
await sock.sendMessage(jid, { text: 'new text', edit: m.key });
// forward
await sock.sendMessage(jid, { forward: m });Media Messages
// image
await sock.sendMessage(jid, { image: { url: 'https://...' }, caption: 'caption' });
await sock.sendMessage(jid, { image: readFileSync('./img.jpg'), caption: 'caption' });
// video
await sock.sendMessage(jid, { video: { url: 'https://...' }, caption: 'caption' });
// regular audio
await sock.sendMessage(jid, {
audio: { url: 'https://...' },
mimetype: 'audio/mp4'
});
// voice note (PTT)
await sock.sendMessage(jid, {
audio: { url: 'https://...' },
mimetype: 'audio/ogg; codecs=opus',
ptt: true
});
// video note (PTV / circular)
await sock.sendMessage(jid, {
video: { url: 'https://...' },
ptv: true
});
// document
await sock.sendMessage(jid, {
document: { url: 'https://...' },
mimetype: 'application/pdf',
fileName: 'file.pdf'
});
// sticker
await sock.sendMessage(jid, { sticker: readFileSync('./sticker.webp') });
// contact
await sock.sendMessage(jid, {
contacts: {
displayName: 'Name',
contacts: [{ vcard: 'BEGIN:VCARD\nVERSION:3.0\nFN:Name\nTEL:+62811\nEND:VCARD' }]
}
});
// location
await sock.sendMessage(jid, {
location: {
degreesLatitude: -6.200000,
degreesLongitude: 106.816666,
name: 'Jakarta',
address: 'Jakarta, Indonesia'
}
});Album (Multi-Media)
Send several images/videos at once in a single album:
await sock.sendMessage(jid, {
album: [
{ image: { url: 'https://...' }, caption: 'photo 1' },
{ image: readFileSync('./img.jpg'), caption: 'photo 2' },
{ video: { url: 'https://...' }, caption: 'video 1' },
]
}, { quoted: m });Images and videos can be freely mixed. Each item can have its own caption.
Sticker Pack
Send a sticker pack to a chat:
// Requires: npm install fflate sharp
await sock.sendMessage(jid, {
stickerPack: {
name: 'Pack Name',
publisher: 'Publisher',
packId: 'unique-pack-id', // optional, auto-generated if omitted
description: 'Pack description', // optional
cover: readFileSync('./cover.webp'), // optional
stickers: [
{ data: readFileSync('./sticker1.webp') },
{ data: readFileSync('./sticker2.jpg') }, // auto-converted to webp
{ data: readFileSync('./animated.webp') }, // animated is supported too
]
}
});Limit: 1–120 stickers per pack. Input formats: webp, jpg, png (auto-converted). Animated webp is supported.
Reaction, Delete, Pin, Keep
// react
await sock.sendMessage(jid, {
react: { text: '👍', key: m.key }
});
// remove reaction
await sock.sendMessage(jid, {
react: { text: '', key: m.key }
});
// delete message (requires admin in groups)
await sock.sendMessage(jid, { delete: m.key });
// pin message (type: 1=86400s, 2=604800s, 3=2592000s, 5=unpin)
await sock.sendMessage(jid, {
pin: { key: m.key, type: 1, time: 86400 }
});
// keep message (save to starred)
await sock.sendMessage(jid, {
keep: { key: m.key, type: 1 }
});Poll & Event
// single-choice poll
await sock.sendMessage(jid, {
poll: {
name: 'Pick one:',
values: ['Option A', 'Option B', 'Option C'],
selectableCount: 1
}
});
// multi-select poll
await sock.sendMessage(jid, {
poll: {
name: 'Pick several:',
values: ['A', 'B', 'C'],
selectableCount: 0 // 0 = all selectable
}
});
// poll for an announcement community group
await sock.sendMessage(jid, {
poll: {
name: 'Vote:',
values: ['Yes', 'No'],
selectableCount: 1,
toAnnouncementGroup: true
}
});
// poll result (snapshot)
await sock.sendMessage(jid, {
pollResult: {
name: 'Poll Name',
values: [
['Option A', 42],
['Option B', 17],
]
}
});
// event
await sock.sendMessage(jid, {
event: {
name: 'Event Name',
description: 'Event description',
startDate: new Date('2026-07-01T09:00:00+07:00'),
endDate: new Date('2026-07-01T17:00:00+07:00'),
location: {
degreesLatitude: -6.2,
degreesLongitude: 106.8,
name: 'Jakarta Convention Center'
},
isCancelled: false,
extraGuestsAllowed: true,
}
});Payment & Order
// request payment
await sock.sendMessage(jid, {
payment: {
currency: 'IDR',
amount: 50000,
offset: 0,
expiry: Math.floor(Date.now() / 1000) + 86400,
from: '[email protected]',
note: 'Please pay',
}
});
// payment invite
await sock.sendMessage(jid, {
paymentInvite: {
type: 2,
expiry: Math.floor(Date.now() / 1000) + 86400
}
});Scheduled Call
await sock.sendMessage(jid, {
call: {
name: 'Weekly Meeting',
type: 1, // 1 = voice, 2 = video
time: Date.now() + 3600000
}
});VoIP (Voice Calls)
hiura-baileys ships a complete VoIP module (lib/Voip/) that lets your
bot make and handle WhatsApp voice calls programmatically.
⚠️ Additional requirement: VoIP requires
@roamhq/wrtc(a native WebRTC addon that needs a C++ compiler) and runs on desktop/server Node.js environments with access to native WebRTC libraries. It will not work on shared hosting without a build toolchain (e.g. Pterodactyl without a compiler).
npm install @roamhq/wrtcSetting up VoipClient
import { VoipClient } from 'hiura-baileys/lib/Voip/index.js';
// or CJS:
const { VoipClient } = require('hiura-baileys/lib/Voip/index.js');
const voip = new VoipClient(sock, creds);
// Required: connect before you can make or receive calls
await voip.connect();Making a Call
// Voice call to a WhatsApp number
const call = await voip.call('[email protected]', {
audio: true, // enable audio (default: true)
video: false, // video call (default: false)
});
// Event when the call is answered
call.on('accepted', () => {
console.log('Call accepted!');
});
// Event when the call ends
call.on('ended', (reason) => {
console.log('Call ended:', reason);
});
// Error event
call.on('error', (err) => {
console.error('VoIP error:', err);
});
// End the call manually
call.end();
// Mute/unmute microphone
call.mute(true); // mute
call.mute(false); // unmuteStreaming Audio Into a Call
import { readFileSync } from 'fs';
const call = await voip.call('[email protected]');
call.on('accepted', () => {
// Stream an audio file (PCM/WAV) into the call
const audioBuffer = readFileSync('./audio.wav');
call.sendAudio(audioBuffer);
});Handling Incoming Calls
// Handle via Baileys' built-in 'call' event
sock.ev.on('call', async ([callEvent]) => {
if (callEvent.status === 'offer') {
// Reject the incoming call
await sock.rejectCall(callEvent.id, callEvent.from);
// Or: answer via VoipClient
// const activeCall = await voip.accept(callEvent);
}
});Disconnecting VoIP
// Close the VoIP connection when the bot shuts down
voip.disconnect();Group Invite & Newsletter Admin Invite
// group invite
const code = await sock.groupInviteCode(groupJid);
await sock.sendMessage(jid, {
groupInvite: {
inviteCode: code,
inviteExpiration: Math.floor(Date.now() / 1000) + 86400 * 3,
jid: groupJid,
subject: 'Group Name',
text: 'Join us!'
}
});
// newsletter admin invite
await sock.sendMessage(jid, {
adminInvite: {
jid: '123456@newsletter',
name: 'Channel Name',
caption: 'Become an admin of our channel!',
expiration: Math.floor(Date.now() / 1000) + 86400 * 7
}
});Share Phone & Limit Sharing
// share phone number
await sock.sendMessage(jid, { sharePhoneNumber: true });
// request phone number
await sock.sendMessage(jid, { requestPhoneNumber: true });
// restrict sharing (privacy)
await sock.sendMessage(jid, { limitSharing: true });
await sock.sendMessage(jid, { limitSharing: false }); // lift the restrictionList Reply & Button Reply
For replying to incoming interactive messages:
// reply to a list (single_select)
await sock.sendMessage(jid, {
buttonReply: { title: 'Item Title', description: 'Description', rowId: 'row_id' },
type: 'list'
});
// reply to a template button
await sock.sendMessage(jid, {
buttonReply: { displayText: 'Button Text', id: 'button_id', index: 0 },
type: 'template'
});
// reply to a plain button
await sock.sendMessage(jid, {
buttonReply: { id: 'button_id', displayText: 'Button Text' },
type: 'plain'
});
// reply to a native flow / interactive button
await sock.sendMessage(jid, {
buttonReply: {
displayText: 'Text',
nativeFlows: {
name: 'quick_reply',
paramsJson: JSON.stringify({ id: 'btn_id' }),
version: 1
}
},
type: 'interactive'
});
// list response (listReply)
await sock.sendMessage(jid, {
listReply: {
singleSelectReply: { selectedRowId: 'row_id' },
title: 'Selection',
listType: 1
}
});Sections (List Message)
await sock.sendMessage(jid, {
sections: [
{
title: 'Category 1',
rows: [
{ title: 'Item A', description: 'Description A', id: 'item_a' },
{ title: 'Item B', description: 'Description B', id: 'item_b' },
]
},
{
title: 'Category 2',
rows: [
{ title: 'Item C', id: 'item_c' },
]
}
],
title: 'List Title',
buttonText: 'View Options',
text: 'Pick one:',
footer: 'footer text',
mentions: ['[email protected]'],
}, { quoted: m });Product List
await sock.sendMessage(jid, {
productList: [
{
title: 'Product Category',
products: [
{ productId: 'prod_1' },
{ productId: 'prod_2' },
]
}
],
businessOwnerJid: '[email protected]',
title: 'Catalog',
buttonText: 'View Products',
text: 'Our products:',
footer: 'footer',
thumbnail: readFileSync('./thumb.jpg'),
}, { quoted: m });Buttons (Plain)
await sock.sendMessage(jid, {
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 },
],
text: 'Choose:',
footer: 'footer',
mentions: ['[email protected]'],
}, { quoted: m });Template Buttons
await sock.sendMessage(jid, {
templateButtons: [
{
urlButton: {
displayText: 'Open Website',
url: 'https://github.com'
}
},
{
callButton: {
displayText: 'Call',
phoneNumber: '+6281234567890'
}
},
{
quickReplyButton: {
displayText: 'Reply',
id: 'reply_id'
}
}
],
text: 'Template button:',
caption: 'caption',
footer: 'footer',
}, { quoted: m });Interactive Buttons (Native Flow)
The most flexible option, supporting every modern WA button type:
await sock.sendMessage(jid, {
interactiveButtons: [
// quick reply
{
name: 'quick_reply',
buttonParamsJson: JSON.stringify({ display_text: 'Yes', id: 'yes' })
},
// open a URL
{
name: 'cta_url',
buttonParamsJson: JSON.stringify({
display_text: 'Open Link',
url: 'https://github.com',
merchant_url: 'https://github.com'
})
},
// call
{
name: 'cta_call',
buttonParamsJson: JSON.stringify({
display_text: 'Contact',
phone_number: '6281234567890'
})
},
// copy text
{
name: 'cta_copy',
buttonParamsJson: JSON.stringify({
display_text: 'Copy Code',
id: 'copy_id',
copy_code: 'CODE123'
})
},
// send location
// (v1.5.3+: the shorthand `{ type: 'location' }` also resolves correctly)
{ name: 'send_location', buttonParamsJson: '' },
// address message
{ name: 'address_message', buttonParamsJson: '' },
// dropdown / single select
{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: 'Open Menu',
sections: [{
title: 'Options',
rows: [
{ title: 'Menu 1', description: 'desc', id: 'menu1' },
{ title: 'Menu 2', id: 'menu2' },
]
}]
})
},
],
text: 'Message body',
title: 'Header title',
subtitle: 'Subtitle',
footer: 'Footer text',
// header media (optional, pick one)
image: { url: 'https://...' },
// video: { url: 'https://...' },
// document: { url: 'https://...' },
mentions: ['[email protected]'],
}, { quoted: m });Buttons not rendering? If you're on a version older than v1.5.3, see the critical fix noted in the Changelog — a shadowed
sendMessagekey meant the button wrapper was never attached for any button type. Upgrading resolves it.
With raw generateWAMessageFromContent
For full control over the proto structure:
import { generateWAMessageFromContent, prepareWAMessageMedia, proto } from 'hiura-baileys';
const media = await prepareWAMessageMedia(
{ image: { url: 'https://...' } },
{ upload: sock.waUploadToServer }
);
const msg = generateWAMessageFromContent(jid, {
interactiveMessage: proto.Message.InteractiveMessage.create({
body: { text: 'Message body' },
footer: { text: 'Footer' },
header: {
title: 'Title',
hasMediaAttachment: true,
...media
},
nativeFlowMessage: {
buttons: [
{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'OK', id: 'ok' }) },
{ name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: 'Link', url: 'https://...' }) },
]
}
})
}, { userJid: sock.user.id, quoted: m });
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id });Shop, Collection, Cards
// shop storefront
await sock.sendMessage(jid, {
shop: { surface: 1, id: 'SHOP_ID' },
text: 'Visit the shop',
title: 'Our Shop',
footer: 'Available now'
});
// or the old style: shop: 1, id: 'SHOP_ID'
// collection
await sock.sendMessage(jid, {
collection: {
bizJid: '[email protected]',
id: 'COLLECTION_ID',
version: 1
},
text: 'Latest collection',
title: 'Collection',
footer: 'Check it out'
});
// cards (shorthand via sendMessage)
await sock.sendMessage(jid, {
cards: [
{
title: 'Product A',
body: 'Price: Rp 100,000',
footer: 'footer',
image: { url: 'https://...' },
buttons: [
{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Buy', id: 'buy_a' }) }
]
},
{
title: 'Product B',
body: 'Price: Rp 150,000',
image: { url: 'https://...' },
buttons: [
{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Buy', id: 'buy_b' }) }
]
}
],
text: 'Choose a product:',
footer: 'footer'
});Carousel (Raw)
For a carousel with media on each card, use generateWAMessageFromContent:
import { generateWAMessageFromContent, prepareWAMessageMedia, proto } from 'hiura-baileys';
async function makeCard(imageUrl, bodyText, buttons) {
const media = await prepareWAMessageMedia(
{ image: { url: imageUrl } },
{ upload: sock.waUploadToServer }
);
return proto.Message.InteractiveMessage.create({
body: { text: bodyText },
footer: { text: '' },
header: { title: '', hasMediaAttachment: true, ...media },
nativeFlowMessage: { buttons }
});
}
const cards = await Promise.all([
makeCard('https://.../a.jpg', 'Product A\nRp 100,000', [
{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Buy A', id: 'buy_a' }) },
{ name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: 'Details', url: 'https://...', merchant_url: 'https://...' }) }
]),
makeCard('https://.../b.jpg', 'Product B\nRp 150,000', [
{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Buy B', id: 'buy_b' }) }
]),
]);
const msg = generateWAMessageFromContent(jid, {
interactiveMessage: proto.Message.InteractiveMessage.create({
body: { text: 'Choose a product:' },
footer: { text: 'footer' },
header: { title: '', hasMediaAttachment: false },
carouselMessage: {
cards,
messageVersion: 1,
carouselCardType: 1
}
})
}, { userJid: sock.user.id, quoted: m });
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id });Interactive Messages (Hiura Engine)
For cases where you need media + buttons via sendMessage with a more
compact format:
// Hiura Engine automatically detects the type and handles the upload
await sock.sendMessage(jid, {
interactiveMessage: {
title: 'Header Title',
footer: 'Footer',
image: { url: 'https://...' }, // or video/document
buttons: [
{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'A', id: 'a' }) }
],
contextInfo: {
mentionedJid: ['[email protected]']
},
externalAdReply: {
title: 'Ad Title',
body: 'Ad Body',
mediaUrl: 'https://...',
sourceUrl: 'https://...'
}
}
}, { quoted: m });Rich Messages (Hiura Engine)
All of the functions below are available directly on the sock object:
Table
// simple table
await sock.sendTable(
jid,
'Table Title',
['Column A', 'Column B', 'Column C'],
[
['data 1', 'data 2', 'data 3'],
['data 4', 'data 5', 'data 6'],
],
m, // quoted (optional)
{} // options
);
// table v2 (object format)
await sock.sendTableV2(
jid,
{
title: 'Title',
headers: ['A', 'B'],
rows: [['1', '2'], ['3', '4']],
footer: 'optional footer'
},
m,
{}
);List
await sock.sendList(
jid,
'List Title',
['first item', 'second item', 'third item'],
m,
{}
);Code Block
// code block v1
await sock.sendCodeBlock(
jid,
{
language: 'javascript', // js, python, cpp, go, rust, lua, css, html, bash, etc.
code: 'console.log("hello world")'
},
m,
{}
);
// code block v2 (with title)
await sock.sendCodeBlockV2(
jid,
{
language: 'python',
title: 'Python Example',
code: 'print("hello world")'
},
m,
{}
);Link
// link v1
await sock.sendLink(
jid,
'Message text',
[
{
url: 'https://github.com',
title: 'GitHub',
description: 'Link description'
}
],
m,
{}
);
// link v2
await sock.sendLinkV2(jid, 'Text', links, m, {});LaTeX
// LaTeX text (rendered on the WA side)
await sock.sendLatex(
jid,
m,
{ formula: 'E = mc^2', caption: 'Einstein\'s formula' }
);
// LaTeX as an image (requires a render function)
await sock.sendLatexImage(
jid,
m,
{ formula: '\\int_0^\\infty e^{-x} dx = 1', caption: 'Integral' },
renderLatexToPng, // custom function to render LaTeX → PNG buffer
sock.waUploadToServer
);
// LaTeX inline image
await sock.sendLatexInlineImage(jid, m, options, renderFn, uploadFn);Rich Message (combined)
await sock.sendRichMessage(
jid,
[
{ type: 'text', text: 'Intro text' },
{ type: 'code', language: 'js', code: 'const x = 1 + 1;' },
{ type: 'list', title: 'List', items: ['a', 'b', 'c'] },
{ type: 'table', title: 'Data', headers: ['K', 'V'], rows: [['key', 'val']] },
{ type: 'link', text: 'Info', links: [{ url: 'https://...', title: 'Title' }] }
],
m,
{}
);Unified Response
For replying to a bot response message:
const captured = await sock.captureUnifiedResponse(jid, m);
await sock.sendUnifiedResponse(jid, m, captured);Link Preview
await sock.sendPreview(jid, {
text: 'Description',
url: 'https://github.com',
title: 'Title',
description: 'Preview description',
image: 'https://.../preview.jpg', // or a Buffer
matchedText: 'https://github.com'
}, { quoted: m });Group Story / Group Status
// send a status to a specific group
await sock.swgc(groupJid, { text: 'Group status!' });
await sock.swgc(groupJid, { image: { url: 'https://...' }, caption: 'Status' });
// shorthand for sendStatusWhatsApp
await sock.sendStatusMention({ text: 'Status!' }, [groupJid, userJid]);Status / WA Story
// send to all contacts
await sock.sendStatusWhatsApp({ text: 'Status!' });
await sock.sendStatusWhatsApp({ image: { url: 'https://...' }, caption: 'Caption' });
// send to specific contacts/groups
await sock.sendStatusWhatsApp(
{ text: 'Just for this group!' },
['[email protected]', groupJid]
);
// alias
await sock.sendStatusMention({ text: 'hello' }, jids);Ephemeral / Disappearing Messages
// enable disappearing messages in a chat/group
await sock.sendMessage(jid, {
disappearingMessagesInChat: true
// or: 86400, 604800, 2592000 (duration in seconds)
});
// disable
await sock.sendMessage(jid, { disappearingMessagesInChat: false });
// check a group's ephemeral duration
const expiration = await sock.getEphemeralGroup(groupJid);
// returns: 0 | 86400 | 604800 | 2592000
// sendMessage automatically follows the group's ephemeral setting —
// no need to set ephemeralExpiration manuallyGroup Management
// metadata
const meta = await sock.groupMetadata(jid);
// create a group
const { id } = await sock.groupCreate('Group Name', [
'[email protected]',
'[email protected]'
]);
// manage participants
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'add');
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'remove');
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'promote');
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'demote');
// update group info
await sock.groupUpdateSubject(jid, 'New Name');
await sock.groupUpdateDescription(jid, 'New description');
// group settings
await sock.groupSettingUpdate(jid, 'announcement'); // only admins can send
await sock.groupSettingUpdate(jid, 'not_announcement');
await sock.groupSettingUpdate(jid, 'locked'); // only admins can edit info
await sock.groupSettingUpdate(jid, 'unlocked');
// group ephemeral
await sock.groupToggleEphemeral(jid, 86400); // enable
await sock.groupToggleEphemeral(jid, 0); // disable
// invite
const code = await sock.groupInviteCode(jid);
await sock.groupAcceptInvite(code);
await sock.groupRevokeInvite(jid);
// group profile picture
await sock.updateProfilePicture(jid, readFileSync('./photo.jpg'));
await sock.removeProfilePicture(jid);
// leave
await sock.groupLeave(jid);Newsletter (Channels)
// channel info
const info = await sock.getNewsletterInfo('jid@newsletter');
const infoByInvite = await sock.getNewsletterInfoFromInvite('https://whatsapp.com/channel/...');
// create a new channel
const nl = await sock.createNewsletter({
name: 'Channel Name',
description: 'Channel description',
picture: readFileSync('./photo.jpg') // optional
});
// manage
await sock.followNewsletter('jid@newsletter');
await sock.unfollowNewsletter('jid@newsletter');
await sock.muteNewsletter('jid@newsletter', true);
await sock.updateNewsletterMetadata('jid@newsletter', { name: 'New Name', description: 'New desc' });
// post to a channel
await sock.sendMessage('jid@newsletter', { text: 'First post' });
await sock.sendMessage('jid@newsletter', { image: { url: 'https://...' }, caption: 'Photo' });
// react to a channel post
await sock.newsletterReactMessage('jid@newsletter', serverId, '👍');JID Utilities
import {
jidNormalizedUser,
jidDecode,
jidEncode,
normalizeMentionJid,
resolveJid,
resolveJids,
areJidsSameUser,
isJidGroup,
isJidBroadcast,
isJidNewsletter,
isJidStatusBroadcast,
isJidBot,
isJidMetaAI,
isJidUser,
isLidUser,
isPnUser
} from 'hiura-baileys';
jidNormalizedUser('[email protected]:0') // '[email protected]'
jidDecode('[email protected]') // { user: '628111', server: 's.whatsapp.net' }
jidEncode('628111', 's.whatsapp.net') // '[email protected]'
normalizeMentionJid('628111@lid') // '[email protected]'
resolveJid('6281234567890') // '[email protected]'
resolveJids(['628111', '628222']) // ['[email protected]', '[email protected]']
areJidsSameUser('[email protected]', '628111@lid') // true
isJidGroup('[email protected]') // true
isJidNewsletter('123@newsletter') // true
isLidUser('628111@lid') // trueDecrypt Handler
Functions for decrypting encrypted messages. All of them are already wired
into processMessage automatically — no extra setup needed. The following
events are emitted automatically:
decryptEventEdit→ emitsmessages.updatedecryptComment→ emitsmessages.upsertdecryptReaction→ emitsmessages.upsert
If you need to decrypt manually:
import {
decryptPollVote,
decryptEventResponse,
decryptEventEdit,
decryptComment,
decryptReaction
} from 'hiura-baileys';
// poll vote
const vote = decryptPollVote(
{ encPayload, encIv },
{ pollCreatorJid, pollMsgId, pollEncKey, voterJid }
);
// event response
const response = decryptEventResponse(
{ encPayload, encIv },
{ eventCreatorJid, eventMsgId, eventEncKey, responderJid }
);
// event edit (v1.5.0+)
const edit = decryptEventEdit(
{ encPayload, encIv },
{ eventCreatorJid, eventMsgId, eventEncKey, responderJid }
);
// comment (v1.5.0+)
const comment = decryptComment(
{ encPayload, encIv },
{ commentCreatorJid, commentMsgId, commentEncKey, commentJid }
);
// reaction (v1.5.0+)
const reaction = decryptReaction(
{ encPayload, encIv },
{ reactionCreatorJid, reactionMsgId, reactionEncKey, reactionJid }
);In-Memory Store
import { makeInMemoryStore } from 'hiura-baileys';
import pino from 'pino';
const store = makeInMemoryStore({ logger: pino({ level: 'silent' }) });
// read/write to a file
store.readFromFile('./data/store.json');
setInterval(() => store.writeToFile('./data/store.json'), 10_000);
const sock = makeWASocket({
auth: state,
getMessage: async (key) => store.loadMessage(key.remoteJid, key.id)?.message,
cachedGroupMetadata: async (jid) => store.groupMetadata[jid]
});
store.bind(sock.ev);
// accessing data
store.chats // all chats
store.messages['[email protected]'].array // all messages for a given jid
store.contacts // all contacts
store.groupMetadata // group metadata that's been fetched
store.loadMessage(jid, id) // find a message by idAuth State
import {
useMultiFileAuthState,
useSingleFileAuthState,
useMongoFileAuthState,
makeCacheableSignalKeyStore,
addTransactionCapability,
initAuthCreds
} from 'hiura-baileys';
// default — save to a folder
const { state, saveCreds } = await useMultiFileAuthState('./sessions');
// save to a single JSON file
const { state, saveCreds } = await useSingleFileAuthState('./session.json');
// MongoDB
const { state, saveCreds } = await useMongoFileAuthState(mongoCollection);
// with caching (better performance)
const sock = makeWASocket({
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger)
}
});Handling Incoming Messages
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return;
for (const m of messages) {
if (!m.message) continue;
if (m.key.fromMe) continue; // skip our own messages
const jid = m.key.remoteJid;
const sender = m.key.participant || jid; // participant for groups
// get text content
const text = m.message?.conversation
|| m.message?.extendedTextMessage?.text
|| m.message?.imageMessage?.caption
|| m.message?.videoMessage?.caption
|| '';
// check for a quoted message
const quoted = m.message?.extendedTextMessage?.contextInfo?.quotedMessage;
const quotedKey = {
id: m.message?.extendedTextMessage?.contextInfo?.stanzaId,
remoteJid: m.message?.extendedTextMessage?.contextInfo?.remoteJid || jid,
participant: m.message?.extendedTextMessage?.contextInfo?.participant
};
// download media
if (m.message?.imageMessage) {
const buffer = await downloadMediaMessage(m, 'buffer', {});
}
}
});
// message status updates (read receipts, etc.)
sock.ev.on('messages.update', (updates) => {
for (const { key, update } of updates) {
console.log(key, update);
}
});
// reaction
sock.ev.on('messages.reaction', (reactions) => {
for (const { key, reaction } of reactions) {
console.log(key, reaction.text);
}
});
// poll update
sock.ev.on('messages.upsert', ({ messages }) => {
for (const m of messages) {
if (m.message?.pollUpdateMessage) {
// handle poll vote
}
}
});Typography & contextInfo
// bold
await sock.sendMessage(jid, { text: '*bold text*' });
// italic
await sock.sendMessage(jid, { text: '_italic text_' });
// strikethrough
await sock.sendMessage(jid, { text: '~strikethrough text~' });
// monospace
await sock.sendMessage(jid, { text: '`code`' });
// message with externalAdReply (ad-banner style)
await sock.sendMessage(jid, {
text: 'Main text',
contextInfo: {
externalAdReply: {
title: 'Banner Title',
body: 'Description',
mediaType: 1,
thumbnailUrl: 'https://.../thumb.jpg',
sourceUrl: 'https://...',
mediaUrl: 'https://...',
showAdAttribution: false,
renderLargerThumbnail: true
}
}
});
// forward a message with a score
await sock.sendMessage(jid, {
forward: m,
force: true // force a forward even if already forwarded before
});DisconnectReason
import { DisconnectReason } from 'hiura-baileys';
// all values
DisconnectReason.connectionClosed // 428
DisconnectReason.connectionLost // 408
DisconnectReason.connectionReplaced // 440
DisconnectReason.timedOut // 408
DisconnectReason.loggedOut // 401
DisconnectReason.badSession // 500
DisconnectReason.connectionError // 500
DisconnectReason.multideviceMismatch // 411
DisconnectReason.forbidden // 403
DisconnectReason.unavailableService // 503
DisconnectReason.restartRequired // 515
// example usage
sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
if (connection === 'close') {
const statusCode = new Boom(lastDisconnect?.error)?.output?.statusCode;
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
if (shouldReconnect) start();
else console.log('Session expired, please scan again');
}
});Browsers
import { Browsers } from 'hiura-baileys';
// available browser presets
Browsers.ubuntu('Chrome')
Browsers.macOS('Desktop')
Browsers.windows('Edge')
Browsers.baileys('Desktop') // default
Browsers.appropriate('Chrome')
// use in config
const sock = makeWASocket({
browser: Browsers.ubuntu('Chrome'),
...
});FAQ
Buttons/interactive not showing up in groups
Add additionalNodes when calling relayMessage:
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' } }]
}]
}]
});LID mentions don't work in groups
import { normalizeMentionJid } from 'hiura-baileys';
const fixedJid = normalizeMentionJid(lidJid);
await sock.sendMessage(groupJid, {
text: `@${fixedJid.split('@')[0]} hello`,
mentions: [fixedJid]
});Session keeps logging out
- Make sure
sock.ev.on('creds.update', saveCreds)is wired up - Don't run two instances against the same session folder
- Delete the session folder and scan again:
rm -rf ./sessions
CJS require() throws / require('hiura-baileys') doesn't work
As of v1.5.3 this is expected behavior, not a bug: index.cjs is a static
shim that immediately throws, pointing you to import or
await import(), regardless of your Node version. Solution: use
await import('hiura-baileys') inside an async function, or convert your
entry file to ESM. There's no synchronous-require() workaround anymore —
see Requirements for the reasoning.
Still on v1.5.0 or older? That version had a different issue — non-function constants (
proto,BufferJSON,DisconnectReason, etc.) only became available after some function had already beenawait-ed, so destructuring at the very top of a file threw a "not ready" error. Addingawait useMultiFileAuthState()before touching any constant worked around it back then, but that workaround is irrelevant since v1.5.1 — upgrading to the latest version is the real fix either way.
sharp error when sending album/stickerPack
Install sharp: npm install sharp
Changelog
v1.5.3
⚠️ Critical fix — buttons and location_button were never rendering
correctly. Socket/messages-send.js had two sendMessage: keys in the
same returned object literal (a leftover from a previous merge). In a JS
object literal the second key silently wins, so the first sendMessage —
the only one that actually called getButtonType()/getButtonArgs() to
attach the <biz><interactive><native_flow> wrapper WhatsApp requires for
buttons to render — was 100% dead code. The live sendMessage, used for
every interactiveButtons send, carousel, product-with-buttons, and
the generic buttonsMessage/templateMessage/listMessage fallback,
never attached that node at all. This affected every button type, not
just send_location.
- Removed the dead, shadowed
sendMessagedefinition — there's now one correct definition. - Wired
getButtonType()/getButtonArgs()into the livesendMessage's dispatch cases (PRODUCT,CAROUSEL,INTERACTIVE,INTERACTIVE_BUTTONS) and the generic fallback path, so the correct button-specific<biz>node is attached every time. - Added a
{ type: 'location' }/{ type: 'send_location' }shorthand tonormalizeButtons()(in bothSocket/hiura.jsandSocket/hiura-advanced.js) — previously only the fully-spelled-out{ name: 'send_location', buttonParamsJson: '{}' }form resolved correctly.
Message-retry / session recovery — a <receipt type="retry"> can carry
a fresh Signal key bundle for the requesting device; it's now injected
directly instead of tearing the session down for a full renegotiation
round trip. A base-key collision across repeated retries of the same
message (a sign the session is stuck) now forces a fresh session instead
of retrying indefinitely. Matters most in large, busy groups, where more
devices means more decrypt failures and more retry receipts.
Device-list cache correctness (main fix for lag in large groups) — the
previous case 'devices': handler was a stub that never touched
userDevicesCache, so the bot kept using a stale cached device list for
up to 5 minutes after any group member added, removed, or changed a
device — causing extra failed sends and retries. handleDevicesNotification()
now properly updates/invalidates the cache, and a mutex protects concurrent
writes to it.
Trusted-contact token (tctoken) freshness — expiry now uses a 28-day
rolling window (matching WA Web) instead of never expiring; outgoing
<tctoken> now carries a timestamp attribute; an out-of-order or
duplicate notification can no longer overwrite a newer stored token with
an older one.
Event buffer cleanup — added destroy() to clear timers, history
cache, and listeners on socket close/logout (avoids leaking timers across
reconnects), plus merge/dedupe logic for pastParticipants across
history-sync chunks.
Interactive buttons audited, not changed — compared feature-by-feature
against the levvleys fork (native flow button names, low-level
<biz><native_flow> stanza wrapping, proto-level message types); no
functional gap found worth porting. Added English doc comments at the two
key spots documenting how to send a location-request button.
ESM/CJS cleanup
index.cjsrewritten: it previously used Node's synchronousrequire(esm)to bridge into the ESM build, which only worked on Node=20.19.0 or >=22.12.0 — many hosting panels ship a "Node 22" that's still below 22.12, so
require('hiura-baileys')would fail there with a confusing nativeERR_REQUIRE_ESM. It's now a static, version-independent shim that immediately throws a clear message pointing toimport(orawait import()from CJS).engine-requirements.jswas dead code enforcing the same narrow Node-version gate; it's now a no-op (kept in place in case anything external still points at the file path).engines.nodeinpackage.jsonrelaxed from>=20.19.0 <21.0.0 || >=22.12.0to>=20.0.0, matching upstream baileys 7.0.0-rc14's own floor.lib/Voip/index.mjswas not touched — it usescreateRequire(), which has no such version-fragility.
v1.5.2
- CJS entry point rewritten from scratch (
index.cjs): previously used a dynamicimport()+ lazy getter, where non-function constants (proto,BufferJSON,DisconnectReason, etc.) only became available after some function had already beenawait-ed — destructuring at the very top of the file threw a "not ready" error. Now uses Node's nativerequire(esm)(stable since v20.19.0/v22.12.0) to loadlib/index.jsfully synchronously, just like a normal CJS module. - Node.js requirement raised to
>=20.19.0 <21.0.0 || >=22.12.0(previously only checked the major version>=20, which wasn't strict enough — Node 20.0–20.18 and all of 21.x were actually unsupported). engine-requirements.jsnow checks the minor version too, not just the major, with a clearer error message.- Auto-diagnosis for
ERR_REQUIRE_ASYNC_MODULE: ifrequire(esm)fails because something in the dependency graph has a top-level await (notlib/index.jsitself, but one of its dependencies),index.cjsnow automatically runs a--experimental-print-required-tladiagnosis via a child process and prints the source file/line straight into the error log — no need to run the flag manually. - VoIP (voice calls): added a complete
lib/Voip/module —VoipClient(connect, call, disconnect),ActiveCall(events: accepted/ended/error, mute, sendAudio), audio streaming into an active call, and WhatsApp's WASM engine underlib/assets/wasm/. Requires@roamhq/wrtcto function. - Added
buttonsMessage.locationMessageas an interactive header — send buttons with a location as the media header ({ location: {...}, buttons: [...] }), distinct from a standalonelocationMessage. nativeFlowInfoon legacy buttons (buttonId/buttonText) is now preserved when converted tobuttonsMessage, so regularquick_replybuttons can be mixed withcta_url/cta_callin a single legacy-format message.
v1.5.0
- Ported
decryptEventEdit,decryptComment,decryptReactionand their automatic handlers inprocessMessage - Added
meLid = creds.me?.lidinprocessMessage - Quoted messages:
quotedType = EXPLICIT+threadId VIEW_REPLIESin groups - Fixed dual shop API styles: both
{ shop: { surface, id } }and{ shop: surface, id }are now supported - Complete TypeScript declarations for every button/interactive type
- CJS wrapper updated: 183 → 317 exports (
DisconnectReason,Browsers, and all utils are now available in CJS)
v1.4.0
- Fixed
isJidNewslettererror on lazy-loadedwaUploadToServer - Fixed carousel crash
upload is not a function - Fixed
ptvMessagereturn type - Added
normalizeMentionJid,resolveJid,resolveJids - Added
getEphemeralGroupand auto ephemeral detection insendMessage - Converted
hiura-advanced.jsto proper ESM
v1.2.1
- Hiura Engine:
handleInteractive,handleInteractiveButtons,handleAlbum,handlePayment,handleProduct,handleEvent,handleGroupStory - Rich messages:
sendTable,sendTableV2,sendCodeBlock,sendCodeBlockV2,sendLatex,sendLatexImage,sendLink,sendLinkV2,sendRichMessage,sendUnifiedResponse,sendPreview - Full source maps
v1.0.0
- Base from blckrose-baileys
- Full LID + JID support
- Every interactive button type (native flow)
- Pairing code, carousel, album
- ESM + CJS dual support
