@lpzeravk/baileys
v1.0.0
Published
Fork customizado do Baileys (@lpzeravk/baileys)
Maintainers
Readme
🟡 @lpzeravk/baileys
v1.0.0 · Fork da LOPES ZERA sobre o Baileys, com suporte nativo a todos os tipos de mensagens do WhatsApp
Lançamento inicial da
@lpzeravk/baileys. Veja a seção de Changelog pra detalhes do que vem incluído desde já.
Instalação
npm i @lpzeravk/baileysDependências opcionais (só se for usar o recurso específico):
npm i sharp # sticker pack a partir de imagem que não é webp (jimp não codifica webp)
npm i fluent-ffmpeg # só para quem usa o MessageBuilder avançado (lib/loppssMB.cjs)Requer ffmpeg instalado no sistema (com suporte a libopus) para conversão automática de áudio PTT. Confirme com ffmpeg -encoders | grep opus.
Índice
- Conexão
- Mensagens de mídia
- Mensagens especiais
- Botões e interativos
- Sticker Pack nativo
- Canais Newsletter
- Enquetes com decrypt
- AI Rich
- LID / JID — Sistema avançado
- Username (@usuario)
- MessageBuilder — Button, ButtonV2, Carousel, AIRich
- Botões estendidos
- Grupos
- Perfil e privacidade
- Eventos do socket
- Utilitários
- Fixar / Desfixar mensagens
- Resolução de nomes (getName)
- PTT real com qualquer formato
- Guard de pagamento stealth
- Bad MAC Handler
- Resolução LID → telefone via grupo
- Versão do WhatsApp sempre atualizada
- WhatsApp Flows — Formulários
- Mensagens agendadas
- Status em grupo (GroupStatusMessageV2)
- Figurinha de Avatar (isAvatar)
- Álbum (múltiplas mídias agrupadas)
- Chamadas de voz (calls)
1. Conexão
Básico
const {
default: makeWASocket,
useMultiFileAuthState,
DisconnectReason,
Browsers,
getBestWaVersion
} = require('@lpzeravk/baileys')
const { state, saveCreds } = await useMultiFileAuthState('./session')
const { version, isLatest, source } = await getBestWaVersion()
const sock = makeWASocket({
version,
auth: state,
browser: Browsers.ubuntu('Chrome'),
printQRInTerminal: false,
logger: require('pino')({ level: 'silent' }),
getMessage: async (key) => {
const msg = await store.loadMessage(key.remoteJid, key.id)
return msg?.message || undefined
}
})
sock.ev.on('creds.update', saveCreds)Use
getBestWaVersion()em vez defetchLatestWaWebVersion()— ela tenta as duas fontes e só aceita resultado se buscou fresco de verdade, sem cair silenciosamente num fallback desatualizado que pode causar erros 403.
Opções recomendadas para produção
const sock = makeWASocket({
version,
auth: state,
browser: Browsers.ubuntu('Chrome'),
printQRInTerminal: false,
logger: require('pino')({ level: 'silent' }),
connectTimeoutMs: 60000,
defaultQueryTimeoutMs: 60000,
keepAliveIntervalMs: 25000, // ping periódico pra manter o socket vivo
syncFullHistory: false, // não baixa histórico inteiro (mais leve e rápido)
shouldSyncHistoryMessage: () => false,
generateHighQualityLinkPreview: false,
emitOwnEvents: false, // não reprocessa mensagens do próprio bot
getMessage: async (key) => {
// IMPORTANTE: retorne msg.message (WAMessage), não o WebMessageInfo inteiro —
// retornar o objeto completo causa falha silenciosa no processamento de eventos
const msg = await store.loadMessage(key.remoteJid, key.id)
return msg?.message || undefined
}
})Reconexão robusta (com anti-ban do 405)
O tratamento correto de cada motivo de desconexão é o que evita loops de reconexão e, principalmente, ban por rate limit (405). Reconectar rápido demais ao receber 405 é o principal precursor de ban.
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect } = update
if (connection === 'open') {
console.log('Conectado ✔')
return
}
if (connection === 'close') {
const code = lastDisconnect?.error?.output?.statusCode
if (code === DisconnectReason.loggedOut) {
// sessão encerrada de vez — apague a pasta session e re-pareie
console.log('Deslogado. Apague a sessão e re-pareie.')
process.exit()
} else if (code === DisconnectReason.forbidden || code === 403) {
// conta revogada pelo WhatsApp
console.log('Sessão revogada (403). Apague a sessão e re-pareie.')
process.exit()
} else if (code === DisconnectReason.connectionReplaced) {
// outra sessão abriu no mesmo número — encerre pra não entrar em loop
console.log('Conflito: outra sessão ativa no mesmo número.')
process.exit()
} else if (code === 405) {
// ANTI-BAN: 405 = rate limit. Reconectar rápido leva a BAN.
// Espere bastante (60s) antes de tentar de novo.
console.log('[ANTI-BAN] 405 (rate limit). Aguardando 60s...')
await new Promise(r => setTimeout(r, 60000))
startBot()
} else if (code === DisconnectReason.restartRequired) {
// 515 é normal logo após o pareamento — reconecta imediato
startBot()
} else {
// qualquer outro motivo (conexão perdida, timeout) — espera 5s
console.log(`Reconectando (motivo ${code})...`)
await new Promise(r => setTimeout(r, 5000))
startBot()
}
}
})Watchdog — detecta socket "zumbi"
Às vezes o socket para de receber mensagens mas o connection.update não dispara close (fica num estado morto silencioso). Um watchdog checa o readyState do WebSocket e reinicia se necessário:
let lastMsgTime = Date.now()
sock.ev.on('messages.upsert', () => { lastMsgTime = Date.now() })
const watchdog = setInterval(async () => {
const inativoSeg = Math.floor((Date.now() - lastMsgTime) / 1000)
if (inativoSeg < 600) return // menos de 10 min sem msg — ok
// 1 = OPEN. Qualquer outro estado = socket morto
if (sock.ws?.readyState !== 1) {
console.log('[WATCHDOG] Socket morto. Reiniciando...')
clearInterval(watchdog)
startBot()
return
}
// socket diz que está aberto — confirma com um ping real
try {
await sock.sendPresenceUpdate('available')
lastMsgTime = Date.now()
} catch {
console.log('[WATCHDOG] Ping falhou. Reiniciando...')
clearInterval(watchdog)
startBot()
}
}, 120000) // checa a cada 2 minPairing Code
const code = await sock.requestPairingCode('5511999999999')
const code = await sock.requestPairingCode('5511999999999', 'MYBOT001') // personalizado — 8 charsPeça o pairing code alguns segundos após criar o socket (ex: dentro de um
setTimeoutde 3s), e só quando!sock.authState.creds.registered.
Store em memória
const store = {
messages: {},
bind: (ev) => {
ev.on('messages.upsert', ({ messages }) => {
for (const msg of messages) {
if (!msg.key?.remoteJid) continue
store.messages[msg.key.remoteJid] ??= {}
store.messages[msg.key.remoteJid][msg.key.id] = msg
}
})
},
loadMessage: async (jid, id) => store.messages[jid]?.[id] || null
}
store.bind(sock.ev)2. Mensagens de mídia
Todos os campos de mídia aceitam Buffer, { url: 'https://...' } ou { stream }.
await sock.sendMessage(jid, { text: 'Olá!' })
await sock.sendMessage(jid, { text: '@5511...!', mentions: ['[email protected]'] })
await sock.sendMessage(jid, { image: { url: 'https://...' }, caption: 'Legenda' })
await sock.sendMessage(jid, { video: buffer, caption: 'Legenda', gifPlayback: false })
await sock.sendMessage(jid, { audio: buffer, mimetype: 'audio/mpeg', ptt: false })
await sock.sendMessage(jid, { audio: buffer, ptt: true }) // PTT — converte automaticamente
await sock.sendMessage(jid, { document: buffer, mimetype: 'application/pdf', fileName: 'doc.pdf' })
await sock.sendMessage(jid, { sticker: buffer })
await sock.sendMessage(jid, {
album: [{ image: buffer1 }, { image: buffer2 }, { video: bufferVideo }]
}) // veja a seção 28 para o guia completo de álbum
await sock.sendMessage(jid, {
contacts: {
displayName: 'mario',
contacts: [{ vcard: 'BEGIN:VCARD\nVERSION:3.0\nFN:João\nTEL:+5511999999999\nEND:VCARD' }]
}
})
await sock.sendMessage(jid, {
location: { degreesLatitude: -23.55, degreesLongitude: -46.63, name: 'São Paulo' }
})3. Mensagens especiais
await sock.sendMessage(jid, { react: { text: '❤️', key: message.key } })
await sock.sendMessage(jid, { delete: message.key })
await sock.sendMessage(jid, { edit: message.key, text: 'Texto editado' })
await sock.sendMessage(jid, { image: buffer, viewOnce: true })
await sock.sendMessage(jid, { image: buffer, viewOnceV2: true })
await sock.sendMessage(jid, { image: buffer, caption: '!', spoiler: true })
await sock.sendMessage(jid, { image: buffer, groupStatus: true })
await sock.sendMessage(jid, { image: buffer, ephemeral: true })
await sock.sendMessage(jid, { sticker: buffer, isLottie: true })
await sock.sendMessage(jid, { text: '@all', mentionAll: true })
await sock.sendMessage(jid, {
text: 'Confira!',
externalAdReply: {
title: 'Título', body: 'Descrição',
thumbnail: bufferImg, mediaType: 1,
sourceUrl: 'https://seusite.com.br',
renderLargerThumbnail: true
}
})
await sock.sendMessage(jid, {
poll: { name: 'Pergunta?', values: ['A', 'B', 'C'], selectableCount: 1 }
})
const { generateForwardMessageContent, generateWAMessageFromContent } = require('@lpzeravk/baileys')
const fwd = generateForwardMessageContent(message, false)
const msg = generateWAMessageFromContent(jid, fwd, { quoted: m })
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })4. Botões e interativos
const { generateWAMessageFromContent, proto } = require('@lpzeravk/baileys')
const msg = generateWAMessageFromContent(jid, {
viewOnceMessage: {
message: {
interactiveMessage: proto.Message.InteractiveMessage.create({
header: { title: 'Título', hasMediaAttachment: false },
body: { text: 'Texto' },
footer: { text: 'Rodapé' },
nativeFlowMessage: { buttons: [ /* tipos abaixo */ ] }
})
}
}
}, { quoted: m })
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id }){ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'OK', id: 'ok' }) }
{ name: 'cta_url', buttonParamsJson: JSON.stringify({ display_text: 'Abrir', url: 'https://seusite.com.br' }) }
{ name: 'cta_copy', buttonParamsJson: JSON.stringify({ display_text: 'Copiar', copy_code: 'CODIGO' }) }
{ name: 'cta_call', buttonParamsJson: JSON.stringify({ display_text: 'Ligar', phone_number: '5511999999999' }) }
{
name: 'single_select',
buttonParamsJson: JSON.stringify({
title: 'Escolher',
sections: [{ title: 'Cat', rows: [{ header: 'Op1', title: 'Opção 1', description: 'Desc', id: 'op1' }] }]
})
}Header com imagem + nativeFlow
// Use `caption`, não `text` — `text` faz a lib pular o bloco do header
await sock.sendMessage(jid, {
image: { url: 'https://...' },
caption: 'Texto do card',
nativeFlow: [{ text: 'Botão', id: 'btn1' }]
})Carousel
await sock.sendMessage(jid, {
cards: [
{ image: buffer1, caption: 'Card 1', nativeFlow: [{ text: 'Ver', id: 'c1' }] },
{ image: buffer2, caption: 'Card 2', nativeFlow: [{ text: 'Ver', id: 'c2' }] },
],
footer: 'Rodapé'
})Template Buttons / Sections
await sock.sendMessage(jid, {
text: 'Escolha:',
templateButtons: [
{ text: 'Resposta', id: 'r1' },
{ text: 'Site', url: 'https://seusite.com.br' },
{ text: 'Ligar', call: '5511999999999' },
]
})
await sock.sendMessage(jid, {
text: 'Selecione', buttonText: 'Ver opções',
sections: [{ title: 'Cat', rows: [{ title: 'Item 1', description: 'Desc', id: 'i1' }] }]
})5. Sticker Pack nativo
await sock.sendMessage(jid, {
cover: bufferWebP,
stickers: [
{ data: bufferWebP, emojis: ['😂'] },
{ data: bufferAnima, emojis: ['🔥'] },
{ data: bufferPng, emojis: ['✨'] },
],
name: 'Nome do Pack',
publisher: 'Autor',
})cover e cada stickers[].data esperam Buffer. Máximo 60 figurinhas, até 1MB cada. Requer sharp ou @napi-rs/image para PNG.
6. Canais Newsletter
await sock.sendMessage('120363...@newsletter', { text: 'Novidade!' })
const { generateWAMessage, generateMessageIDV2 } = require('@lpzeravk/baileys')
const fullMsg = await generateWAMessage(canalJid, { image: buffer, caption: 'Legenda' }, {
upload: sock.waUploadToServer, userJid: sock.user.id,
messageId: generateMessageIDV2(sock.user.id),
})
await sock.relayMessage(canalJid, fullMsg.message, { messageId: fullMsg.key.id })
const canal = await sock.newsletterCreate('Nome', 'Descrição')
await sock.newsletterFollow(jid)
await sock.newsletterUnfollow(jid)
await sock.newsletterMute(jid)
await sock.newsletterUnmute(jid)
const meta = await sock.newsletterMetadata('jid', jid)
await sock.newsletterUpdateName(jid, 'Novo Nome')
await sock.newsletterUpdateDescription(jid, 'Nova descrição')
await sock.newsletterUpdatePicture(jid, buffer)
await sock.newsletterRemovePicture(jid)
await sock.newsletterReactMessage(jid, serverId, '❤️')
const msgs = await sock.newsletterFetchMessages(jid, 30, 0, 0)
await sock.newsletterDelete(jid)
const { subscribers } = await sock.newsletterSubscribers(jid)Reencaminhar qualquer mídia marcada para um canal
Baixa a mídia de uma mensagem citada (texto, imagem, vídeo, áudio, sticker ou documento) e reenvia para o canal. Use relayMessage — é o único método que passa pelo encodeNewsletterMessage com os extraAttrs corretos (o mediatype no plaintext); sendMessage direto não faz isso para mídia em canal.
const {
downloadContentFromMessage,
generateWAMessage,
generateMessageIDV2,
} = require('@lpzeravk/baileys')
const canalJid = '120363...@newsletter'
const q = quotedMsg // a mensagem citada
const raw = q.msg || {}
const type = q.mtype || ''
const mime = raw.mimetype || ''
const msgText = q.text || raw.caption || ''
// ── TEXTO ────────────────────────────────────────────────────────────────
if (type === 'conversation' || type === 'extendedTextMessage' || (!mime && !raw.mediaKey && msgText)) {
await sock.sendMessage(canalJid, { text: msgText || '.' })
return
}
// ── mapeia o tipo de mídia ───────────────────────────────────────────────
const MEDIA_TYPE_MAP = {
stickerMessage: 'sticker',
imageMessage: 'image',
videoMessage: 'video',
audioMessage: 'audio',
documentMessage: 'document',
}
const mediaType = MEDIA_TYPE_MAP[type] || (
mime.startsWith('image/') ? 'image' :
mime.startsWith('video/') ? 'video' :
mime.startsWith('audio/') ? 'audio' : 'document'
)
// ── baixa a mídia ────────────────────────────────────────────────────────
const stream = await downloadContentFromMessage(
{ mediaKey: raw.mediaKey, directPath: raw.directPath, url: raw.url },
mediaType
)
const chunks = []
for await (const chunk of stream) chunks.push(chunk)
const buffer = Buffer.concat(chunks)
// ── monta o content conforme o tipo ──────────────────────────────────────
let content
if (mediaType === 'sticker') content = { sticker: buffer }
else if (mediaType === 'image') content = { image: buffer, caption: msgText }
else if (mediaType === 'video') content = { video: buffer, caption: msgText, gifPlayback: raw.gifPlayback || false }
else if (mediaType === 'audio') content = { audio: buffer, mimetype: mime || 'audio/mp4', ptt: raw.ptt || false }
else content = { document: buffer, mimetype: mime || 'application/octet-stream', fileName: raw.fileName || 'arquivo', caption: msgText }
// ── gera a mensagem (faz upload) e envia via relayMessage ────────────────
const fullMsg = await generateWAMessage(canalJid, content, {
upload: sock.waUploadToServer,
userJid: sock.user.id,
messageId: generateMessageIDV2(sock.user.id),
})
await sock.relayMessage(canalJid, fullMsg.message, { messageId: fullMsg.key.id })7. Enquetes com decrypt
await sock.sendMessage(jid, {
poll: { name: 'Pergunta?', values: ['A', 'B', 'C'], selectableCount: 1 }
})
const { getAggregateVotesInPollMessage } = require('@lpzeravk/baileys/lib/Utils/messages.js')
sock.ev.on('messages.update', async (updates) => {
for (const { key, update } of updates) {
if (!update.pollUpdates) continue
const pollMsg = await store.loadMessage(key.remoteJid, key.id)
if (!pollMsg?.message) continue
const result = getAggregateVotesInPollMessage({
message: pollMsg.message, pollUpdates: update.pollUpdates
})
// [{ name: 'A', voters: ['[email protected]'] }]
}
})8. AI Rich
await sock.sendRich(jid, [
sock.makeText('Acesse [nosso site](https://seusite.com.br).'),
sock.makeCode('bash', 'npm i @lpzeravk/baileys'),
sock.makeTable([['Nome', 'Status'], ['Botões', '✅']]),
sock.makeList(['Item 1', 'Item 2']),
], quotedMsg, ['RICH_RESPONSE_CODE', 'RICH_RESPONSE_TABLE'])
await sock.sendRichText(jid, 'Texto com [link](https://seusite.com.br)', quotedMsg)
await sock.sendRichCode(jid, 'Título', 'javascript', 'const x = 1', quotedMsg)
await sock.sendRichList(jid, 'Lista', ['A', 'B'], quotedMsg)9. LID / JID — Sistema avançado
O WhatsApp usa dois tipos de identificador:
- JID (
@s.whatsapp.net) — baseado em número de telefone - LID (
@lid) — identificador opaco sem número
A lib resolve automaticamente LID↔JID via sharedLidPhoneCache, inclusive ao buscar metadata de grupo (groupMetadata).
const {
lidToJid, resolveJid, resolveAll, normalizeJid, validateJid,
getSenderInfo, sharedLidPhoneCache, isLidUser, isPnUser,
getBotJid, setBotMap
} = require('@lpzeravk/baileys')
const jid = lidToJid('123456@lid')
const { jid, lid } = resolveAll('[email protected]')
const { jid, lid, isGroup } = getSenderInfo(message)
normalizeJid('5511999999999') // → '[email protected]'
normalizeJid('123456@lid') // → resolve via cache
sharedLidPhoneCache.set('123456@lid', '[email protected]')
const jid = sharedLidPhoneCache.getPhoneForLid('123456@lid')
const lid = sharedLidPhoneCache.getLidForPhone('[email protected]')
console.log('Cache size:', sharedLidPhoneCache.size)⚠️ Identidades do próprio bot (
sock.user.id/.lid) vêm com sufixo de dispositivo (numero:N@dominio). Pra comparar comparticipants, remova só o:Npreservando o domínio:jid.replace(/:\d+(?=@)/, '')— não usesplit(':')[0].
10. Username (@usuario)
const { resolveUsername, isUsername } = require('@lpzeravk/baileys')
isUsername('@josue') // true
const jid = await resolveUsername('@josue', sock.onWhatsApp.bind(sock))
if (jid) await sock.sendMessage(jid, { text: 'Olá!' })11. MessageBuilder — Button, ButtonV2, Carousel, AIRich
const { Button, ButtonV2, Carousel, AIRich } = require('@lpzeravk/baileys/lib/loppssMB.cjs')
// O socket vai sempre no construtor: new ButtonV2(sock)ButtonV2
const msg = new ButtonV2(sock)
msg.setTitle('Título'); msg.setBody('Corpo'); msg.setFooter('Rodapé')
msg.setThumbnail('https://exemplo.com/imagem.jpg')
msg.addButton('✅ Opção 1', 'opcao_1')
msg.addButton('❌ Opção 2', 'opcao_2')
await msg.send(jid, { quoted: m })
// clique chega como m.body === 'opcao_1'Button (native flow)
const msg = new Button(sock)
msg.addReply('✅ Confirmar', 'confirmar')
msg.addUrl('🔗 Abrir', 'https://seusite.com.br')
msg.addCopy('📋 Copiar', 'PROMO2025')
msg.addCall('📞 Ligar', '5511999999999')
await msg.send(jid, { quoted: m })Carousel
const msg = new Carousel(sock)
msg.setBody('Escolha:')
msg.card(c => c.image('https://...').title('Card 1').text('Desc').button('Ver', 'c1'))
msg.card(c => c.image('https://...').title('Card 2').text('Desc').button('Ver', 'c2'))
await msg.send(jid, { quoted: m })AIRich
const msg = new AIRich(sock)
msg.addText('Acesse [LOPES ZERA](https://seusite.com.br).')
msg.addCode('javascript', `require('@lpzeravk/baileys')`)
msg.addTable([['Comando', 'Desc'], ['!menu', 'Menu principal']])
await msg.send(jid, { quoted: m })12. Botões estendidos
await sock.sendMessage(jid, { text: 'Mensagem', nativeFlow: [ /* botões */ ] })
{ text: '⏰ Me lembre', reminder: 'lembrete_id' }
{ text: '🔕 Cancelar', cancelReminder: 'lembrete_id' }
{ text: '📍 Endereço', address: true }
{ text: '📡 Localização', location: true }
{ text: '🛍️ Catálogo', catalog: '[email protected]' }
{ text: 'Copiar código', otp: '123456' }
{ text: '📞 Ligar', phoneNumber: '5511999999999' }
{ text: '🗑️ Limpar chat', clearChat: true }
{ text: '🔗 Abrir', urlBtn: 'https://seusite.com.br' }isSystemNotification
sock.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0]
if (msg.isSystemNotification) return
})13. Grupos
const meta = await sock.groupMetadata(jid)
const grupo = await sock.groupCreate('Nome', ['[email protected]'])
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'add')
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'remove')
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'promote')
await sock.groupParticipantsUpdate(jid, ['[email protected]'], 'demote')
await sock.groupLeave(jid)
await sock.groupUpdateSubject(jid, 'Novo Nome')
await sock.groupUpdateDescription(jid, 'Nova descrição')
const code = await sock.groupInviteCode(jid)
await sock.groupRevokeInvite(jid)
await sock.groupAcceptInvite('CODIGO')
await sock.groupToggleEphemeral(jid, 86400)
const grupos = await sock.groupFetchAllParticipating()14. Perfil e privacidade
const url = await sock.profilePictureUrl(jid, 'image')
await sock.updateProfilePicture(jid, buffer)
await sock.updateProfileStatus('🤖 Bot ativo')
const [result] = await sock.onWhatsApp('5511999999999')
await sock.updateBlockStatus(jid, 'block')
await sock.updateBlockStatus(jid, 'unblock')
await sock.sendPresenceUpdate('composing', jid)
await sock.sendPresenceUpdate('available', jid)
await sock.presenceSubscribe(jid)
await sock.readMessages([message.key])
await sock.chatModify({ archive: true }, jid)
await sock.chatModify({ pin: true }, jid)
await sock.chatModify({ mute: 8 * 60 * 60 * 1000 }, jid)
await sock.logout()15. Eventos do socket
sock.ev.on('messages.upsert', ({ messages, type }) => {})
sock.ev.on('messages.update', (updates) => {})
sock.ev.on('messages.delete', (item) => {})
sock.ev.on('messages.reaction', (reactions) => {})
sock.ev.on('messages.decrypt-failed', (failInfo) => {}) // ver seção 20
sock.ev.on('presence.update', ({ id, presences }) => {})
sock.ev.on('groups.update', (updates) => {})
sock.ev.on('group-participants.update', ({ id, participants, action }) => {})
sock.ev.on('contacts.update', (contacts) => {})
sock.ev.on('connection.update', ({ connection, lastDisconnect, qr }) => {})
sock.ev.on('creds.update', saveCreds)
sock.ev.on('call', (calls) => {
for (const call of calls) {
if (call.status === 'offer') sock.rejectCall(call.id, call.from)
}
})16. Utilitários
const { downloadMediaMessage } = require('@lpzeravk/baileys')
const buffer = await downloadMediaMessage(message, 'buffer', {})
const { generateWAMessage, generateWAMessageFromContent, generateMessageIDV2 } = require('@lpzeravk/baileys')
const { jidNormalizedUser, isJidGroup, isJidNewsletter, isLidUser, isPnUser } = require('@lpzeravk/baileys')
// Detectar dispositivo — analisa o ID DA MENSAGEM, não o JID
const { getDevice } = require('@lpzeravk/baileys')
const device = getDevice(message.key.id) // 'android' | 'ios' | 'web' | 'unknown'17. Fixar / Desfixar mensagens
await sock.sendMessage(jid, { pin: quotedMsg.key, type: 1 }) // PIN_FOR_ALL
await sock.sendMessage(jid, { pin: quotedMsg.key, type: 2 }) // UNPIN_FOR_ALL18. Resolução de nomes (getName)
const { getName } = require('@lpzeravk/baileys')
const nome = getName(msg, contactStore)
// Prioridade: contato salvo → verifiedName → pushName → @lid → telefone → "Usuário desconhecido"19. PTT real com qualquer formato
// Qualquer formato de entrada — a lib converte via ffmpeg automaticamente
await sock.sendMessage(jid, { audio: { url: 'https://musica.mp3' }, ptt: true })A lib detecta a extensão real da fonte (não o mimetype declarado) e converte pra mono/16kHz opus quando necessário. Requer ffmpeg com libopus.
20. Guard de pagamento stealth (anti-fraude)
const { bindPaymentGuard } = require('@lpzeravk/baileys')
bindPaymentGuard(sock, {
isPaymentMessage: (webMessage) => { /* sua lógica */ },
recordEnvelope: (webMessage, isPayment) => { /* seu registro */ },
treatDecryptFailureAsSuspicious: true,
onDetect: (detection) => {
// detection.type: 'direct' | 'edited' | 'undecryptable'
}
})| Caminho | Evento | Garantia |
|---|---|---|
| Mensagem direta | messages.upsert | Roda detector no conteúdo |
| Mensagem editada | messages.update | Extrai editedMessage e roda detector |
| Falha de decrypt | messages.decrypt-failed | Nunca ignora silenciosamente |
21. Bad MAC Handler — sessões Signal
const { badMacHandler } = require('@lpzeravk/baileys')
if (badMacHandler.isBadMacError(error)) {
badMacHandler.handleError(error, 'contexto')
}
badMacHandler.clearProblematicSessionFiles() // preserva creds.json22. Resolução LID → telefone via grupo
const { resolveLidPhoneFromGroup } = require('@lpzeravk/baileys')
const telefone = await resolveLidPhoneFromGroup(sock, groupJid, lid)Força resolução de @lid → telefone via groupMetadata — útil quando o cache ainda não tem aquele par.
23. Versão do WhatsApp sempre atualizada
const { getBestWaVersion } = require('@lpzeravk/baileys')
const { version, isLatest, source } = await getBestWaVersion()
// tenta web.whatsapp.com → GitHub → fallback fixo (avisando explicitamente)24. WhatsApp Flows — Formulários interativos
Requer
flow_idaprovado no Meta Business Manager.
const { generateWAMessageFromContent } = require('@lpzeravk/baileys')
const formMsg = generateWAMessageFromContent(jid, {
viewOnceMessage: {
message: {
messageContextInfo: { deviceListMetadata: {}, deviceListMetadataVersion: 2 },
interactiveMessage: {
body: { text: 'Preencha seus dados.' },
nativeFlowMessage: {
buttons: [{
name: 'galaxy_message',
buttonParamsJson: JSON.stringify({
flow_message_version: '4',
flow_id: 'SEU_FLOW_ID',
flow_action_payload: {
screen: 'contact_details',
data: { full_name_visible: true, email_visible: true }
},
flow_cta: '__localize:FLOWS_SIGN_UP_BUTTON_TITLE',
flow_action: 'navigate',
flow_token: 'T0ZGRVJfU0lHTlVQ'
})
}],
messageParamsJson: '{}'
}
}
}
}
}, { userJid: sock.user.id })
await sock.relayMessage(jid, formMsg.message, { messageId: formMsg.key.id })Recebendo a resposta
sock.ev.on('messages.upsert', async ({ messages }) => {
const m = messages[0]
const nfr = m.message?.interactiveResponseMessage?.nativeFlowResponseMessage
if (nfr?.name !== 'galaxy_message' || !nfr?.paramsJson) return
const parsed = JSON.parse(nfr.paramsJson)
if (!parsed.wa_flow_response_params) return
const flowId = parsed.wa_flow_response_params.flow_id
const screens = JSON.parse(parsed.wa_flow_response_params.response_message).screens || []
const campos = {}
for (const screen of screens) {
for (const comp of (screen.components || [])) {
if (comp.name && comp.value !== undefined && comp.value !== '')
campos[comp.name] = comp.value
}
}
if (flowId === 'SEU_FLOW_ID') {
let resposta = 'Formulário recebido!\n\n'
if (campos.full_name) resposta += `Nome: ${campos.full_name}\n`
if (campos.email) resposta += `Email: ${campos.email}\n`
await sock.sendMessage(m.key.remoteJid, { text: resposta }, { quoted: m })
}
})25. Mensagens agendadas
Sistema de agendamento em memória. Auto-inicia o timer quando há mensagens na fila, auto-para quando a fila esvazia.
const { createMessageScheduler } = require('@lpzeravk/baileys')
const scheduler = createMessageScheduler(
(jid, content) => sock.sendMessage(jid, content),
{
onSent: (s, msg) => console.log(`Enviado pra ${s.jid} — ID: ${s.id}`),
onFailed: (s, err) => console.error(`Falhou: ${err.message}`)
}
)
// Agendar para data/hora específica
const entry = scheduler.schedule(
'[email protected]',
{ text: 'Feliz Aniversário! 🎂' },
new Date('2026-12-25T09:00:00')
)
console.log('Agendado com ID:', entry.id)
// Agendar com delay (30 minutos)
scheduler.scheduleDelay(jid, { text: 'Lembrete!' }, 30 * 60 * 1000)
// Agendar qualquer tipo de mensagem
scheduler.schedule(groupJid, {
image: { url: './promo.jpg' },
caption: 'Promoção de fim de semana!'
}, new Date('2026-12-20T08:00:00'))
// Cancelar um agendamento
scheduler.cancel(entry.id)
// Cancelar todos de um JID
scheduler.cancelForJid(jid)
// Ver pendentes
const pendentes = scheduler.getPending()
// Parar/retomar
scheduler.stop()
scheduler.start()
// Limpar tudo
scheduler.clearAll()Objeto retornado:
{
id: 'sched_1782000000000_a3f2b1',
jid: '[email protected]',
content: { text: '...' },
scheduledTime: Date,
createdAt: Date,
status: 'pending' | 'sent' | 'failed' | 'cancelled',
messageId: '...', // preenchido após envio
error: '...' // preenchido em caso de falha
}26. Status em grupo (GroupStatusMessageV2)
Posta conteúdo como status dentro de um grupo — texto, imagem, vídeo ou áudio. Suporta modo Close Friends (apenas amigos próximos), usando o mesmo mecanismo do status nativo do WhatsApp.
const { generateWAMessageFromContent, prepareWAMessageMedia, downloadMediaMessage } = require('@lpzeravk/baileys')
const crypto = require('crypto')
// ── Texto para todos do grupo ────────────────────────────────────────────────
const messageSecret = crypto.randomBytes(32)
const msg = generateWAMessageFromContent(jid, {
messageContextInfo: { messageSecret },
groupStatusMessageV2: {
message: {
extendedTextMessage: {
text: 'Olá, grupo! 👋',
contextInfo: { isGroupStatus: true }
},
messageContextInfo: { messageSecret }
}
}
}, {})
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })// ── Texto apenas para Amigos Próximos (Close Friends) ────────────────────────
const messageSecret = crypto.randomBytes(32)
const msg = generateWAMessageFromContent(jid, {
messageContextInfo: { messageSecret },
groupStatusMessageV2: {
message: {
extendedTextMessage: {
text: 'Só pra você que é chegado 🤫',
contextInfo: {
isGroupStatus: true,
statusAudienceMetadata: { audienceType: 1 } // 1 = Close Friends
}
},
messageContextInfo: { messageSecret }
}
}
}, {})
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })// ── Imagem para todos do grupo ───────────────────────────────────────────────
const messageSecret = crypto.randomBytes(32)
const prep = await prepareWAMessageMedia(
{ image: buffer },
{ upload: sock.waUploadToServer }
)
const msg = generateWAMessageFromContent(jid, {
messageContextInfo: { messageSecret },
groupStatusMessageV2: {
message: {
imageMessage: {
...prep.imageMessage,
caption: 'Legenda da imagem',
contextInfo: { isGroupStatus: true }
},
messageContextInfo: { messageSecret }
}
}
}, {})
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })// ── Imagem apenas para Amigos Próximos ───────────────────────────────────────
const messageSecret = crypto.randomBytes(32)
const prep = await prepareWAMessageMedia(
{ image: buffer },
{ upload: sock.waUploadToServer }
)
const msg = generateWAMessageFromContent(jid, {
messageContextInfo: { messageSecret },
groupStatusMessageV2: {
message: {
imageMessage: {
...prep.imageMessage,
caption: 'Só pra chegados 🤫',
contextInfo: {
isGroupStatus: true,
statusAudienceMetadata: { audienceType: 1 }
}
},
messageContextInfo: { messageSecret }
}
}
}, {})
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })// ── Vídeo ─────────────────────────────────────────────────────────────────────
const messageSecret = crypto.randomBytes(32)
const prep = await prepareWAMessageMedia(
{ video: buffer },
{ upload: sock.waUploadToServer }
)
const msg = generateWAMessageFromContent(jid, {
messageContextInfo: { messageSecret },
groupStatusMessageV2: {
message: {
videoMessage: {
...prep.videoMessage,
caption: 'Legenda do vídeo',
contextInfo: { isGroupStatus: true }
},
messageContextInfo: { messageSecret }
}
}
}, {})
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })// ── Áudio ─────────────────────────────────────────────────────────────────────
const messageSecret = crypto.randomBytes(32)
const prep = await prepareWAMessageMedia(
{ audio: buffer, mimetype: 'audio/mp4' },
{ upload: sock.waUploadToServer }
)
const msg = generateWAMessageFromContent(jid, {
messageContextInfo: { messageSecret },
groupStatusMessageV2: {
message: {
audioMessage: {
...prep.audioMessage,
contextInfo: { isGroupStatus: true }
},
messageContextInfo: { messageSecret }
}
}
}, {})
await sock.relayMessage(jid, msg.message, { messageId: msg.key.id })Pontos importantes:
messageSecretdeve ser umBufferde 32 bytes gerado comcrypto.randomBytes(32)— cada postagem precisa de um novo secretaudienceType: 1ativa o modo Close Friends; omitirstatusAudienceMetadataposta para todos do grupo- Para repostar uma mensagem citada (quoted), baixe a mídia com
downloadMediaMessageantes de fazer oprepareWAMessageMedia - O
groupStatus: truenosendMessagesimples (seção 3) existe como atalho, mas para controle de Close Friends e repost de mídias, use ogroupStatusMessageV2direto como mostrado acima
27. Figurinha de Avatar (isAvatar)
A lib suporta o envio de figurinhas com metadata de avatar do WhatsApp — o tipo que carrega os campos is-avatar-sticker, avatar-sticker-style, etc. Basta passar isAvatar: true no sendMessage.
O EXIF de avatar é injetado diretamente nos bytes do container WebP (RIFF), sem reprocessar a imagem (a animação é preservada) e sem depender de bibliotecas externas como node-webpmux.
// envio básico — usa o metadata de avatar padrão
await sock.sendMessage(jid, {
sticker: buffer, // Buffer do webp
isAvatar: true
})Customizar nome do pack e autor
await sock.sendMessage(jid, {
sticker: buffer,
isAvatar: true,
stickerPackName: '𝕷’𝖆𝖌𝖚̈𝖊𝖗𝖔', // opcional — default: 'lpps'
stickerAuthor: 'lopess', // opcional — default: 'lppss'
stickerPackId: 'lopesapi' // opcional — default gerado
})Metadata injetado
Quando isAvatar: true, a lib injeta o EXIF de avatar no webp (os campos de nome/autor/id são substituídos pelos que você passar):
{
"sticker-pack-name": "lopess",
"sticker-pack-publisher": "lopess",
"is-avatar-sticker": 1,
"avatar-sticker-template-id": "whatsapp",
"is-ai-sticker": 1,
"is-avatar-country-sticker": 1,
"is-avatar-instant-sticker": 1,
"sticker-maker-source-type": 4,
"is-avatar-social-sticker": 1,
"avatar-sticker-style": "whatsapp",
"avatar-sticker-revision-id": "2026",
"is-from-user-created-pack": 1,
"origin-pack-id": "whatsapp",
"is-text-sticker": 1
}Exemplo real — roubar figurinha e reenviar como avatar
const { downloadContentFromMessage } = require('@lpzeravk/baileys')
const stream = await downloadContentFromMessage(stickerMsg, 'sticker')
let buffer = Buffer.from([])
for await (const chunk of stream) buffer = Buffer.concat([buffer, chunk])
await sock.sendMessage(jid, {
sticker: buffer,
isAvatar: true,
stickerPackName: 'Roubada',
stickerAuthor: 'lopess',
}, { quoted: m })Notas:
- Se já existir um chunk EXIF no webp original, ele é removido antes de injetar o novo (não duplica).
- Funciona com figurinhas estáticas e animadas — os frames não são tocados, só o metadata.
- Diferente de
isLottie, oisAvatarenvia umstickerMessagewebp normal (não umlottieStickerMessage), que é o formato que renderiza corretamente no envio.
28. Álbum (múltiplas mídias agrupadas)
Envia várias imagens e/ou vídeos agrupados num único bloco visual (carrossel de mídia), como o WhatsApp faz quando você seleciona várias fotos de uma vez.
Uso simples com album
A forma recomendada. Passe um array de itens { image } ou { video } — a lib conta os tipos e monta o albumMessage sozinha.
await sock.sendMessage(jid, {
album: [
{ image: buffer1 },
{ image: { url: 'https://.../foto2.jpg' } },
{ video: bufferVideo },
]
}, { quoted: m })Regras:
- Mínimo de 2 itens (imagem + imagem, imagem + vídeo, etc). Menos que isso lança erro.
- Cada item aceita
Buffer,{ url }ou{ stream }, igual às mídias normais. - Imagens e vídeos podem ser misturados livremente no mesmo álbum.
Álbum com legenda
O álbum em si não tem campo de legenda. Para acompanhar de um texto, envie a legenda como mensagem separada logo em seguida:
await sock.sendMessage(jid, {
album: [{ image: img1 }, { image: img2 }]
}, { quoted: m })
await sock.sendMessage(jid, { text: legenda }, { quoted: m })Álbum a partir de URLs remotas
Útil quando a mídia vem de uma API (galeria, slides, carrossel):
const urls = ['https://.../1.jpg', 'https://.../2.jpg', 'https://.../3.jpg']
await sock.sendMessage(jid, {
album: urls.map(url => ({ image: { url } }))
}, { quoted: m })Para uma lista que pode conter imagens e vídeos, decida o tipo por item:
const itens = midias.map(m2 => {
const ehVideo = /\.mp4($|\?)/i.test(m2.url)
return ehVideo ? { video: { url: m2.url } } : { image: { url: m2.url } }
})
await sock.sendMessage(jid, { album: itens }, { quoted: m })Controle manual com albumMessage
Se precisar montar o proto manualmente (por exemplo, mídia já preparada com prepareWAMessageMedia), pode passar albumMessage como array diretamente. Nesse modo você controla cada item, mas perde a contagem automática de tipos que o album faz.
await sock.sendMessage(jid, {
albumMessage: [
{ image: { url: url1 } },
{ image: { url: url2 } },
]
}, { quoted: m })Prefira
albumsempre que possível — ele valida o mínimo de 2 itens e preencheexpectedImageCount/expectedVideoCountcorretamente, o que faz o carrossel renderizar direito em todos os clientes.
29. Chamadas de voz (calls)
Sistema nativo de chamada de voz. Liga sozinho em todo makeWASocket, sem precisar de nenhuma configuração. O estado de cada chamada fica em sock.calls, indexado pelo callId.
Requer werift-dtls e werift-sctp (já são dependências da lib, instaladas junto). Pra tocar áudio numa chamada, instala também opusscript (peer dependency opcional).
Ligar (1:1)
const { callId, callCreator } = await sock.startCall(jid, originChatId)originChatId é opcional. Se não passar, usa o próprio jid alvo. Serve pra você saber depois em qual conversa avisar o resultado da chamada.
Ligar em grupo
const { callId, participantCount } = await sock.startGroupCall(groupJid, originChatId)O grupo precisa ter pelo menos 2 outros participantes com LID já resolvido pelo WhatsApp. Se ainda faltar resolver, a chamada não abre e um erro claro é lançado explicando isso.
Entrar por link de chamada
const result = await sock.joinCallLink('https://call.whatsapp.com/voice/...')
if (result.ok) {
console.log('Entrou na call', result.callId)
} else {
console.log('Não entrou:', result.reason) // 'sala de espera', 'timeout', etc
}Aceita tanto o link completo quanto só o token. Se a call estiver numa sala de espera aguardando admissão, result.ok vem false e result.reason explica o motivo.
Encerrar
await sock.endCall(callId)Manda o terminate pro servidor e limpa o registro em sock.calls.
Tocar áudio na chamada
sock.playCallAudio(callId, pcmBuffer) // PCM s16le, mono, 16kHz
sock.stopCallAudio(callId)pcmBuffer precisa estar exatamente nesse formato (PCM cru, canal único, 16000Hz, 16 bits por amostra). Converta com ffmpeg antes se sua fonte for mp3, ogg ou qualquer outro formato:
ffmpeg -i entrada.mp3 -f s16le -ac 1 -ar 16000 saida.pcmEnquanto nenhuma faixa está tocando, a chamada mantém um fluxo de silêncio contínuo. Isso é proposital: a conexão precisa ficar sempre mandando pacote, senão o relay derruba.
Eventos
sock.ev.on('call', (calls) => {
for (const call of calls) {
console.log(call.id, call.status) // offer, accept, reject, timeout, terminate
if (call.status === 'offer') sock.rejectCall(call.id, call.from)
}
})Esse é o evento padrão do Baileys, sem nada de especial nele. sock.calls[call.id] tem o resto do estado (peer, callCreator, isGroup, pids conectados de grupo, entre outros).
Reconexão automática
Toda chamada de grupo é vigiada sozinha por um watchdog interno: se o roster mudar (alguém entrando ou saindo) ou a chamada estiver perto de um limite conhecido da própria plataforma pra dispositivo companion, ela reconecta na hora, sem precisar de nenhum código seu pra isso.
Log
Todo log de chamada passa pelo logger do próprio socket (o mesmo que você passa em logger no makeWASocket). Não existe arquivo de log separado.
Changelog
v1.0.0 — Lançamento inicial
Mensagens e mídia
- Sticker Pack nativo com animadas e PNG
- Album, Spoiler, ViewOnce V2, Ephemeral, Lottie, Evento
- Native Flow, Carousel, Template Buttons, botões estendidos (20+ tipos via
nativeFlow) - AI Rich (
makeText,makeCode,makeTable,makeList,sendRich) - Poll decrypt com
getAggregateVotesInPollMessage - MessageBuilder (
Button,ButtonV2,Carousel,AIRich) - Figurinha de Avatar (
isAvatar) — envio de figurinha com metadata de avatar do WhatsApp; EXIF injetado direto nos bytes do WebP (RIFF), sem reprocessar a imagem e sem depender denode-webpmux - Waveform (spectral) — o waveform (ondinhas) do áudio saía como linha reta porque o
audio-decodenão decodifica opus/ogg; agora extrai os samples PCM viaffmpeg, funcionando em qualquer formato; gerado para todos os áudios, não sóptt - PTT real —
ptt: trueconverte viaffmpegantes do upload; corrigido bugCannot use 'in' operator to search for 'stream'(caminho do arquivo transcrito era passado como string crua em vez de{ url }) - Limite de upload de 1GB — envio de documentos/mídia suporta arquivos de até 1GB por padrão (igual ao limite de documento do WhatsApp oficial), configurável via
options.maxContentLengthno socket. A validação vale pra todos os tipos de origem (arquivo, buffer, stream e URL remota), com erro claro (413) em vez de falha silenciosa/travamento em arquivos grandes demais.
LID / JID e conexão
- Sistema LID/JID avançado com cache bidirecional (
sharedLidPhoneCache) lidToJid,resolveJid,resolveAll,normalizeJid,validateJid,getSenderInfo- Suporte a
@username—isUsername,resolveUsername - Fix canal newsletter, fix groupStatus áudio,
isSystemNotification - Fix groupMetadata → sharedLidPhoneCache — pares LID↔telefone registrados automaticamente ao buscar metadata de grupo
- Conexão / pareamento —
getCompanionPlatformId(enumCompanionWebClientTypecorreto) no lugar dogetPlatformIdgenérico, que causava rejeição de pareamento;pushNameadicionado ao payload de login;lidDbMigratedno login reflete o estado real das credenciais; companion version usa a versão real do WA;"forbidden"mapeado corretamente praDisconnectReason.forbidden - Anti-ban 405 —
DisconnectReason.rateLimited(405) adicionado; handler loga o rate limit para o reconnect esperar antes de tentar de novo FATAL_DISCONNECT_REASONSexportado — array (loggedOut,badSession,multideviceMismatch,forbidden) com os motivos deconnection.update(close) que significam sessão realmente inválida.connectionReplaced(440) fica de fora de propósito: só significa que outro socket assumiu no lugar desse, as credenciais continuam válidas. Quem escreve a própria lógica de reconexão pode usar isso pra decidir quando limpar a sessão salva vs. só tentar reconectar. Não muda nenhum comportamento domakeWASocket— é só a constante, sem reconexão automática embutida.- PreKey recovery melhorado — upload de 30 chaves (era 5), delay de 2500ms (era 1000ms), parâmetro
forcepra ignorarMIN_UPLOAD_INTERVALem recuperação de erro - Instalação — a dependência
libsignalaponta para a versão publicada no npm (libsignal@^6.0.0), sem depender de git (evita quebrarnpm installem ambientes que bloqueiam fetch de pacotes git, como Pterodactyl, CI, VPS sem git)
Outros utilitários
- Guard de pagamento stealth, Bad MAC Handler,
getName,getBestWaVersion,resolveLidPhoneFromGroup, WhatsApp Flows - Mensagens agendadas —
MessageScheduler/createMessageScheduler - Rede limpa —
makeWASocketnão abre nenhuma conexão externa além da conexão real com o WhatsApp; não existe nenhum canal remoto de comando embutido na lib.
💛 @lpzeravk/baileys v1.0.0 💛
Desenvolvido por LOPES
