Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/whiskeysockets/Baileys/llms.txt

Use this file to discover all available pages before exploring further.

Baileys uses WhatsApp’s binary protocol for efficient communication. This guide explains the protocol structure and how messages are encoded/decoded.

Binary Protocol Overview

WhatsApp uses a custom binary protocol instead of plain XML to reduce bandwidth and improve performance. Messages are encoded as binary nodes with a compact token-based encoding system.

Key Components

  • Binary Nodes: Structured data units with tags, attributes, and content
  • Token Dictionary: Predefined tokens for common strings
  • Binary Encoding: Efficient byte-level encoding
  • Compression: Optional frame compression

Binary Node Structure

From src/WABinary/types.ts:9-13:
export type BinaryNode = {
    tag: string
    attrs: { [key: string]: string }
    content?: BinaryNode[] | string | Uint8Array
}

Components

Tag: Identifies the node type
const node = {
    tag: 'message',  // Node type
    attrs: { ... },
    content: [ ... ]
}
Attributes: Key-value metadata
const node = {
    tag: 'iq',
    attrs: {
        id: 'msg-id-123',
        type: 'get',
        xmlns: 'encrypt'
    },
    content: [ ... ]
}
Content: Can be nested nodes, strings, or binary data
// Nested nodes
const nodeWithChildren = {
    tag: 'iq',
    attrs: { id: '1' },
    content: [
        { tag: 'query', attrs: {}, content: 'some data' },
        { tag: 'list', attrs: {}, content: [ ... ] }
    ]
}

// String content
const textNode = {
    tag: 'body',
    attrs: {},
    content: 'Hello, World!'
}

// Binary content
const binaryNode = {
    tag: 'enc',
    attrs: {},
    content: new Uint8Array([1, 2, 3, 4])
}

Token-Based Encoding

To reduce message size, WhatsApp uses a dictionary of predefined tokens for common strings.

Token Types

From src/WABinary/constants.ts:1-19:
export const TAGS = {
    LIST_EMPTY: 0,
    DICTIONARY_0: 236,
    DICTIONARY_1: 237,
    DICTIONARY_2: 238,
    DICTIONARY_3: 239,
    INTEROP_JID: 245,
    FB_JID: 246,
    AD_JID: 247,
    LIST_8: 248,
    LIST_16: 249,
    JID_PAIR: 250,
    HEX_8: 251,
    BINARY_8: 252,
    BINARY_20: 253,
    BINARY_32: 254,
    NIBBLE_8: 255,
    PACKED_MAX: 127
}

Single-Byte Tokens

Common strings encoded as single bytes (0-235):
// From constants.ts:1056-1293
export const SINGLE_BYTE_TOKENS = [
    '',
    'xmlstreamstart',
    'xmlstreamend',
    's.whatsapp.net',
    'type',
    'participant',
    'from',
    'receipt',
    'id',
    'notification',
    // ... 200+ more tokens
]
Example: The string "type" is encoded as byte 4 instead of 4 bytes.

Double-Byte Tokens

Less common strings use two bytes (dictionary index + token index):
// From constants.ts:21-1054
export const DOUBLE_BYTE_TOKENS = [
    [  // Dictionary 0
        'read-self',
        'active',
        'fbns',
        'protocol',
        // ...
    ],
    [  // Dictionary 1
        'reject',
        'dirty',
        'announcement',
        // ...
    ],
    // ... more dictionaries
]

Encoding Process

From src/WABinary/encode.ts:5-12:
export const encodeBinaryNode = (
    node: BinaryNode,
    opts = constants,
    buffer: number[] = [0]
): Buffer => {
    const encoded = encodeBinaryNodeInner(node, opts, buffer)
    return Buffer.from(encoded)
}

Encoding Steps

  1. Write list header - Number of elements (tag + attributes + content)
  2. Write tag - Encoded as token or raw string
  3. Write attributes - Each key-value pair encoded
  4. Write content - Recursively encode based on type

String Encoding

