Zaileys v4.0.0 is a complete rewrite — a clean break from the v3.x API. There is no backward-compatibility shim; the public surface was redesigned around typed events, a chainable builder, an auto-connect lifecycle, and pluggable storage. This guide maps every v3 concept to its v4 replacement with side-by-side snippets.
It also tracks the underlying upgrade to Baileys 7.0.0-rc13 (from 7.0.0-rc.9),
which patches the critical message-spoofing vulnerability CVE-2026-48063. See
SECURITY.md for details.
| Area | v3 | v4 |
|---|---|---|
| Connect | new Client({...}) + implicit connect |
new Client({...}) auto-connects on construction |
| Session option | session: 'zaileys' |
sessionId: 'zaileys' |
| Command prefix | prefix: '/' |
commandPrefix: '/' | ['/', '!'] |
| Pairing number | phoneNumber: 6280000000 (number) |
phoneNumber: '6280000000' (string) |
| Inbound events | single raw wa.on('messages', ctx => ...) |
typed per-event client.on('text' | 'image' | ...) |
| Message ctx | ctx.text, ctx.roomId, ctx.isFromMe |
msg.content, msg.jid, msg.fromMe |
| Sending | wa.send(jid, { text, image, ... }) object spread |
chainable client.send(jid).text(...).image(...) |
| Buttons | wa.button(jid, {...}) |
client.send(jid).buttons([...]) / .list({...}) |
| Reactions | wa.reaction(msg, '👍') |
client.react(key, '👍') |
| Edit / delete | wa.edit(msg, text) / wa.delete(msg) |
client.edit(key).text(...) / client.delete(key, {...}) |
| Storage | implicit file-based LMDB store | explicit pluggable AuthStore + MessageStore (file/sqlite/redis/pg/convex) |
| Plugins | definePlugins(...) + plugins/ dir + HMR |
command framework: client.command(name, handler) + client.use(mw) |
| Logs | showLogs, fancyLogs |
logger option (structural, pino-compatible) |
| FFmpeg | bundled, disableFFmpeg |
native media handling; no bundled FFmpeg flag |
v3 constructed the client and connected implicitly with a large options bag.
v3
import { Client } from 'zaileys'
const wa = new Client({
session: 'zaileys',
authType: 'qr',
prefix: '/',
showLogs: true,
fancyLogs: false,
})v4 — the client auto-connects on construction (no separate connect call). Register handlers synchronously and they fire as soon as the connection comes up.
import { Client } from 'zaileys'
const client = new Client({
sessionId: 'zaileys',
authType: 'qr',
commandPrefix: '/',
})Pairing-code auth now takes the phone number as a string:
// v3
const wa = new Client({ authType: 'pairing', phoneNumber: 6280000000 })
// v4
const client = new Client({ authType: 'pairing', phoneNumber: '6280000000' })If you need manual control over when the socket opens, disable auto-connect:
const client = new Client({ autoConnect: false })
await client.connect()v3 used a single raw messages event with a wide context object you had to inspect.
v3
wa.on('messages', async (ctx) => {
if (ctx.text === 'ping') {
await wa.send(ctx.roomId, 'Pong!')
}
if (ctx.isFromMe) return
})v4 — events are split by type, each with a typed payload. No if (ctx.chatType === ...)
branching.
client.on('text', async (msg) => {
if (msg.content === 'ping') {
await client.send(msg.jid).text('Pong!')
}
if (msg.fromMe) return
})
client.on('image', async (msg) => {
const { buffer } = await msg.download()
})
client.on('reaction', (msg) => console.log(msg.emoji))v3 combined connection state into logs and a single connection flow. v4 exposes
discrete typed events:
client.on('qr', ({ qrString }) => console.log(qrString))
client.on('pairing-code', ({ code }) => console.log(code))
client.on('connect', ({ me }) => console.log('online as', me.id))
client.on('reconnecting', ({ attempt, delayMs }) => console.log('retry', attempt, 'in', delayMs))
client.on('disconnect', ({ reason, willReconnect }) => console.log(reason, willReconnect))
client.on('error', ({ error }) => console.error(error))New inbound events surfaced in v4 (Baileys rc10–rc13 features): 'group-update',
'group-join', 'group-leave', 'member-tag', 'mention', 'mention-all',
'poll-vote', 'button-click', 'list-select', 'history-sync', 'limited' (463
reach-out timelock), 'presence', 'call-incoming', 'call-ended', 'newsletter'.
v3 sent messages by spreading an options object. v4 uses a chainable builder that returns
the sent WAMessageKey when awaited.
v3
await wa.send(ctx.roomId, { text: 'Hello' })
await wa.send(ctx.roomId, {
text: 'This is a reply!',
replied: ctx.message(),
})
await wa.send(ctx.roomId, {
image: 'https://example.com/image.jpg',
caption: 'Check this out!',
})v4
await client.send(jid).text('Hello')
await client.send(jid).text('This is a reply!').reply(quotedKey)
await client.send(jid).image('https://example.com/image.jpg', { caption: 'Check this out!' })Media, polls, location, contacts, and albums are all builder methods:
await client.send(jid).video('./clip.mp4', { caption: 'cool' })
await client.send(jid).audio('./voice.ogg', { ptt: true })
await client.send(jid).document('./file.pdf', { fileName: 'file.pdf' })
await client.send(jid).sticker('./art.webp')
await client.send(jid).poll('Favorite color?', ['Red', 'Blue', 'Green'])
await client.send(jid).album([
{ type: 'image', src: './a.jpg' },
{ type: 'video', src: './b.mp4' },
])v3
await wa.button(ctx.roomId, {
text: 'Choose an option:',
buttons: { type: 'simple', data: [{ id: 'btn1', text: 'Option 1' }] },
})v4
await client.send(jid).buttons([{ id: 'btn1', text: 'Option 1' }])
await client.send(jid).list({
title: 'Menu',
sections: [{ title: 'Section', rows: [{ id: 'r1', title: 'Row 1' }] }],
})v3 auto-detected @number strings in text. v4 is explicit via builder modifiers:
await client.send(jid).text('Hi @6281234567890').mentions(['6281234567890@s.whatsapp.net'])
await client.send(groupJid).text('Everyone!').mentionAll()v3
const sent = await wa.send(ctx.roomId, 'Original')
await wa.edit(sent, 'Edited')
await wa.delete(sent)
await wa.reaction(ctx.message(), '👍')
await wa.forward(ctx.roomId, { text: 'fwd', isForwardedMany: true })v4 — these are first-class Client methods keyed by WAMessageKey:
const key = await client.send(jid).text('Original')
await client.edit(key).text('Edited')
await client.delete(key, { forEveryone: true })
await client.react(key, '👍')
await client.forward(key, otherJid)v3 stored auth and chat history together implicitly (LMDB). v4 splits storage into two
independent, pluggable interfaces — AuthStore (credentials + signal data) and
MessageStore (message/chat history) — so you can, e.g., keep auth in Redis while
messages live in Postgres.
v3 — implicit, tied to session:
const wa = new Client({ session: 'zaileys' })v4 — file is the zero-config default; opt into other adapters explicitly:
import { Client, SqliteAuthStore, RedisMessageStore } from 'zaileys'
const client = new Client({
sessionId: 'zaileys',
auth: new SqliteAuthStore({ database: './auth.db' }),
store: new RedisMessageStore({ url: 'redis://localhost:6379' }),
})Available adapters: FileAuthStore (default), MemoryAuthStore, SqliteAuthStore,
RedisAuthStore, PostgresAuthStore, ConvexAuthStore; and MemoryMessageStore,
SqliteMessageStore, RedisMessageStore, PostgresMessageStore, ConvexMessageStore.
The sqlite/redis/postgres/convex adapters require their optional peer dependency
(better-sqlite3 / redis / pg / convex).
v3 auto-loaded files from a plugins/ directory via definePlugins with HMR. v4 replaces
this with an in-process command framework registered on the client.
v3 — plugins/hello.ts:
import { definePlugins } from 'zaileys'
export default definePlugins(
async (wa, ctx) => {
await wa.send(ctx.messages.roomId, 'Hello from plugin!')
},
{ matcher: ['/hello'], metadata: { description: 'hello plugin' } },
)v4 — register commands and middleware directly:
const client = new Client({ commandPrefix: ['/', '!'] })
client.use(async (ctx, next) => {
if (ctx.command === 'admin' && !isAdmin(ctx)) return
await next()
})
client.command('hello', async (ctx) => {
await ctx.reply('Hello from a command!')
})
client.command('echo', async (ctx) => {
await ctx.reply(ctx.args.join(' '))
})The command context (ctx) exposes ctx.command, parsed ctx.args, and helpers
ctx.reply(...), ctx.react(...), and ctx.edit(...).
The
pluginsDir,pluginsHmr, anddefinePluginsAPIs no longer exist. There is no file-based plugin loader in v4.
| v3 symbol / option | Status in v4 | Replacement |
|---|---|---|
session |
renamed | sessionId |
prefix |
renamed | commandPrefix (accepts string or string[]) |
phoneNumber: number |
retyped | phoneNumber: string |
showLogs, fancyLogs, showSpinner |
removed | logger (structural, pino-compatible) |
disableFFmpeg |
removed | native media handling; no bundled FFmpeg flag |
pluginsDir, pluginsHmr |
removed | command framework (command() / use()) |
definePlugins(...) |
removed | client.command(name, handler) |
wa.on('messages', ...) |
removed | typed events: client.on('text' | 'image' | ...) |
ctx.text |
renamed | msg.content |
ctx.roomId |
renamed | msg.jid |
ctx.isFromMe |
renamed | msg.fromMe |
ctx.senderName |
renamed | msg.sender.pushName |
ctx.message() |
replaced | msg.key (a WAMessageKey) |
wa.send(jid, { text, image, ... }) |
replaced | client.send(jid).text(...).image(...) builder |
wa.button(jid, {...}) |
replaced | client.send(jid).buttons([...]) / .list({...}) |
wa.reaction(msg, emoji) |
renamed | client.react(key, emoji) |
wa.edit(msg, text) |
replaced | client.edit(key).text(text) |
wa.delete(msg) |
replaced | client.delete(key, { forEveryone }) |
wa.forward(jid, {...}) |
replaced | client.forward(key, to) |
wa.inject / wa.getInjection |
removed | use client.use(mw) to attach context |
wa.plugins.* controls |
removed | no plugin registry; commands are static |
citation: { premium, banned } |
removed | implement authorization in client.use(...) |
| LMDB implicit store | replaced | pluggable AuthStore + MessageStore |
- Re-run
npm i zaileys@4(and the optional peer dep for your chosen storage adapter). - Delete v3
plugins/directories — they are no longer loaded. - Existing v3 session folders are not compatible; expect a fresh QR/pairing on first run.
- Verify your code against the new typed events — TypeScript will flag every renamed field.
Need help? Open an issue or check the examples/ directory for full v4 programs.