Handling Events
The socket is an event emitter (sock.ev). This is the complete event map of the package β every event name below exists in the BaileysEventMap type, with the payload it actually carries.
Connection & credentials
| Event | Payload | Notes |
|---|---|---|
connection.update | Partial<ConnectionState> | { connection, lastDisconnect, qr, isNewLogin, receivedPendingNotifications } β see Connection & Pairing |
creds.update | Partial<AuthenticationCreds> | Always wire this to saveCreds from your auth state, or the session won't survive restarts |
Messages
| Event | Payload | Notes |
|---|---|---|
messages.upsert | { messages: WAMessage[], type, requestId? } | type: 'notify' = new incoming messages in real time. type: 'append' = history/offline catch-up β most bots should react to notify only, or they'll reply to old messages after a reconnect |
messages.update | WAMessageUpdate[] | Status changes (sent β delivered β read), edits, and poll updates (update.pollUpdates) |
messages.delete | { keys } | { jid, all: true } | Messages deleted for everyone, or a whole chat cleared |
messages.reaction | { key, reaction }[] | Reactions added/removed (empty reaction.text = removed) |
message-receipt.update | MessageUserReceiptUpdate[] | Per-user receipts in groups: who read/played what, and when |
messages.media-update | { key, media?, error? }[] | Result of a media re-upload request (requestPlaceholderResend) β the new direct path for media that failed to download |
Chats, contacts, presence
| Event | Payload | Notes |
|---|---|---|
messaging-history.set | { chats, contacts, messages, isLatest?, progress?, syncType? } | The big history sync after pairing β reverse-chronologically sorted. progress reports sync percentage |
chats.upsert / chats.update / chats.delete | Chat[] / ChatUpdate[] / string[] | New chats, changed chats (name, unread count, archive stateβ¦), deleted chat JIDs |
chats.phoneNumberShare | { lid, jid } | A contact shared their phone number for a LID |
contacts.upsert / contacts.update | Contact[] / Partial<Contact>[] | Address-book and profile changes |
presence.update | { id, presences: { [participant]: PresenceData } } | Only fires after sock.presenceSubscribe(jid). lastKnownPresence is one of available | unavailable | composing | recording | paused |
blocklist.set / blocklist.update | { blocklist } | { blocklist, type: 'add' | 'remove' } | The account's blocklist changed |
labels.edit / labels.association | Label / { label, type: 'add' | 'remove' } | WhatsApp Business label changes |
Groups & newsletters
| Event | Payload | Notes |
|---|---|---|
groups.upsert / groups.update | GroupMetadata[] / Partial<GroupMetadata>[] | Joined a group / group metadata changed |
group-participants.update | { id, author, participants, action } | action: add | remove | promote | demote | modify. The backbone of welcome/goodbye bots β see Recipes |
group.join-request | { id, author, participant, action, method } | Join requests when approval mode is on (action: 'created' | 'revoked' | β¦) |
newsletter.view / newsletter-participants.update / newsletter-settings.update / newsletter.reaction | newsletter payloads | Channel views, subscriber changes, settings changes, and reactions to channel posts |
call | WACallEvent[] | Incoming call offers/updates β reject with sock.rejectCall(callId, callFrom) |
Reading an incoming message properly
sock.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return // skip history catch-up
for (const m of messages) {
if (!m.message) continue // key-only updates happen
if (m.key.fromMe) continue // don't loop on yourself
const jid = m.key.remoteJid
const isGroup = jid.endsWith('@g.us')
const sender = isGroup ? m.key.participant : jid
const text =
m.message.conversation ||
m.message.extendedTextMessage?.text ||
m.message.imageMessage?.caption ||
m.message.videoMessage?.caption || ''
console.log(sender, ': ', text)
}
})
Buffered processing (don't lose events)
The event emitter buffers events while your async handler is busy, if you use its processing helper β this exists so bursts (like the initial history sync) don't interleave half-written store updates:
const off = sock.ev.process(async (events) => {
if (events['connection.update']) { /* ... */ }
if (events['messages.upsert']) { /* ... */ }
})
// call off() to stop processing
And when you tear a socket down completely, call sock.ev.destroy() (a real destroy since v1.0.4) so no listeners leak into your next reconnect attempt.
Rule of thumb: if your bot "replies to old messages" or "answers twice after reconnecting", it's almost always an
append upsert being treated as notify, or two sockets alive at once. Filter the type, and destroy the old emitter.