From src/WABinary/encode.ts:179-209:
const writeString = (str?: string) => {
    if (str === undefined || str === null) {
        pushByte(TAGS.LIST_EMPTY)
        return
    }
    
    // Check if string is in token map
    const tokenIndex = TOKEN_MAP[str]
    if (tokenIndex) {
        if (typeof tokenIndex.dict === 'number') {
            pushByte(TAGS.DICTIONARY_0 + tokenIndex.dict)
        }
        pushByte(tokenIndex.index)
    } 
    // Check if string can be packed as nibbles (numbers, -, .)
    else if (isNibble(str)) {
        writePackedBytes(str, 'nibble')
    } 
    // Check if string can be packed as hex
    else if (isHex(str)) {
        writePackedBytes(str, 'hex')
    } 
    // Try to encode as JID
    else {
        const decodedJid = jidDecode(str)
        if (decodedJid) {
            writeJid(decodedJid)
        } else {
            writeStringRaw(str)  // Fallback: raw UTF-8
        }
    }
}

Packed Encoding

Nibble Packing: Stores numbers and special chars (-, .) as 4-bit values
// "123-456" packed into fewer bytes
const packNibble = (char: string) => {
    switch (char) {
        case '-': return 10
        case '.': return 11
        case '\0': return 15
        default:
            if (char >= '0' && char <= '9') {
                return char.charCodeAt(0) - '0'.charCodeAt(0)
            }
    }
}
Hex Packing: Stores hex strings (0-9, A-F) efficiently
// "ABCD1234" packed as 4-bit values
const packHex = (char: string) => {
    if (char >= '0' && char <= '9') {
        return char.charCodeAt(0) - '0'.charCodeAt(0)
    }
    if (char >= 'A' && char <= 'F') {
        return 10 + char.charCodeAt(0) - 'A'.charCodeAt(0)
    }
    // ...
}

JID Encoding

WhatsApp IDs (JIDs) are specially encoded:
const writeJid = ({ domainType, device, user, server }: FullJid) => {
    if (typeof device !== 'undefined') {
        // Device JID (e.g., "123@s.whatsapp.net:1")
        pushByte(TAGS.AD_JID)
        pushByte(domainType || 0)
        pushByte(device || 0)
        writeString(user)
    } else {
        // Simple JID pair (e.g., "123@s.whatsapp.net")
        pushByte(TAGS.JID_PAIR)
        if (user.length) {
            writeString(user)
        } else {
            pushByte(TAGS.LIST_EMPTY)
        }
        writeString(server)
    }
}

Decoding Process

From src/WABinary/decode.ts:9-18:
export const decompressingIfRequired = async (buffer: Buffer) => {
    if (2 & buffer.readUInt8()) {
        // Bit 1 set: compressed
        buffer = await inflatePromise(buffer.slice(1))
    } else {
        // No compression, skip 0x00 prefix
        buffer = buffer.slice(1)
    }
    return buffer
}

Decoding Steps

  1. Decompress - If compression flag is set
  2. Read list size - Number of elements
  3. Read tag - First element is always the tag
  4. Read attributes - Pairs of key-value strings
  5. Read content - If list size is even, read content
From src/WABinary/decode.ts:251-296:
const listSize = readListSize(readByte()!)
const header = readString(readByte()!)

const attrs: BinaryNode['attrs'] = {}
let data: BinaryNode['content']

// Read attributes (pairs)
const attributesLength = (listSize - 1) >> 1
for (let i = 0; i < attributesLength; i++) {
    const key = readString(readByte()!)
    const value = readString(readByte()!)
    attrs[key] = value
}

// Read content if present
if (listSize % 2 === 0) {
    const tag = readByte()!
    if (isListTag(tag)) {
        data = readList(tag)  // Child nodes
    } else {
        // Binary or string content
        switch (tag) {
            case TAGS.BINARY_8:
                data = readBytes(readByte()!)
                break
            case TAGS.BINARY_20:
                data = readBytes(readInt20())
                break
            case TAGS.BINARY_32:
                data = readBytes(readInt(4))
                break
            default:
                data = readString(tag)
        }
    }
}

return { tag: header, attrs, content: data }

Frame Format

WhatsApp frames are wrapped with: From src/Defaults/index.ts:34:
export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, DICT_VERSION])
// WA (0x57 0x41) + Protocol Version (6) + Dictionary Version (3)

Compression

