Overview
Baileys uses an EventEmitter-based system to notify your application of WhatsApp events like new messages, connection changes, and contact updates. All events are strongly typed through the BaileysEventMap interface.
BaileysEventMap
From src/Types/Events.ts:20:
type BaileysEventMap = {
// Connection events
'connection.update': Partial<ConnectionState>
'creds.update': Partial<AuthenticationCreds>
// Message events
'messages.upsert': {
messages: WAMessage[];
type: MessageUpsertType;
requestId?: string
}
'messages.update': WAMessageUpdate[]
'messages.delete': { keys: WAMessageKey[] } | { jid: string; all: true }
'messages.reaction': { key: WAMessageKey; reaction: proto.IReaction }[]
'messages.media-update': {
key: WAMessageKey;
media?: { ciphertext: Uint8Array; iv: Uint8Array };
error?: Boom
}[]
// Chat events
'chats.upsert': Chat[]
'chats.update': ChatUpdate[]
'chats.delete': string[]
'chats.lock': { id: string; locked: boolean }
// Contact events
'contacts.upsert': Contact[]
'contacts.update': Partial<Contact>[]
// History sync
'messaging-history.set': {
chats: Chat[]
contacts: Contact[]
messages: WAMessage[]
isLatest?: boolean
progress?: number | null
syncType?: proto.HistorySync.HistorySyncType | null
}
// Presence
'presence.update': {
id: string;
presences: { [participant: string]: PresenceData }
}
// Groups
'groups.upsert': GroupMetadata[]
'groups.update': Partial<GroupMetadata>[]
'group-participants.update': {
id: string
author: string
participants: GroupParticipant[]
action: ParticipantAction
}
'group.join-request': {
id: string
author: string
participant: string
action: RequestJoinAction
method: RequestJoinMethod
}
'group.member-tag.update': {
groupId: string
participant: string
label: string
messageTimestamp?: number
}
// Calls
'call': WACallEvent[]
// Labels
'labels.edit': Label
'labels.association': {
association: LabelAssociation;
type: 'add' | 'remove'
}
// Receipts
'message-receipt.update': MessageUserReceiptUpdate[]
// Blocklist
'blocklist.set': { blocklist: string[] }
'blocklist.update': { blocklist: string[]; type: 'add' | 'remove' }
// Settings
'settings.update': /* various setting types */
}
Event Emitter Interface
From src/Types/Events.ts:154:
interface BaileysEventEmitter {
on<T extends keyof BaileysEventMap>(
event: T,
listener: (arg: BaileysEventMap[T]) => void
): void
off<T extends keyof BaileysEventMap>(
event: T,
listener: (arg: BaileysEventMap[T]) => void
): void
removeAllListeners<T extends keyof BaileysEventMap>(event: T): void
emit<T extends keyof BaileysEventMap>(
event: T,
arg: BaileysEventMap[T]
): boolean
}
Basic Event Handling
Single Event Listener
const sock = makeWASocket()
sock.ev.on('messages.upsert', ({ messages, type }) => {
console.log('Received messages:', messages)
console.log('Type:', type) // 'notify' | 'append'
})
Multiple Event Listeners
sock.ev.on('connection.update', (update) => {
console.log('Connection update:', update)
})
sock.ev.on('creds.update', async () => {
await saveCreds()
})
sock.ev.on('chats.upsert', (chats) => {
console.log('New chats:', chats)
})
Event Processing Pattern
Baileys provides ev.process() for batch event processing.
From Example/example.ts:70:
sock.ev.process(
async (events) => {
// Connection updates
if (events['connection.update']) {
const update = events['connection.update']
const { connection, lastDisconnect, qr } = update
if (connection === 'close') {
const shouldReconnect =
(lastDisconnect?.error as Boom)?.output?.statusCode
!== DisconnectReason.loggedOut
if (shouldReconnect) {
startSock()
}
}
if (qr) {
console.log('QR Code:', qr)
}
}
// Credential updates
if (events['creds.update']) {
await saveCreds()
}
// New messages
if (events['messages.upsert']) {
const { messages, type } = events['messages.upsert']
for (const msg of messages) {
console.log('Message:', msg)
}
}
// Message updates
if (events['messages.update']) {
for (const { key, update } of events['messages.update']) {
if (update.pollUpdates) {
// Handle poll vote updates
}
}
}
// History sync
if (events['messaging-history.set']) {
const { chats, contacts, messages, isLatest } =
events['messaging-history.set']
console.log(
`Received ${messages.length} messages, `,
`${chats.length} chats, `,
`${contacts.length} contacts`
)
}
}
)
Using ev.process() is more efficient than individual listeners when handling multiple related events, as it batches updates together.
Message Events
messages.upsert
Fired when new messages arrive or are added to history:
sock.ev.on('messages.upsert', ({ messages, type, requestId }) => {
// type = 'notify' - new message from WhatsApp
// type = 'append' - message from history sync
// requestId - if message was requested via placeholder resync
for (const msg of messages) {
const text = msg.message?.conversation ||
msg.message?.extendedTextMessage?.text
if (text && !msg.key.fromMe) {
console.log('Received:', text, 'from', msg.key.remoteJid)
// Reply to message
await sock.sendMessage(
msg.key.remoteJid!,
{ text: 'Hello!' },
{ quoted: msg }
)
}
}
})
Always use a loop when handling messages.upsert as the messages array can contain multiple messages.
messages.update
Fired when message status changes (delivered, read, deleted, edited):
sock.ev.on('messages.update', (updates) => {
for (const { key, update } of updates) {
if (update.status) {
console.log(
`Message ${key.id} status:`,
update.status // 0=ERROR, 1=PENDING, 2=SERVER_ACK, 3=DELIVERY_ACK, 4=READ, 5=PLAYED
)
}
// Handle poll updates
if (update.pollUpdates) {
const pollCreation = await getMessage(key)
if (pollCreation) {
const votes = getAggregateVotesInPollMessage({
message: pollCreation,
pollUpdates: update.pollUpdates
})
console.log('Poll votes:', votes)
}
}
}
})
messages.reaction
Fired when a message receives a reaction:
sock.ev.on('messages.reaction', (reactions) => {
for (const { key, reaction } of reactions) {
console.log(
`Message ${key.id} reacted with:`,
reaction.text || '(removed)'
)
}
})
messages.delete
Fired when messages are deleted:
sock.ev.on('messages.delete', (deletion) => {
if ('all' in deletion) {
console.log('All messages deleted in:', deletion.jid)
} else {
console.log('Deleted messages:', deletion.keys)
}
})
Chat Events
chats.upsert
sock.ev.on('chats.upsert', (chats) => {
for (const chat of chats) {
console.log('New chat:', chat.id, chat.name)
}
})
chats.update
sock.ev.on('chats.update', (updates) => {
for (const update of updates) {
console.log('Chat updated:', update.id)
if (update.unreadCount !== undefined) {
console.log('Unread count:', update.unreadCount)
}
}
})
sock.ev.on('contacts.upsert', (contacts) => {
for (const contact of contacts) {
console.log('Contact:', contact.id, contact.name)
}
})
From Example/example.ts:204:
sock.ev.on('contacts.update', async (updates) => {
for (const contact of updates) {
if (typeof contact.imgUrl !== 'undefined') {
const newUrl = contact.imgUrl === null
? null
: await sock.profilePictureUrl(contact.id!)
.catch(() => null)
console.log(`${contact.id} has new profile pic:`, newUrl)
}
}
})
Group Events
group-participants.update
sock.ev.on('group-participants.update', ({ id, participants, action }) => {
console.log(`Group ${id}: ${action}`, participants)
// action: 'add' | 'remove' | 'promote' | 'demote'
})
groups.update
sock.ev.on('groups.update', (updates) => {
for (const update of updates) {
console.log('Group updated:', update.id)
if (update.subject) {
console.log('New subject:', update.subject)
}
}
})
Presence Events
// Subscribe to presence updates
await sock.presenceSubscribe(jid)
sock.ev.on('presence.update', ({ id, presences }) => {
for (const [participant, presence] of Object.entries(presences)) {
console.log(`${participant} is ${presence.lastKnownPresence}`)
// 'unavailable' | 'available' | 'composing' | 'recording' | 'paused'
}
})
Call Events
sock.ev.on('call', (calls) => {
for (const call of calls) {
console.log('Call from:', call.from)
console.log('Call status:', call.status)
if (call.status === 'offer') {
// Reject call
await sock.rejectCall(call.id, call.from)
}
}
})
History Sync Events
From Example/example.ts:119:
sock.ev.on('messaging-history.set', ({
chats,
contacts,
messages,
isLatest,
progress,
syncType
}) => {
console.log(`
Received history:
- ${messages.length} messages
- ${chats.length} chats
- ${contacts.length} contacts
- Is latest: ${isLatest}
- Progress: ${progress}
- Sync type: ${syncType}
`)
})
Event Order on First Connection
connection.update - Connection state changes to ‘connecting’
connection.update - QR code received (if not authenticated)
connection.update - Connection opens
creds.update - Credentials updated
messaging-history.set - Message history received
chats.upsert - Chats loaded
contacts.upsert - Contacts loaded
connection.update - receivedPendingNotifications: true
Removing Event Listeners
const messageHandler = ({ messages }) => {
console.log('Messages:', messages)
}
// Add listener
sock.ev.on('messages.upsert', messageHandler)
// Remove specific listener
sock.ev.off('messages.upsert', messageHandler)
// Remove all listeners for an event
sock.ev.removeAllListeners('messages.upsert')
Best Practices
Event Handling Best Practices:
- Use
ev.process() - Batch related events for efficiency
- Handle errors - Wrap handlers in try-catch blocks
- Async handlers - Use async/await for async operations
- Loop messages - Always iterate
messages array in messages.upsert
- Save credentials - Always handle
creds.update to save auth state
- Avoid blocking - Don’t block event handlers with long operations
- Clean up listeners - Remove listeners when done to prevent memory leaks
Error Handling
sock.ev.process(async (events) => {
try {
if (events['messages.upsert']) {
const { messages } = events['messages.upsert']
// Handle messages
}
} catch (error) {
console.error('Error handling events:', error)
}
})
TypeScript Type Safety
Baileys provides full TypeScript support for events:
// ✅ Type-safe event handling
sock.ev.on('messages.upsert', ({ messages, type }) => {
// messages: WAMessage[]
// type: MessageUpsertType
})
// ❌ TypeScript will error on invalid events
sock.ev.on('invalid.event', (data) => {
// Error: Argument of type '"invalid.event"' is not assignable...
})
See Also