@sasa-dev/void-baileys
v2.4.0
Published
Void Baileys is a full-featured TypeScript WhatsApp Web API library, built on the Baileys v7 engine with complete button support, interactive messages, albums, pairing-code push notifications and many extra features.
Maintainers
Readme
Why Void Baileys
Requirements
| Requirement | Version |
| --- | --- |
| Node.js | >= 20.0.0 (checked automatically at install time) |
| A WhatsApp account | linked via pairing code or QR |
The package is pure ESM. Use "type": "module" in your package.json, or run your code with tsx.
Install
npm install @sasa-dev/void-baileysyarn add @sasa-dev/void-baileyspnpm add @sasa-dev/void-baileysOptional peers enable extra media handling:
npm install @napi-rs/image sharp jimp audio-decode link-preview-js better-sqlite3Quick Start
The fastest way to get a session is a pairing code — your phone gets a real WhatsApp notification and you simply type the code.
import makeWASocket, { useMultiFileAuthState } from '@sasa-dev/void-baileys'
const { state, saveCreds } = await useMultiFileAuthState('void-auth')
const sock = makeWASocket({
auth: state,
printQRInTerminal: false,
})
if (!sock.authState.creds.registered) {
const phoneNumber = '94771234567'
const code = await sock.requestPairingCode(phoneNumber)
console.log('pairing code:', code?.match(/.{1,4}/g)?.join('-'))
}
sock.ev.on('connection.update', update => {
const { connection } = update
if (connection === 'open') {
console.log('connected to WhatsApp')
}
})
sock.ev.on('creds.update', saveCreds)The code arrives on your phone as a WhatsApp notification — no QR scanner needed, and no missed link requests. If the phone prompt ever fails to appear, Void Baileys automatically retries the request and falls back to a QR code in the terminal.
Pairing Code Deep Dive
const sock = makeWASocket({ auth: state })
const code = await sock.requestPairingCode('94771234567', {
customPairingCode: 'VOID123',
retryMs: 20_000,
waitMs: 90_000,
})
console.log(code)| Option | Default | Meaning |
| --- | --- | --- |
| customPairingCode | random 8 chars | a code of your choosing |
| retryMs | 20_000 | how often the link request is re-sent |
| waitMs | 90_000 | how long the code stays valid |
What happens under the hood, and why other Baileys builds fail here:
- The socket waits until the internal
pair-deviceexchange completes before touching pairing state. - The registration IQ is sent with a platform id between 1 and 6 — WhatsApp's servers silently reject the
Desktopid that the stock library sends, which is exactly why the phone notification never fires. - Only after the server accepts the registration are your credentials updated and the
pairing.requestedevent emitted. - The reg IQ carries
should_show_push_notification = true, so the phone shows the Link device prompt.
You can listen for the confirmation:
sock.ev.on('pairing.requested', data => {
console.log('phone notification requested for', data.code)
})If you prefer QR codes, flip one option:
const sock = makeWASocket({ auth: state, printQRInTerminal: true })Sending Messages
const jid = '[email protected]'
await sock.sendMessage(jid, { text: 'Hello from Void Baileys' })
await sock.sendMessage(jid, { image: { url: './cat.jpg' }, caption: 'look at this cat' })
await sock.sendMessage(jid, {
video: { url: './clip.mp4' },
mimetype: 'video/mp4',
caption: 'short clip',
})
await sock.sendMessage(jid, { audio: { url: './song.mp3' }, mimetype: 'audio/mpeg' })
await sock.sendMessage(jid, { sticker: { url: './sticker.webp' } })
await sock.sendMessage(jid, { document: { url: './report.pdf' }, mimetype: 'application/pdf', fileName: 'report.pdf' })
await sock.sendMessage(jid, {
location: { degreesLatitude: 6.9271, degreesLongitude: 79.8612 },
})
await sock.sendMessage(jid, {
contacts: {
displayName: 'Sasa Dev',
contacts: [{ vcard: 'BEGIN:VCARD\nVERSION:3.0\nFN:Sasa Dev\nTEL;type=CELL:+94771234567\nEND:VCARD' }],
},
})Albums send several media files as one swipeable message:
await sock.sendMessage(jid, {
album: [
{ image: { url: './1.jpg' } },
{ image: { url: './2.jpg' } },
{ video: { url: './3.mp4' } },
],
})Every media message accepts viewOnce: true and isSpoiler: true:
await sock.sendMessage(jid, {
image: { url: './secret.jpg' },
viewOnce: true,
isSpoiler: true,
})Buttons
Void Baileys 2.0 ships the ultra button style by default — rounded native-flow pills with a modern card border that render in every chat (personal, group and community) — while every legacy format stays one flag away:
await sock.sendMessage(jid, {
text: 'Select an option below',
footer: 'Void Pizza',
buttons: [
{ buttonId: 'order-pizza', buttonText: { displayText: 'Order Pizza' }, type: 1 },
{ buttonId: 'track-order', buttonText: { displayText: 'Track Order' }, type: 1 },
{ buttonId: 'talk-human', buttonText: { displayText: 'Talk to a Human' }, type: 1 },
],
})Reply buttons attach to a template-style message:
await sock.sendMessage(jid, {
text: 'Your pizza is on the way',
title: 'Order confirmation',
templateButtons: [
{ index: 1, text: 'OK', buttonText: 'OK' },
{ index: 2, text: 'Call us', call: '+94771234567' },
{ index: 3, text: 'Website', url: 'https://example.com' },
],
})The extras layer gives you one-liners — sendButton now renders the ultra style by default (quick-reply pills, copy-to-clipboard and CTA buttons inside the modern bordered card):
const socket = makeVoidExtrasSocket(sock)
await socket.sendButton(jid, {
text: 'Choose your meal',
footer: 'Void Pizza',
buttons: [
{ id: 'pizza', text: 'Pizza' },
{ id: 'burger', text: 'Burger' },
{ copy: 'npm i @sasa-dev/void-baileys', text: 'Copy install cmd' },
{ url: 'https://example.com/menu', text: 'Website' },
],
})Need the old arrow-row look for a specific send? Flip the style flag:
await socket.sendButton(jid, {
text: 'Legacy layout',
style: 'classic',
buttons: [{ id: 'old-school', text: 'Old style' }],
})Read button replies from your user:
sock.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0]
const text = msg?.message?.buttonsResponseMessage?.selectedDisplayText
?? msg?.message?.templateButtonReplyMessage?.selectedDisplayText
if (text) {
console.log('user tapped:', text)
}
})Interactive Messages
List messages:
await sock.sendMessage(jid, {
title: 'MENU',
text: 'Today\'s specials',
buttonText: 'Browse',
footer: 'Void Store',
sections: [
{
title: 'Pizza',
rows: [
{ title: 'Margherita', description: 'tomato, basil, mozzarella', rowId: 'margherita' },
{ title: 'Pepperoni', description: 'spicy pepperoni, extra cheese', rowId: 'pepperoni' },
],
},
],
})Carousel cards — swipeable product cards:
await sock.sendMessage(jid, {
carouselMessage: [
{
cardId: 'card-1',
title: 'Margherita',
description: 'Classic tomato and basil',
media: { url: './pizza.jpg' },
buttons: [
{ buttonId: 'buy-1', buttonText: { displayText: 'Buy' }, type: 1 },
],
},
],
})Or use the extras helpers:
const socket = makeVoidExtrasSocket(sock)
await socket.sendInteractive(jid, {
title: 'Welcome',
text: 'What can I help you with today?',
footer: 'Void Bot',
nativeFlow: [
{ id: 'support', text: 'Support' },
{ id: 'sales', text: 'Sales' },
],
})
await socket.sendList(jid, {
title: 'MENU',
text: 'Pick a category',
buttonText: 'Open',
sections: [
{ title: 'Food', rows: [{ title: 'Pizza', rowId: 'pizza' }, { title: 'Pasta', rowId: 'pasta' }] },
],
})
await socket.sendCarousel(jid, [card1, card2, card3])Low-RAM Mode
Version 2.0 is tuned for tiny VPS boxes and 1 GB hosts. With lowMemoryMode the socket disables full history sync, skips the recent-message cache and swaps every internal retry, device, call and media store for a bounded cache with expiry, so RAM stays flat instead of growing with uptime:
const sock = makeWASocket({
auth: state,
lowMemoryMode: true,
})Big media is streamed straight to disk instead of living in RAM:
const saved = await sock.saveMedia(message, './downloads/clip.mp4')
console.log('wrote', saved.bytes, 'bytes to', saved.path)Live memory telemetry for your dashboard or heartbeat logs:
console.log(sock.memoryStats())v2 Power Features
| Feature | Call |
| --- | --- |
| Scheduled sends | sock.scheduleMessage(jid, content, 60000) or pass a Date — returns { id, at, cancel } |
| Auto-responder engine | sock.onAutoReply('price', { text: '...' }) with string or RegExp match, function replies, offAutoReply(id), setAutoReplyEnabled(false) |
| Safe broadcast | sock.broadcastMessage(jids, content, { delayMs: 1500 }) — staggered sends, per-jid report |
| Recall with retry | await sock.recallMessage(key) deletes for everyone and retries on flaky networks |
| Stream-to-disk media | sock.saveMedia(msg, path) pipes downloads to disk, keeps 1 GB hosts alive |
| Channel poll votes | sock.voteChannelPoll({ channel, serverId?, options }) — by link, server id or option number |
| Group poll votes | sock.votePoll(pollMessage, [1, 3]) — encrypted E2E voting |
| 24/7 engine | alwaysOn: true + alwaysOnline: true — self-healing socket, stable event bus |
| Auto call answer | sock.autoAnswerCalls({ onCall }) — answers voice/video offers, agent hookup hook |
| Memory guard | bounded caches by default + heap guard, memoryGuardMb for tiny dynos |
| Mini games | startGame(jid, 'chess' \| 'tictactoe' \| 'connect4') + gameMove — full-rules chess with bot |
| Menu cards | sendMenuCard(...) — neon/emerald/sunset tap-to-copy boards |
Channel Poll Voting
Version 2.1 votes in channel polls the same way WhatsApp Web does — a plaintext poll stanza keyed on the poll's server id, one SHA-256 digest per chosen option. Point it at a channel by invite link, code or jid; pick options by number, exact name or raw hash; drop serverId and the latest poll in the channel is found automatically:
await sock.voteChannelPoll({
channel: 'https://whatsapp.com/channel/0029VabYwdn7tQQxgGJTKz3V',
options: [1],
})
await sock.voteChannelPoll({
channel: '120363000000000000@newsletter',
serverId: 42,
options: ['Blue', 'Green'],
})
await sock.voteChannelPoll({ channel: inviteCode, remove: true })Option numbers are 1-based (1 = first option). Every call sends the whole selection and replaces the previous one; remove: true withdraws the vote.
Regular group and chat polls are covered too — pass the poll creation message and your picks:
sock.ev.on('messages.upsert', async ({ messages }) => {
const poll = messages.find(m => m.message?.pollCreationMessage)
if (poll) {
await sock.votePoll(poll, [1, 3])
}
})24/7 Mode
Bots on tiny hosts should simply never die. alwaysOn gives the socket a self-healing lifecycle: disconnects that are not logouts are retried with exponential backoff while your event handlers stay attached to a stable event bus, and alwaysOnline keeps broadcasting presence so the account shows as online around the clock:
const sock = makeWASocket({
auth: state,
alwaysOn: true,
alwaysOnline: true,
lowMemoryMode: true,
})
sock.ev.on('connection.update', update => {
if (update.connection === 'open') {
console.log('alive for', sock.uptimeMs(), 'ms')
}
})sock.shutdown() stops the engine cleanly when you really want out.
Call Engine
Incoming voice and video calls no longer have to ring out. autoAnswerCalls watches call events and answers offers on the same stanza envelope WhatsApp itself uses, then hands the live call to your handler — the natural hookup point for an AI voice agent:
const stop = sock.autoAnswerCalls({
onCall: async info => {
console.log('live call with', info.from, info.isVideo ? '(video)' : '(voice)')
await agent.joinCall(info)
},
})sock.answerCall(id, from) and sock.hangupCall(id, from) give manual control, and video: false limits auto-answer to voice calls.
Protocol note: the answer happens at the signaling layer. WhatsApp does not expose the WebRTC audio path of a live call to the web protocol, so piping TTS audio into the call itself needs an external media bridge — the onCall hook is exactly where yours plugs in.
Memory Guard
Bots on Heroku-class dynos used to die from heap growth. Every socket now caps its internal caches by default, and a guard samples the heap every 60 seconds: past the limit (256 MB in lowMemoryMode, 512 MB otherwise, or your own memoryGuardMb) it flushes the caches and the event buffer and releases the slack, keeping long-lived bots flat instead of saw-toothed into a crash:
const sock = makeWASocket({
auth: state,
lowMemoryMode: true,
memoryGuardMb: 200,
})Mini Games & Menu Cards
Ship the bots you saw everywhere — chess, tic-tac-toe and connect-four with rendered neon cards — plus tap-to-copy menu boards, all from the library. The chess engine is a full rules implementation (castling, en passant, promotion, check and mate detection, verified against perft node counts), and every game ships a bot opponent out of the box:
await sock.startGame(jid, 'chess')
await sock.startGame(jid, 'tictactoe')
await sock.startGame(jid, 'connect4')
sock.ev.on('messages.upsert', async ({ messages }) => {
const text = (messages[0]?.message as any)?.conversation || ''
if (text.startsWith('.move')) {
await sock.gameMove(jid, text.slice(6))
}
})Menu cards render exactly like the modern download-menu bots, only sharper:
await sock.sendMenuCard(jid, {
title: 'DOWNLOAD MENU',
subtitle: 'Tap to copy!',
theme: 'neon',
tiles: [
{ name: 'VIDEO', desc: 'YouTube downloads', cmd: '.video', emoji: '🎬' },
{ name: 'AI', desc: 'AI settings', cmd: '.ai', emoji: '🤖' },
],
})Cards are rasterised to PNG when sharp is installed (npm i sharp) and gracefully fall back to styled text otherwise. Themes: neon, emerald, sunset.
In-Call Media Engine
Real multi-device call media, implemented from the WhatsApp relay protocol (offer/preaccept/accept signalling, per-JID SRTP key derivation from the e2e call key, deterministic HKDF SSRCs, WebRTC data-channel relay transport with pinned relay certificate, STUN registration, RTP pacing, RTCP NACK/PLI/FIR handling and VP8 packetisation):
const session = await sock.answerCallWithAudio(call.id, call.from, {
source: '/path/welcome.mp3',
autoHangup: true,
maxDurationMs: 300000
})
await session.waitForConnected()
await session.waitForPlaybackEnd()
const video = await sock.answerCallWithVideo(call.id, call.from, {
source: '/path/welcome.mp4',
includeAudio: true,
allowSilentVideo: true,
quality: 'balanced'
})Also: answerCallWithMedia(call, { audio, video }), createIncomingCallSession(call) and lifecycle events call.session, call.connected, call.audio.started, call.audio.ended, call.video.started, call.video.ended, call.media.stats, call.ended, call.error. Stable error codes (CALL_MEDIA_UNSUPPORTED, CALL_CONNECT_TIMEOUT, CALL_ALREADY_ACTIVE, CALLER_HUNG_UP, VIDEO_AUDIO_TRACK_MISSING, …) are thrown as CallMediaError.
Honest status — read before relying on audible voice:
- Signalling, relay transport, SRTP/RTP crypto and VP8 video packetisation are implemented and covered by the unit suite in this repo (roundtrip, pacing, packetisation, RTCP, relay parsing, state machine, cleanup and isolation tests).
- WhatsApp relays carry MLow audio, Meta's proprietary codec. The engine exposes it as an injectable codec (
@sasa-dev/void-mlowcompanion ortranscoderoption). Without it,answerCallWithAudiorefuses to accept withCALL_MEDIA_UNSUPPORTEDand your existing fallback (plainanswerCall) runs instead — silent fake connections are never produced. - Video calls play the VP8 stream through the relay; the file's sound needs the same MLow companion, so use
allowSilentVideo: trueuntil the companion is installed. - Transcoding uses the host's
ffmpeg/ffprobe; sources accept path,Buffer, stream or local{ url }(remote http is rejected for SSRF safety). - Final proof on real Android/iOS devices is on your side: pair a bot, call it, and verify the caller hears/sees the media. Everything below the device boundary is tested here.
Newsletters
Void Baileys ships the newsletter media-upload fixes from the reference library — the media URL map and retry keys are handled correctly, so channel posts with images work reliably.
const newsletterId = '123456789@newsletter'
const created = await sock.newsletterCreate('Void Dev Log', { description: 'updates about Void Baileys' })
await sock.sendMessage(newsletterId, { text: 'v2.0.0 is out' })
await sock.sendMessage(newsletterId, { image: { url: './banner.jpg' }, caption: 'the new release banner' })
const followers = await sock.newsletterMetadata('jid', newsletterId)Reactions, Edits & More
await sock.sendMessage(jid, { react: { text: '🔥', key: originalMsg.key } })
await sock.sendMessage(jid, { text: 'fixed typo', edit: originalMsg.key })
await sock.sendMessage(jid, { delete: originalMsg.key })
await sock.sendMessage(jid, { text: 'keep this pinned', keepInChat: originalMsg.key })
await sock.sendMessage(jid, {
poll: {
name: 'Which feature next?',
values: ['Buttons', 'Newsletters', 'Carousels'],
selectableCount: 1,
},
})Group status updates:
await sock.sendMessage('status@broadcast', { text: 'good morning', backgroundColor: '#075e54', font: 3 })Receiving Messages
sock.ev.on('messages.upsert', ({ messages, type }) => {
for (const msg of messages) {
if (!msg.message) continue
const text = msg.message.conversation || msg.message.extendedTextMessage?.text || ''
console.log(`${msg.key.remoteJid}: ${text}`)
}
})Common events:
| Event | Fires when |
| --- | --- |
| connection.update | the socket connects, reconnects or closes |
| creds.update | credentials change — always persist them |
| messages.upsert | messages arrive or get updated |
| messages.update | reactions, receipt ticks and message edits |
| messages.delete | messages get revoked |
| contacts.update | contact list changes |
| chats.update | chat list changes |
| groups.upsert | group metadata changes |
| group-participants.update | members join, leave or get promoted |
| presence.update | typing and online status |
| pairing.requested | a pairing-code link request was sent to the phone |
Persist credentials across restarts:
import makeWASocket, { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from '@sasa-dev/void-baileys'
const start = async () => {
const { state, saveCreds } = await useMultiFileAuthState('void-auth')
const sock = makeWASocket({ auth: state, printQRInTerminal: false })
sock.ev.on('creds.update', saveCreds)
sock.ev.on('connection.update', update => {
const { connection, lastDisconnect } = update
if (connection === 'close') {
const code = (lastDisconnect?.error as any)?.output?.statusCode
if (code !== DisconnectReason.loggedOut) {
start()
}
}
if (connection === 'open') {
console.log('ready')
}
})
}
start()Extras API
Wrap your socket with makeVoidExtrasSocket and all helpers are available directly on it:
import makeWASocket, { makeVoidExtrasSocket } from '@sasa-dev/void-baileys'
const sock = makeVoidExtrasSocket(makeWASocket({ auth: state, printQRInTerminal: false }))| Helper | What it does |
| --- | --- |
| startPairing(phone, opts) | pairing code with automatic retries and QR fallback |
| sendButton(jid, opts) | ultra-modern buttons that render in every chat (style: 'classic' for the old look) |
| sendInteractive(jid, opts) | interactive message with buttons, every-chat by default |
| sendTemplate(jid, opts) | template message |
| sendList(jid, opts) | list message with sections |
| sendCarousel(jid, cards) | carousel of cards |
| sendPoll(jid, name, values) | poll |
| sendQuiz(jid, name, values, answer) | quiz with a correct answer |
| sendLink(jid, text, url, title, desc) | link preview message |
| sendQuote(jid, text, quoted) | reply to a message |
| sendTextWithMentions(jid, text, jids) | text mentioning users |
| sendMentionAll(jid, text) | text mentioning everyone in a group |
| sendReaction(jid, emoji, key) | react to a message |
| sendEdit(jid, text, key) | edit a sent message |
| sendKeepInChat(jid, key, type) | keep a disappearing message in chat |
| sendVoice(jid, buffer, seconds) | voice note |
| sendAudio(jid, buffer, seconds) | audio message |
| sendContact(jid, name, number) | contact card |
| sendGallery(jid, images, caption) | album |
| sendBroadcast(jids, content, delayMs) | send the same content to many chats |
| sendStickerPack(jid, stickers) | sticker pack |
| sendCodeBlock(jid, code, language) | formatted code block |
| sendTable(jid, title, rows) | formatted table |
| sendProduct(jid, product, owner) | catalog product |
| sendOrder(jid, items) | order message |
| sendPaymentRequest(jid, ...) | payment request |
| sendGroupStatus(jid, content) | status update in a group |
| sendSpoiler(jid, content) | spoiler media |
| sendSticker(jid, sticker, animated) | sticker |
| sendRaw(jid, raw) | raw message passthrough |
| scheduleMessage(jid, content, when) | delayed send — returns { id, at, cancel } |
| cancelScheduled(id) / listScheduled() | manage the send queue |
| onAutoReply(match, reply) | keyword or RegExp auto-responder, offAutoReply(id), setAutoReplyEnabled(on) |
| broadcastMessage(jids, content, opts) | staggered broadcast with a per-jid report |
| recallMessage(key) | delete-for-everyone with automatic retries |
| saveMedia(msg, path) | stream downloads straight to disk |
| memoryStats() | RSS and heap telemetry for tiny hosts |
| downloadMediaMessage(msg, type, opts) | download incoming media |
| getName(jid) | resolve a contact or group name |
| getDeviceInfo() | current device and platform info |
Examples
The Examples folder contains runnable TypeScript starters:
| File | What it shows |
| --- | --- |
| example.ts | complete bot — pairing, persistence, buttons, events |
| example-pairing.ts | pairing-code flow with the phone notification |
| example-buttons.ts | every button and interactive message style |
git clone https://github.com/darksasa1-eng/void-baileys.git
cd void-baileys
npm install
npm run example:pairingFAQ
The pairing code never shows on my phone. This is fixed in Void Baileys — the link request is only sent after the socket handshake completes and uses a platform id WhatsApp accepts. Make sure WhatsApp is installed, logged in and allowed to show notifications on the phone.
Can I use QR codes instead?
Yes — makeWASocket({ printQRInTerminal: true }) prints a QR in the terminal.
Node 20 or newer is required.
The install-time check (engine-requirements.js) refuses to install on older Node.
Is this compatible with other Baileys code?
The core API is the Baileys v7 API, so most existing code ports over unchanged. All classic content types — including buttons — keep working.
Credits
Built on the Baileys WhatsApp Web API engine (MIT), with feature work inspired by the @itsliaaa/baileys fork. Written in TypeScript, from source.
Author: Sasa Dev
License: MIT