Frames can be optionally compressed using zlib:
// First byte indicates compression
// 0x00 = uncompressed
// 0x02 = compressed (zlib inflate)
if (2 & buffer.readUInt8()) {
    buffer = await inflatePromise(buffer.slice(1))
}

Common Node Examples

Query Node

const queryNode: BinaryNode = {
    tag: 'iq',
    attrs: {
        id: 'msg-123',
        type: 'get',
        xmlns: 'encrypt',
        to: 's.whatsapp.net'
    },
    content: [
        {
            tag: 'count',
            attrs: {},
            content: undefined
        }
    ]
}
This queries the server for pre-key count.

Message Node

const messageNode: BinaryNode = {
    tag: 'message',
    attrs: {
        id: 'msg-456',
        type: 'text',
        from: '1234567890@s.whatsapp.net',
        to: '0987654321@s.whatsapp.net',
        t: '1678901234'
    },
    content: [
        {
            tag: 'enc',
            attrs: { v: '2', type: 'msg' },
            content: new Uint8Array([/* encrypted data */])
        }
    ]
}

Presence Node

const presenceNode: BinaryNode = {
    tag: 'presence',
    attrs: {
        name: 'John Doe',
        type: 'available'
    }
}

Working with Binary Nodes

Creating Nodes

import { encodeBinaryNode } from '@whiskeysockets/baileys'

const node: BinaryNode = {
    tag: 'iq',
    attrs: { id: '1', type: 'get' },
    content: [
        { tag: 'ping', attrs: {} }
    ]
}

const encoded = encodeBinaryNode(node)
// Send via WebSocket

Parsing Nodes

import { decodeBinaryNode } from '@whiskeysockets/baileys'

const buffer = Buffer.from([/* binary data */])
const node = await decodeBinaryNode(buffer)

console.log('Tag:', node.tag)
console.log('Attrs:', node.attrs)
console.log('Content:', node.content)

Extracting Content

import { 
    getBinaryNodeChild, 
    getBinaryNodeChildren,
    getAllBinaryNodeChildren 
} from '@whiskeysockets/baileys'

// Get first child with specific tag
const countNode = getBinaryNodeChild(node, 'count')

// Get all children with specific tag
const refNodes = getBinaryNodeChildren(node, 'ref')

// Get all child nodes (of any type)
const allChildren = getAllBinaryNodeChildren(node)

Converting to String (Debug)

import { binaryNodeToString } from '@whiskeysockets/baileys'

const xmlString = binaryNodeToString(node)
console.log(xmlString)
// Output: <iq id="1" type="get"><ping/></iq>

Protocol Constants

From src/Defaults/index.ts:
// Noise protocol header
export const NOISE_MODE = 'Noise_XX_25519_AESGCM_SHA256\0\0\0\0'
export const NOISE_WA_HEADER = Buffer.from([87, 65, 6, 3])

// Dictionary version
export const DICT_VERSION = 3

// Key bundle type
export const KEY_BUNDLE_TYPE = Buffer.from([5])

// Default ephemeral message duration
export const WA_DEFAULT_EPHEMERAL = 7 * 24 * 60 * 60

Best Practices

Use Helper Functions: Baileys provides utilities for working with binary nodes. Use them instead of manual parsing.
// Good
const child = getBinaryNodeChild(node, 'query')

// Avoid
const child = Array.isArray(node.content) 
    ? node.content.find(n => typeof n === 'object' && n.tag === 'query')
    : null
Type Safety: Always check content types before accessing:
if (Array.isArray(node.content)) {
    // Process as child nodes
    for (const child of node.content) {
        if (typeof child === 'object' && child.tag) {
            // Process child node
        }
    }
} else if (typeof node.content === 'string') {
    // Process as text
} else if (node.content instanceof Uint8Array) {
    // Process as binary
}

Debugging Tools

Enable trace logging to see binary node XML representations:
import makeWASocket from '@whiskeysockets/baileys'
import P from 'pino'

const sock = makeWASocket({
    logger: P({ level: 'trace' })
})
Output example:
{
    "xml": "<iq id='1' type='get'><ping/></iq>",
    "msg": "xml send"
}

Learn More

To understand the underlying cryptography:
  • Libsignal Protocol: End-to-end encryption protocol
  • Noise Protocol Framework: Handshake patterns and session keys

Next Steps