chat-adapter-zaileys plugs WhatsApp into Chat SDK through Zaileys. Auth (QR / pairing code), reconnection, session persistence, and message decoding are all handled by Zaileys — so the adapter stays thin, and your bot gets real message history, native buttons from Cards, decrypted poll votes, scheduling, and rich media that raw-Baileys adapters can't offer.
Note
This adapter wraps the full Zaileys client — everything in the Zaileys documentation (groups, communities, newsletters, privacy, broadcast, plugins) is reachable via adapter.client.
import { Chat } from 'chat'
import { createMemoryState } from '@chat-adapter/state-memory'
import { createZaileysAdapter } from 'chat-adapter-zaileys'
const whatsapp = createZaileysAdapter({
session: { sessionId: 'main' }, // QR prints to the terminal on first run
})
const bot = new Chat({
userName: 'mybot',
adapters: { whatsapp },
state: createMemoryState(),
})
bot.onNewMention(async (thread, message) => {
await thread.subscribe()
await thread.post(`Hello, ${message.author.fullName}!`)
})
await bot.initialize()
await whatsapp.connect() // register handlers first, then connectScan the printed QR via WhatsApp → Linked Devices, done. Prefer a pairing code?
const whatsapp = createZaileysAdapter({
session: { sessionId: 'main', authType: 'pairing', phoneNumber: '6281234567890' },
})| Capability | chat-adapter-zaileys | raw-Baileys adapters |
|---|---|---|
Message history (thread.fetchMessages) |
✅ real, backed by the Zaileys message store (memory/SQLite/Postgres/Redis/Convex) | ❌ empty arrays |
Cards & buttons (chat.onAction) |
✅ rendered as native WhatsApp buttons, clicks round-trip to onAction |
❌ fallback text only |
| Poll votes | ✅ decrypted natively — works across restarts, zero bookkeeping | messageSecret persistence |
Scheduled messages (thread.schedule) |
✅ native, persisted through the Zaileys scheduler | ❌ |
| Auth & reconnect | ✅ QR terminal / pairing code built in, auto-reconnect with backoff | onQR/reconnect yourself |
| Rich sends | ✅ image/video/audio/document/sticker (incl. animated Lottie), voice notes, locations, polls, albums | |
Media in queue/debounce strategies |
✅ rehydrateAttachment re-downloads by message key |
❌ |
| Raw escape hatch | message.raw.context = full Zaileys MessageContext (media helpers, reply/react, citation) |
plain WAMessage |
npm i chat-adapter-zaileys zaileys chat @chat-adapter/state-memory # or: pnpm add • yarn add • bun addRequires Node.js v20+. Peer dependencies are just chat and zaileys — no direct Baileys dependency.
For message history that survives restarts, give Zaileys a durable store (optional peer deps, install only what you use):
npm i better-sqlite3 # sqlite • redis (redis) • pg (postgres) • convex (convex)Full control — stores, plugins, citation, commands — then hand it to the adapter:
import { Client, SqliteMessageStore } from 'zaileys'
import { createZaileysAdapter } from 'chat-adapter-zaileys'
const client = new Client({
sessionId: 'main',
store: new SqliteMessageStore({ database: './wa.db' }), // durable fetchMessages history
})
const whatsapp = createZaileysAdapter({ client })bot.onNewMention(async (thread) => {
await thread.post(
<Card title="Deploy?">
<Actions>
<Button id="deploy" value="prod">Ship it</Button>
<Button id="cancel">Cancel</Button>
</Actions>
</Card>
)
})
bot.onAction('deploy', async (event) => {
await event.thread?.post(`Deploying ${event.value}…`)
})Every live message carries the full zaileys MessageContext — flags, lazy media, quoted decode, citation:
import { zaileysContext } from 'chat-adapter-zaileys'
bot.onSubscribedMessage(async (thread, message) => {
const ctx = zaileysContext(message)
if (!ctx) return
ctx.isGroup / ctx.isForwarded / ctx.isViewOnce / ctx.isEphemeral // 20+ flags
ctx.senderDevice // 'android' | 'ios' | 'web' | …
const media = ctx.media // lazy — nothing downloads until you ask
const quoted = await ctx.replied() // full decoded quoted message
await ctx.react('🔥') // zaileys shortcuts still work
})Narrow any Thread/Channel with requireZaileysAdapter and go beyond the Chat SDK surface:
import { requireZaileysAdapter } from 'chat-adapter-zaileys'
bot.onSubscribedMessage(async (thread, message) => {
const wa = requireZaileysAdapter(thread)
await wa.markRead(thread.id) // blue ticks
await wa.reply(message, 'Got it!') // native quoted reply
await wa.sendLocation({ threadId: thread.id, latitude: -6.2, longitude: 106.8 })
await wa.sendSticker(thread.id, stickerBuffer) // auto webp conversion, Lottie included
await wa.sendVoiceNote(thread.id, oggBuffer) // push-to-talk bubble
await wa.sendContact(thread.id, vcardString)
await wa.startRecording(thread.id) // "recording audio…" indicator
await wa.forwardMessage(thread.id, message.id, otherThreadId)
await wa.pinMessage(thread.id, message.id)
await wa.setPresence('available')
await wa.setDisappearing(thread.id, 86_400) // disappearing messages (0 disables)
const participants = await wa.fetchGroupParticipants(thread.id)
const admins = participants.filter((p) => p.isAdmin)
})const poll = await wa.sendPoll({ threadId: thread.id, question: 'Lunch?', options: ['A', 'B'] })
wa.onPollVote(poll.id, (vote) => {
console.log(vote.voter.userName, 'picked', vote.selectedOptions)
})Votes are decrypted natively by Zaileys — no messageSecret persistence, and it works across restarts for any poll this account sent.
One method unlocks the entire Zaileys message builder, pre-targeted at a thread:
await wa.native(thread.id).image(buffer, { viewOnce: true })
await wa.native(thread.id).album([{ type: 'image', src: img1 }, { type: 'image', src: img2 }])
await wa.native(thread.id).list({ title: 'Menu', buttonText: 'Open', sections })
await wa.native(thread.id).text('hey').mentionAll()const scheduled = await thread.schedule('Reminder!', { postAt: new Date(Date.now() + 3600_000) })
await scheduled.cancel() // if you change your mindScheduled jobs persist in the Zaileys store and survive restarts (with a durable store adapter).
| Option | Default | Description |
|---|---|---|
client / session |
— | Existing Zaileys Client, or ClientOptions to create one |
adapterName |
"zaileys" |
Thread-ID prefix; set unique per account for multi-account |
userName |
"zaileys-bot" |
Bot display name |
forwardPollVotes |
true |
Also deliver poll votes to processMessage as text |
autoMarkRead |
false |
Mark chats read on inbound messages |
richMessages |
false |
Render { markdown }/{ ast } posts as Meta-AI-style rich bubbles (zaileys AIRich) |
slashCommands |
false |
Route prefixed messages (/cmd args) to chat.onSlashCommand |
logger |
Chat SDK logger | Logger override |
- WhatsApp via Zaileys/Baileys is an unofficial API — protocol changes can break things, and accounts risk suspension under WhatsApp's ToS. Use responsibly.
fetchMessageshistory depth equals what your Zaileys store has seen. Use a durable store (SQLite/Postgres/Redis/Convex) for history that survives restarts.- No modals, no ephemeral messages — WhatsApp has no equivalent.
- 🌐 zeative.github.io/chat-adapter-zaileys — full adapter documentation: guides, events, payload, extensions
- ⚙️ zeative.github.io/zaileys — Zaileys documentation: the engine's guides, API reference, recipes
- 💬 chat-sdk.dev — Chat SDK documentation
- 📦 zaileys — the engine underneath this adapter
Hit a problem or have a feature request? Open an issue.
- Buy me a coffee ☕ • Ko-Fi • Trakteer
- ⭐ Star the repo on GitHub
Distributed under the MIT License. See LICENSE for details.