@dnuzi/baileys
A fast, lightweight WhatsApp Web API for Node.js.
Install · Quick Start · Performance · Messages · Interactive · AI Rich · Calls · Credits
✨ Highlights
| 🆔 Identity | LID, phone number and username support |
| ⚡ Performance | Built for many sessions at once, low CPU and memory use |
| 📦 Dependencies | Zero runtime dependencies, no native or WebRTC builds |
| 📞 Calls | Audio and video calls, playlists, multi-call |
| 🧩 Messages | Buttons, carousels, widgets, polls, events, payments, AI rich |
| 🗄️ Sessions | Multi-file, single-file, SQLite or MongoDB auth storage |
Made by @DanuZz · Package: npmjs.com/package/@dnuzi/baileys
v0.1.2 Update: Optimized CPU & RAM consumption across all internal caches (adaptive sweep intervals, bounded cache limits, cache-first signal store mutex bypass, and proper timer cleanup). Full CommonJS require() support without ERR_REQUIRE_ASYNC_MODULE.
v0.1.1 Update: Fixed VoIP video call rendering on answered calls. Resolved an issue where answering video calls played audio but showed no video by properly packetizing H.264 NALUs (SPS, PPS, and fragmented IDR keyframes) per RFC 6184, and synchronized video SRTP sessions on call acceptance.
📥 Install
Requires Node.js 18+ and FFmpeg. Calls also need ffprobe available in PATH.
npm install @dnuzi/baileys
{
"dependencies": {
"@dnuzi/baileys": "v0.1.1"
}
}
🚀 Quick Start
A complete bot that links by QR or pairing code and answers !ping.
import makeWASocket, {
useMultiFileAuthState,
DisconnectReason,
extractMessageContent
} from '@dnuzi/baileys'
const PHONE_NUMBER = '' // e.g. '948123456789' to use a pairing code
const PAIR_CODE = '' // optional custom 8-character code
const start = async () => {
const { state, saveCreds } = await useMultiFileAuthState('./session')
const sock = makeWASocket({
auth: state,
printQRInTerminal: !PHONE_NUMBER,
syncFullHistory: false
})
sock.ev.on('creds.update', saveCreds)
let pairingRequested = false
sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
if (qr && PHONE_NUMBER && !pairingRequested) {
pairingRequested = true
try {
console.log('Pairing code:', await sock.requestPairingCode(PHONE_NUMBER, PAIR_CODE || undefined))
} catch (err) {
console.error('Could not request a pairing code:', err.message)
}
}
if (connection === 'open') console.log('Connected as', sock.user?.id)
if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode
if (code === DisconnectReason.loggedOut) {
return console.log('Logged out. Delete ./session and run again to link a new device.')
}
if (code === DisconnectReason.connectionReplaced) {
return console.log('This session was opened somewhere else. Not reconnecting.')
}
console.log(`Connection closed (${code}), reconnecting...`)
setTimeout(start, 1000)
}
})
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return
for (const m of messages) {
if (m.key.fromMe || !m.message) continue
const content = extractMessageContent(m.message)
const text = content?.conversation || content?.extendedTextMessage?.text
if (text === '!ping') {
await sock.sendMessage(m.key.remoteJid, { text: 'pong' }, { quoted: m }).catch(console.error)
}
}
})
}
start()
How to link
| Method | Steps |
|---|---|
| QR code | Leave PHONE_NUMBER empty, then scan from Linked devices → Link a device |
| Pairing code | Set PHONE_NUMBER, choose Link with phone number instead, enter the code |
| Custom code | Set PAIR_CODE to any 8 characters (empty = random) |
Your login is stored in
./session. Keep it private and out of git.
⚙️ Socket options
| Option | Default | What it does |
|---|---|---|
auth | – | Required. The state from an auth helper |
performanceMode | 'balanced' | Performance preset: 'balanced' (safe defaults), 'high' (aggressive caching & lowest latency), or 'low-memory' (RAM-constrained environments) |
dedupTTL | 30000 (ms) | Deduplication window for incoming message stanzas |
dedupMax | 2000 | Maximum messages tracked in deduplication buffer |
groupMetadataCacheTTL | 300000 (ms) | In-memory group metadata cache TTL |
browser | Browsers.macOS('Chrome') | Device name in Linked devices (also .ubuntu(), .windows(), .appropriate()) |
printQRInTerminal | false | Print each QR in the terminal (qr is always emitted on connection.update) |
qrTimeout | 60000, then 20000 | How long each QR stays valid |
syncFullHistory | true | Set false for lighter bots |
markOnlineOnConnect | true | Set false to keep notifications on your phone |
connectTimeoutMs | 20000 | Opening handshake timeout |
keepAliveIntervalMs | 30000 | Ping interval |
defaultQueryTimeoutMs | 60000 | Server request timeout |
getMessage | undefined | Return a stored message by key so retries and poll votes can be decrypted |
disableStickyRouting | false | true stops sending ED in the URL and the sticky_routing cookie |
waWebSocketUrl | wss://web.whatsapp.com/ws/chat | Custom endpoint only |
logger | built-in | Any pino-style logger |
⚡ Performance Optimization & Latency Tuning
@dnuzi/baileys includes an internal performance engine engineered to minimize bot response latency and eliminate redundant network operations without breaking any existing Baileys APIs.
🚀 Performance Modes
You can select a performance mode when initializing your socket:
const sock = makeWASocket({
auth: state,
performanceMode: 'high' // 'balanced' | 'high' | 'low-memory'
})
| Mode | Target | Description |
|---|---|---|
'balanced' (default) | General Bots | Safe, backward-compatible defaults. In-memory group metadata caching (5 min), message deduplication (30s window), request coalescing, and safe event isolation. |
'high' | Ultra-fast Response Bots | Maximum throughput and lowest latency. Extended caching (15 min group cache, 60s dedup), aggressive request coalescing, enlarged signal cache, and zero-overhead lazy logging. |
'low-memory' | VPS / Embedded (≤512MB RAM) | Compact cache bounds (100 groups, 500 messages, 2 min TTL) with aggressive garbage collection. |
🛠️ Core Optimization Architecture
-
Fast Message Pipeline & Deduplication
- Incoming message stanzas are checked against a high-speed, bounded circular cache (
MessageDeduplicator) before expensive cryptographic decryption. - Duplicate message delivery stanzas are acknowledged and dropped instantly (~116,000+ msgs/sec throughput).
- If decryption fails (e.g. key missing/resend needed), the entry is automatically forgotten so retries can proceed seamlessly.
- Incoming message stanzas are checked against a high-speed, bounded circular cache (
-
Intelligent Request Coalescing
- Prevents duplicate concurrent lookups (e.g. 50 incoming messages requesting group metadata at the same millisecond).
- Reuses the in-flight Promise so 50 concurrent requests trigger exactly 1 network query, reducing server round-trips by up to 98%.
-
In-Memory Group Metadata & LID Caching
sock.groupMetadata(jid)caches results with automatic invalidation on group changes (subject, description, participants, settings).- Pre-populates the cache during initial sync (
groupFetchAllParticipating) so your bot answers commands without cold-start delay. - Access the cache directly via
sock.groupMetadataCache.
-
Event Emitter Fault Isolation
- Broken or crashing listener functions registered on
sock.ev.on('messages.upsert', ...)are safely isolated. - One plugin/handler error will never crash other listeners or break the WhatsApp connection pipeline.
- Broken or crashing listener functions registered on
-
Optimized Serialization & Sender Keys
- In-memory sender key record caching avoids constant JSON serialization and file/database re-reading during rapid group chat traffic.
-
Zero-Overhead Lazy Logging
- Uses
logger.isLevelEnabled(level)and lazy argument evaluation so debug information is never formatted when running in production.
- Uses
📊 Real-World Benchmarks
Run the benchmark suite on your own machine:
npm run benchmark
Results on standard hardware (Node.js 20+):
- Message Deduplicator: 115,000+ msgs/sec throughput
- Request Coalescer: 50 concurrent callers → 1 network operation (100% deduplicated in 27ms)
- Isolated Event Dispatch: 19,000+ events/sec with 100% fault-isolation
- LRU-TTL Cache: 165,000+ operations/sec
🗄️ Session storage
All helpers return { state, saveCreds }, so you can swap them freely.
import {
useMultiFileAuthState,
useSingleFileAuthState,
useSqliteAuthState,
useMongoDBAuthState
} from '@dnuzi/baileys'
const multi = await useMultiFileAuthState('./session') // one file per key
const single = await useSingleFileAuthState('./session.json') // one file, flushed in batches
const sqlite = await useSqliteAuthState({ dbPath: './session.db' }) // SQLite (file-based, zero setup)
const mongo = await useMongoDBAuthState({ url: 'mongodb://localhost:27017' }) // MongoDB (cloud & Docker)
SQLite Auth Storage
Stores session credentials and Signal cryptographic keys in an SQLite database file. Built-in WAL (Write-Ahead Logging) mode ensures safe concurrent transactions with ultra-low memory overhead.
[!TIP] Zero extra dependencies required! Baileys includes a built-in lightweight pure-JS SQLite engine, or you can plug in
better-sqlite3for native C++ performance.
1. Basic setup (Built-in engine)
import makeWASocket, { useSqliteAuthState } from '@dnuzi/baileys'
const { state, saveCreds } = await useSqliteAuthState({
dbPath: './session.db'
})
const sock = makeWASocket({
auth: state,
printQRInTerminal: true
})
sock.ev.on('creds.update', saveCreds)
2. Using with better-sqlite3 (Optional native driver)
import Database from 'better-sqlite3'
import makeWASocket, { useSqliteAuthState } from '@dnuzi/baileys'
const db = new Database('./session.db')
const { state, saveCreds } = await useSqliteAuthState({
database: db
})
const sock = makeWASocket({
auth: state
})
sock.ev.on('creds.update', saveCreds)
Options for useSqliteAuthState
| Option | Type | Description |
|---|---|---|
dbPath | string | File path to the SQLite database (e.g. './session.db'). Uses the built-in pure-JS database engine |
database | object | An existing database instance (e.g. from better-sqlite3 or sqlite3) |
MongoDB Auth Storage
Store session credentials and encryption keys directly in MongoDB (local or MongoDB Atlas). Ideal for cloud deployments (Railway, Render, Heroku, AWS, Docker) where local files are ephemeral, or for managing multiple accounts in a single database.
[!NOTE] When connecting via MongoDB URI (
url/uri), ensure themongodbpackage is installed in your project:npm install mongodb
1. Connect via MongoDB URI
import makeWASocket, { useMongoDBAuthState } from '@dnuzi/baileys'
const { state, saveCreds } = await useMongoDBAuthState({
url: 'mongodb+srv://user:[email protected]/?retryWrites=true&w=majority',
dbName: 'whatsapp_bot', // optional (default: 'baileys')
collectionName: 'sessions', // optional (default: 'auth_state')
sessionId: 'bot-1' // optional: isolate multiple accounts in one collection
})
const sock = makeWASocket({
auth: state,
printQRInTerminal: true
})
sock.ev.on('creds.update', saveCreds)
2. Use an existing MongoDB collection or client
If your app already connects to MongoDB, pass the collection directly — no extra driver connection needed:
import { MongoClient } from 'mongodb'
import { useMongoDBAuthState } from '@dnuzi/baileys'
const client = new MongoClient('mongodb://localhost:27017')
await client.connect()
const collection = client.db('whatsapp_bot').collection('sessions')
// Pass collection directly (with optional sessionId as 2nd argument)
const { state, saveCreds } = await useMongoDBAuthState(collection, 'main-session')
3. Use with Mongoose
import mongoose from 'mongoose'
import { useMongoDBAuthState } from '@dnuzi/baileys'
await mongoose.connect('mongodb://localhost:27017/whatsapp_bot')
const collection = mongoose.connection.collection('sessions')
const { state, saveCreds } = await useMongoDBAuthState(collection)
Options for useMongoDBAuthState
| Option | Type | Description |
|---|---|---|
url / uri | string | MongoDB connection string (e.g. mongodb://localhost:27017 or Atlas URI) |
collection | Collection | Existing MongoDB collection or Mongoose model |
db / database | Db | Existing MongoDB database instance (client.db(...)) |
client | MongoClient | Existing MongoClient instance |
dbName | string | Database name (default: 'baileys' or DB from connection string) |
collectionName | string | Collection name (default: 'auth_state') |
sessionId | string | Session ID to namespace credentials and keys in a shared collection |
mongoOptions | object | Connection options passed directly to MongoClient |
🔌 Disconnect reasons
lastDisconnect.error.output.statusCode maps to DisconnectReason:
| Code | Reason | Do this |
|---|---|---|
401 | loggedOut | Delete the session and link again |
440 | connectionReplaced | Do not reconnect |
515 | restartRequired | Normal after pairing, reconnect |
428 / 408 | connectionClosed / connectionLost / timedOut | Network drop, reconnect |
500 | badSession | Reconnect |
When the server closes the socket, lastDisconnect.error.data includes wsCode and wsReason.
💬 Messages
Text, mentions, reactions, contacts
await sock.sendMessage(jid, { text: 'Hello there!' })
await sock.sendMessage(jid, {
text: 'Hi @948123456789',
mentions: ['[email protected]']
}, { quoted: m, ephemeralExpiration: 86400 })
await sock.sendMessage(jid, { react: { text: '👍', key: m.key } })
await sock.sendMessage(jid, {
contacts: {
contacts: [{
displayName: 'DanuZz',
vcard: 'BEGIN:VCARD\nVERSION:3.0\nFN:DanuZz\nTEL;type=CELL:+948123456789\nEND:VCARD'
}]
}
})
Media
await sock.sendMessage(jid, { image: { url: './photo.jpg' }, caption: 'Nice view' })
await sock.sendMessage(jid, { video: { url: './clip.mp4' }, caption: 'Clip', gifPlayback: false })
await sock.sendMessage(jid, { audio: { url: './voice.ogg' }, ptt: true })
await sock.sendMessage(jid, { document: { url: './file.pdf' }, mimetype: 'application/pdf', fileName: 'file.pdf' })
await sock.sendMessage(jid, { sticker: { url: './sticker.webp' } })
Wrappers and albums
await sock.sendMessage(jid, { image: { url: './photo.jpg' }, viewOnce: true }) // view once
await sock.sendMessage(jid, { video: { url: './clip.mp4' }, ptv: true }) // video note
await sock.sendMessage(jid, { image: { url: './surprise.jpg' }, caption: 'Peekaboo', spoiler: true }) // spoiler
await sock.sendMessage(jid, {
album: [
{ image: { url: './1.jpg' } },
{ image: { url: './2.jpg' } },
{ video: { url: './clip.mp4' } }
]
})
Stickers & sticker packs
Single sticker
await sock.sendMessage(jid, { sticker: { url: './sticker.webp' } })
📦 Sticker Pack
[!IMPORTANT] If
sharpor@napi-rs/imageis not installed, thecoverandstickersmust already be in WebP format.
await sock.sendMessage(jid, {
cover: {
url: './path/to/image.webp'
},
stickers: [
{
data: {
url: './path/to/image.webp'
}
},
{
data: {
url: './path/to/image.webp'
}
},
{
data: {
url: './path/to/image.webp'
}
}
],
name: '📦 My Sticker Pack',
publisher: 'DanuZz',
description: 'Optional pack description'
})
Sticker pack options
| Property | Type | Description |
|---|---|---|
cover | { url: string } | Buffer | Required. The pack tray icon / cover image |
stickers | Array<{ data: { url } | Buffer, emojis?: string[] }> | Required. List of stickers (up to 60 stickers) |
name | string | Name of the sticker pack (e.g. '📦 My Sticker Pack') |
publisher | string | Author / publisher name (e.g. 'DanuZz') |
description | string | Optional description of the pack |
Location & events
await sock.sendMessage(jid, {
location: { degreesLatitude: -6.2088, degreesLongitude: 106.8456, name: 'Monas' }
})
await sock.sendMessage(jid, {
event: {
name: 'Team Sync',
description: 'Discuss the Q1 roadmap',
startDate: new Date(Date.now() + 3600_000)
}
})
Polls
await sock.sendMessage(jid, {
poll: { name: 'Favorite language?', values: ['JavaScript', 'Python', 'Rust'], selectableCount: 1 }
})
// Quiz polls: newsletters only, require correctAnswer
await sock.sendMessage(newsletterJid, {
poll: { name: 'Capital of Japan?', values: ['Tokyo', 'Osaka'], pollType: 1, correctAnswer: 'Tokyo' }
})
// Final tally of a poll you created
await sock.sendMessage(jid, {
pollResult: {
name: 'Favorite language?',
votes: [{ name: 'JavaScript', voteCount: 12 }, { name: 'Python', voteCount: 9 }]
}
})
Edit, delete, pin, keep
| Action | Code |
|---|---|
| Edit | sock.sendMessage(jid, { text: 'Updated text', edit: m.key }) |
| Delete | sock.sendMessage(jid, { delete: m.key }) |
| Pin / unpin | sock.sendMessage(jid, { pin: m.key, type: 1, time: 86400 }) (type: 0 unpins) |
| Keep / remove | sock.sendMessage(jid, { keep: m.key, type: 1 }) (type: 0 removes) |
| Disappearing (group) | sock.sendMessage(groupJid, { disappearingMessagesInChat: 86400 }) (true = 7 days, false = off) |
Status updates
// jid can be an array to choose who sees the status
await sock.sendMessage([contactJid1, contactJid2], { text: 'Status update text' })
🎛️ Interactive Messages
Native flow buttons
await sock.sendMessage(jid, {
text: 'Choose one:',
footer: 'DanuZz',
nativeFlow: [
{ text: '👋🏻 Yes', id: 'yes_1' },
{ text: '📞 Call', call: '948123456789' },
{ text: '📋 Copy', copy: 'PROMO123' },
{ text: '🌐 Source', url: 'https://example.com' },
{
text: '📋 Select',
sections: [{ title: '✨ Section 1', rows: [{ title: '🏷️ Coupon', id: 'coupon_code' }] }]
}
]
})
With a media header, use caption instead of text (works with image, video, document, location, product):
await sock.sendMessage(jid, {
image: { url: './promo.jpg' },
title: 'Flash Sale',
caption: 'Up to 50% off today only',
footer: 'DanuZz',
nativeFlow: [
{ text: 'Shop Now', url: 'https://shop.example.com' },
{ text: 'Copy Code', copy: 'SALE50' }
]
})
Carousel
Card headers can be an image, a video or a product.
await sock.sendMessage(jid, {
text: 'Check out our new arrivals:',
footer: 'DanuZz',
cards: [
{ image: { url: './item-a.jpg' }, title: 'Item A', caption: 'New in stock', nativeFlow: [{ text: 'View', id: 'view_a' }] },
{ video: { url: './item-b.mp4' }, title: 'Item B', caption: 'Limited edition', nativeFlow: [{ text: 'View', id: 'view_b' }] },
{
businessOwnerJid: '[email protected]',
product: { title: 'Wireless Mouse', productImage: { url: './mouse.png' } },
title: 'Wireless Mouse',
caption: '$19.99',
nativeFlow: [{ text: 'Buy Now', id: 'buy_mouse' }]
}
]
})
Widgets (A2UI)
Combine with buttons, or pass nativeFlow: [] for none.
await sock.sendMessage(jid, {
text: 'Widget demo',
footer: 'A2UI Showcase',
nativeFlow: [{ text: '🌐 Source', url: 'https://example.com' }],
widget: {
align: 'center',
fallback: 'Widget cannot be loaded on this device', // optional
items: [
{ text: 'Welcome to the Widget Demo', variant: 'title' }, // title | body | caption
{ text: 'This is a longer description.', variant: 'body' },
{ icon: 'info' },
{ icon: 'warning', style: 'symbol' },
{ icon: 'favorite', native: true },
{ image: 'https://example.com/banner.jpg', variant: 'header', fit: 'cover', description: 'Promo banner' },
{ video: 'https://example.com/preview.mp4' },
{ audio: 'https://example.com/audio.mp3', description: 'Listen to this audio' },
{ divider: 'horizontal' }, // or 'vertical'
{ button: 'Open Website', url: 'https://example.com', variant: 'primary' },
{ input: 'name', label: 'Enter your name', value: '', variant: 'shortText' }, // or 'longText'
{ input: 'email', label: 'Email', validationRegexp: '^[^@]+@[^@]+\\.[^@]+$' },
{ checkbox: 'I agree to the terms & conditions', value: false },
{ choice: ['Red', 'Green', 'Blue'], label: 'Favorite color', variant: 'mutuallyExclusive', displayStyle: 'chips', filterable: true },
{ choice: [{ label: 'Option A', value: 'a' }, { label: 'Option B', value: 'b' }], label: 'Multi-select', variant: 'multipleSelection', value: ['a'] },
{ slider: 100, min: 0, value: 50, label: 'Volume' }, // slider = max value
{ datetime: true, label: 'Pick a date', enableDate: true, enableTime: false },
{ row: [{ text: 'Left' }, { text: 'Center' }, { text: 'Right' }], justify: 'space-between', align: 'center' },
{ column: [{ text: 'Row 1' }, { text: 'Row 2' }], justify: 'start', align: 'stretch' },
{ list: [{ text: '• First item' }, { text: '• Second item' }], direction: 'vertical' }
]
}
})
Legacy template buttons
Old format. Prefer nativeFlow.
await sock.sendMessage(jid, {
text: 'Choose one:',
footer: 'DanuZz',
templateButtons: [
{ text: 'Reply', id: 'r1' },
{ text: 'Visit', url: 'https://example.com' },
{ text: 'Call', call: '+948123456789' }
]
})
Simulated replies
Send a reply as if a user tapped a button or list row.
await sock.sendMessage(jid, { buttonReply: { id: 'yes_1', displayText: 'Yes' }, type: 'plain' })
await sock.sendMessage(jid, { listReply: { id: 'row1', title: 'Row 1', description: 'desc' } })
await sock.sendMessage(jid, { flowReply: { name: 'single_select', paramsJson: JSON.stringify({ id: 'row1' }) } })
🤖 AI Rich Messages
Build structured rich responses with a chainable builder.
await sock.aiRich()
.setTitle('Ai Rich Message')
.addText('[HyperLink](https://example.com)\nCitation [](https://example.com)')
.addImage('https://example.com/image.png')
.addCode('javascript', `console.log('Hello World')`)
.addHtml(['<html>Hello world</html>', 'Tab 1'], ['<html>Hi twin</html>', 'Tab 2'])
.addTable([
['Name', 'DanuZz'],
['Bio', 'Im developer'],
['Age', '67']
])
.addSource([['https://example.com/favicon.ico', 'https://example.com', 'Source']])
.addTip('Tip Text')
.addSuggest(['Continue', 'Cancel'])
.send(jid, { quoted: m })
// Animated progress indicator
await sock.aiRich().addProcess('Loading...').send(jid)
// Shorthand without the builder
await sock.sendMessage(jid, {
aiRich: { title: 'Assistant', text: 'Here is what I found:', table: [['Name', 'Score'], ['Alice', '90']] }
})
Bypass download is on by default for both
send()andsendMessage(jid, { aiRich }). The message edits itself right after sending so the client renders it without downloading first. PassbypassDownload: falseto turn it off. If the edit fails, the thrown error haserror.relayedKeyset to the key of the message that was already sent.
Builder method reference
| Method | Purpose |
|---|---|
setTitle(title) | Disclaimer label shown on the message |
setFooter(footer) | Trailing metadata text block |
setContextInfo(obj) | Merge extra fields into contextInfo |
addText(text, opts?) | Markdown text. Extracts [text](url) links, [](url) citations, [text|w|h](<url>) LaTeX |
addCode(language, code) | Syntax-highlighted code block |
addTable(rows, opts?) | [[header...], [row...], ...] array of strings |
addImage(image) | string | Buffer | array, shown as a grid |
addVideo(video) | string | Buffer | { url, mimeType?, duration? } | array |
addSource(sources) | [icon, url, text][] source cards |
addProduct(data) | Product card, or an array for a carousel |
addPost(data) | Social-post card, or an array for a carousel |
addReels(data) | Reel card, or an array |
addTip(text) | Small tip line |
addSuggest(suggestion, opts?) | Follow-up suggestion pill |
addProcess(title) | In-progress status indicator |
addHtml(...) | One inline HTML block, or [html, title] tab pairs |
build(opts?) | Build the raw content without sending |
buildEdit(jid, key, message) | Build the edit payload used by bypass download |
send(jid, opts?) | Send it. opts.bypassDownload (default true) sends the follow-up edit, other options go to sendMessage |
🛍️ Commerce & Payments
// Product message
await sock.sendMessage(jid, {
businessOwnerJid: '[email protected]',
image: { url: './mouse.png' },
product: {
title: 'Wireless Mouse',
description: 'Ergonomic, 2.4GHz',
currencyCode: 'USD',
priceAmount1000: 19990, // $19.99
retailerId: 'sku-001'
}
})
// Payments
await sock.sendMessage(jid, { requestPaymentFrom: recipientJid, text: 'Payment for order #123' })
await sock.sendMessage(jid, { paymentInviteServiceType: 1 })
await sock.sendMessage(jid, { orderText: 'Your order summary', thumbnail: fs.readFileSync('./order-thumb.jpg') })
await sock.sendMessage(jid, { document: { url: './invoice.pdf' }, mimetype: 'application/pdf', invoiceNote: 'Invoice #1024' })
Extras
// Sponsored ad card on any message
await sock.sendMessage(jid, {
text: 'Check this deal!',
externalAdReply: {
title: 'Big Sale',
body: '50% off',
thumbnail: fs.readFileSync('./thumb.jpg'),
mediaType: 1,
url: 'https://example.com'
}
})
// Phone number requests
await sock.sendMessage(jid, { requestPhoneNumber: true })
await sock.sendMessage(jid, { sharePhoneNumber: true })
// Group invite card
await sock.sendMessage(jid, {
groupInvite: { jid: groupJid, inviteCode: code, inviteExpiration: Date.now() + 3600_000, subject: 'My Group', text: 'Join us!' }
})
// Disable forwarding
await sock.sendMessage(jid, { limitSharing: true })
📞 Calls
Calls need ffmpeg and ffprobe in PATH. Create one Voip per socket after it opens. After a reconnect you get a new socket, so create a new Voip.
Start a call
import Voip from '@dnuzi/baileys/voip'
const voip = new Voip(sock, {
ffprobePath: 'ffprobe',
voipLogLevel: 'warn', // trace | debug | info | warn | error
tmpDir: './tmp', // temp folder for downloads
maxConcurrentCalls: 1
})
const call = await voip.call('948123456789', './song.mp3')
await call.hangup() // or call.end()
await voip.call('948123456789') // ring only, no media
Media, video & playlists
await voip.call(jid, './song.mp3') // local file
await voip.call(jid, 'https://example.com/song.mp3') // URL, downloaded first
await voip.call(jid, { audio: './voice.ogg' })
await voip.call(jid, './clip.mp4', '720p') // video call
await voip.call(
jid,
['./intro.mp3', './clip.mp4', 'https://example.com/outro.mp3'],
'480p',
{
loop: false, // repeat the playlist
autoEndCall: true, // hang up when the playlist ends
autoDowngrade: true // drop to audio when the peer turns every camera off
}
)
| Item | Supported |
|---|---|
| Audio formats | mp3 ogg opus wav m4a aac flac weba |
| Video formats | mp4 mov webm mkv avi m4v 3gp |
| Download limits | Audio 20 MB, video 50 MB |
| Resolution | '240p' '360p' '480p' '720p' '1080p' or { width, height, frameRate } (default 480p, portrait is rotated automatically) |
jid | A number or a JID, non-digits are stripped |
In-call controls
await call.silent(true) // silence audio (false = resume, no arg = toggle)
console.log(call.isSilenced)
call.mute(true) // mic only
call.raiseHand(true)
call.react('👍') // returns false if not connected yet
await call.shareScreen(true) // needs video, 1:1 only
await call.shareScreen(false)
const result = await call.upgradeToVideo()
// 'accepted' | 'rejected' | 'rejected_by_timeout' | 'error' | 'timeout' | 'cancelled'
shareScreenonly tells the peer a screen is being shared. The picture is whatever you feed tovoip.coordinator.feedLiveVideo().
Call events
call.on('ringing', () => {})
call.on('connected', () => {})
call.on('ended', reason => {}) // 'connection_closed' if the socket drops
call.on('error', err => {})
call.on('item', ({ index, kind, source }) => {})
call.on('playlist_looped', () => {})
call.on('playlist_ended', () => {})
call.on('downgraded', () => {}) // video dropped to audio
call.on('silent', state => {})
call.on('peer_mute', muted => {})
call.on('hand_raise', ({ jid, raised }) => {})
call.on('reaction', reaction => {})
call.on('screen_share', share => {})
call.on('peer_video', change => {})
call.on('inbound_audio', pcm => {}) // Float32Array, 16 kHz mono, 60 ms
call.on('inbound_video', frame => {}) // decoded H.264 frame
call.on('audio_finished', () => {}) // current audio item reached its end
Incoming calls
await voip.listen() // register call handlers, safe to call twice
voip.on('call_incoming', async call => {
console.log('incoming from', call.peerJid, call.callId)
await voip.acceptCall(call.callId)
})
voip.on('call_inbound_audio', ({ call, pcm }) => {})
voip.on('call_ended', call => console.log('ended', call.callId))
await voip.rejectCall(callId)
await voip.hangup(callId)
Available voip events: call_incoming, call_state, call_ended, call_peer_mute, call_inbound_audio, call_inbound_video, call_hand_raise, call_reaction, call_screen_share, call_peer_video_state, call_outbound_audio_finished, call_error.
voip.coordinator is the raw engine (feedLiveAudio, feedLiveVideo, setExternalAudioMode, ...).
Multi-call
const voip = new Voip(sock, { maxConcurrentCalls: 3 })
const a = await voip.call('948111111111', './song.mp3')
const b = await voip.call('948222222222', './song.mp3')
// Many numbers at once
const results = await voip.callMany(
['948111111111', '948222222222', '948333333333'],
'./song.mp3'
)
for (const { jid, call, error } of results) {
if (error) { console.log(jid, 'failed:', error.message); continue }
call.on('ended', reason => console.log(jid, 'ended:', reason))
call.on('error', err => console.error(jid, err))
}
voip.calls // active calls
voip.calls[0].callId
voip.calls[0].target // dialed number
await voip.hangup(callId) // end one call
await voip.end() // end all calls
await voip.end(true) // only release the slots
maxConcurrentCallsdefaults to1. Each call gets its own relay, codec and audio.- Incoming calls count toward the limit.
- Going over the limit throws
Maximum concurrent calls reached. IncallMany()it is returned aserrorfor that number and the rest still go through. callMany()accepts the same media and options ascall()and dials duplicate numbers once.- A safety timeout emits
errorif a call never ends (starts at 105 s).
🙏 Credits
This library is built on the original Baileys by @WhiskeySockets.
Thanks to the Baileys maintainers and contributors: purpshell · jlucaso1 · adiwajshing
Protocol Buffer definitions come from wa-proto by WPP Connect.
Additional code and modifications by DanuZz.
📄 License
📞 Contact
Built with ❤️ for the WhatsApp dev community.