Baileys communicates with WhatsApp Web servers using WebSockets and a custom binary protocol. This page explains how to listen to and handle WebSocket events.
Event System Overview
Baileys uses an event-driven architecture where you can register callbacks for specific WebSocket events using the sock.ws.on() pattern.
Event Callback Prefix
All WebSocket event callbacks use the CB: prefix by default:
import { DEF_CALLBACK_PREFIX } from '@whiskeysockets/baileys'
console.log(DEF_CALLBACK_PREFIX) // 'CB:'
BinaryNode Structure
WhatsApp messages are transmitted as binary nodes with a simple structure:
type BinaryNode = {
tag: string
attrs: { [key: string]: string }
content?: BinaryNode[] | string | Uint8Array
}
Components Explained
tag - Identifies the message type
- Examples:
message, ib, iq, receipt, notification, presence
attrs - Key-value metadata
- Common attributes:
id, from, to, type, participant
- All values are strings
content - The message payload
- Can be nested
BinaryNode[] for complex messages
- Can be
string for text data
- Can be
Uint8Array for binary data (encrypted payloads, media, etc.)
Example BinaryNode
const exampleNode: BinaryNode = {
tag: 'iq',
attrs: {
id: '12345',
type: 'result',
from: 's.whatsapp.net'
},
content: [
{
tag: 'query',
attrs: { xmlns: 'status' },
content: 'Hello World'
}
]
}
Event Pattern Matching
Baileys provides flexible pattern matching for WebSocket events. The pattern determines which messages trigger your callback.
Basic Pattern: Tag Only
Listen for all messages with a specific tag:
import { BinaryNode } from '@whiskeysockets/baileys'
// Fires for ANY message with tag 'message'
sock.ws.on('CB:message', (node: BinaryNode) => {
console.log('Message received:', node)
})
// Fires for ANY message with tag 'presence'
sock.ws.on('CB:presence', (node: BinaryNode) => {
console.log('Presence update:', node.attrs.from, node.attrs.type)
})
Pattern: Tag + Attribute
Match messages with a specific tag AND attribute value:
// Only IQ messages with type='result'
sock.ws.on('CB:iq,type:result', (node: BinaryNode) => {
console.log('IQ result:', node)
})
// Only IQ messages with type='set'
sock.ws.on('CB:iq,type:set', (node: BinaryNode) => {
console.log('IQ set request:', node)
})
The pattern format is: CB:{tag},{attrKey}:{attrValue}
Pattern: Tag + Attribute + Content Tag
Match the first content child’s tag:
// Match: tag='iq', type='set', first content child tag='pair-device'
sock.ws.on('CB:iq,type:set,pair-device', (node: BinaryNode) => {
console.log('Pair device request:', node)
})
// Match: tag='ib', any attributes, first content child tag='offline'
sock.ws.on('CB:ib,,offline', (node: BinaryNode) => {
console.log('Offline notifications received:', node)
})
The pattern format is: CB:{tag},{attrKey}:{attrValue},{contentTag}
Notice the double comma ,, when you want to match content tag but don’t care about a specific attribute.
Pattern: Multiple Attributes
You can match against multiple attributes:
// Match tag='iq', id='abcd'
sock.ws.on('CB:iq,id:abcd', (node: BinaryNode) => {
console.log('Specific IQ response:', node)
})
// Match tag='iq', id='abcd', first content='query'
sock.ws.on('CB:iq,id:abcd,query', (node: BinaryNode) => {
console.log('Query response:', node)
})
Event Routing Internals
When a message arrives, Baileys emits multiple events in a specific order:
// From src/Socket/socket.ts - onMessageReceived function
const l0 = frame.tag
const l1 = frame.attrs || {}
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
// Events are emitted in this order:
// 1. TAG:{msgId} - for awaiting specific responses
ws.emit(`TAG:${msgId}`, frame)
// 2. CB:{tag},{key}:{value},{contentTag}
for (const key of Object.keys(l1)) {
ws.emit(`CB:${l0},${key}:${l1[key]},${l2}`, frame)
ws.emit(`CB:${l0},${key}:${l1[key]}`, frame)
ws.emit(`CB:${l0},${key}`, frame)
}
// 3. CB:{tag},,{contentTag}
ws.emit(`CB:${l0},,${l2}`, frame)
// 4. CB:{tag}
ws.emit(`CB:${l0}`, frame)
The most specific pattern is checked first. Register the most specific handler you need to avoid unnecessary callbacks.
Common Event Patterns
Here are real-world examples from the Baileys source code:
Connection Events
// Stream ended by server
sock.ws.on('CB:xmlstreamend', () => {
console.log('Stream ended')
})
// Pair device request (QR code)
sock.ws.on('CB:iq,type:set,pair-device', async (stanza: BinaryNode) => {
console.log('QR code pairing requested')
})
// Successful pairing
sock.ws.on('CB:iq,,pair-success', async (stanza: BinaryNode) => {
console.log('Device paired successfully')
})
// Connection success
sock.ws.on('CB:success', async (node: BinaryNode) => {
console.log('Logged in successfully')
})
// Stream error
sock.ws.on('CB:stream:error', (node: BinaryNode) => {
console.error('Stream error:', node)
})
// Connection failure
sock.ws.on('CB:failure', (node: BinaryNode) => {
console.error('Connection failed:', node.attrs.reason)
})
Message Events
// Incoming messages
sock.ws.on('CB:message', async (node: BinaryNode) => {
console.log('New message:', node)
})
// Message receipts (delivered, read, etc.)
sock.ws.on('CB:receipt', async (node: BinaryNode) => {
console.log('Receipt:', node.attrs.type)
})
// Notifications (group changes, etc.)
sock.ws.on('CB:notification', async (node: BinaryNode) => {
console.log('Notification:', node.attrs.type)
})
// Message acknowledgment
sock.ws.on('CB:ack,class:message', (node: BinaryNode) => {
console.log('Message acknowledged')
})
Presence & Chat State
// User presence (online, offline)
sock.ws.on('CB:presence', (node: BinaryNode) => {
const from = node.attrs.from
const type = node.attrs.type // 'available', 'unavailable'
console.log(`${from} is ${type}`)
})
// Typing indicator, recording audio, etc.
sock.ws.on('CB:chatstate', (node: BinaryNode) => {
const from = node.attrs.from
const state = node.attrs.type // 'composing', 'recording'
console.log(`${from} is ${state}`)
})
Data Sync Events
// Dirty flag (data needs syncing)
sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
console.log('Data sync required')
})
// Offline messages received
sock.ws.on('CB:ib,,offline', (node: BinaryNode) => {
const child = getBinaryNodeChild(node, 'offline')
const count = +(child?.attrs.count || 0)
console.log(`Received ${count} offline messages`)
})
// Edge routing update
sock.ws.on('CB:ib,,edge_routing', (node: BinaryNode) => {
console.log('Routing information updated')
})
Call Events
// Incoming call
sock.ws.on('CB:call', async (node: BinaryNode) => {
const callId = node.attrs.id
const from = node.attrs.from
console.log(`Call from ${from}:`, callId)
})
Debugging Event Flow
Enable trace-level logging to see the XML representation of all frames:
import P from 'pino'
const sock = makeWASocket({
logger: P({ level: 'trace' })
})
You’ll see output like:
{xml: '<iq id="123" type="result">...</iq>', msg: 'recv xml'}
Trace-level logging produces a LOT of output. Use it only for debugging specific issues.
Frame Decoding
Incoming WebSocket data is decoded through the noise protocol encryption:
// Internal flow (from src/Socket/socket.ts)
const onMessageReceived = async (data: Buffer) => {
await noise.decodeFrame(data, frame => {
// frame is either Uint8Array (raw binary) or BinaryNode
if (!(frame instanceof Uint8Array)) {
// It's a BinaryNode - emit events
ws.emit('frame', frame)
// ... pattern matching and event emission
}
})
}
Next Steps