npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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-baileys
yarn add @sasa-dev/void-baileys
pnpm add @sasa-dev/void-baileys

Optional peers enable extra media handling:

npm install @napi-rs/image sharp jimp audio-decode link-preview-js better-sqlite3

Quick 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:

  1. The socket waits until the internal pair-device exchange completes before touching pairing state.
  2. The registration IQ is sent with a platform id between 1 and 6 — WhatsApp's servers silently reject the Desktop id that the stock library sends, which is exactly why the phone notification never fires.
  3. Only after the server accepts the registration are your credentials updated and the pairing.requested event emitted.
  4. 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-mlow companion or transcoder option). Without it, answerCallWithAudio refuses to accept with CALL_MEDIA_UNSUPPORTED and your existing fallback (plain answerCall) 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: true until 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:pairing

FAQ

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