Getting Started

Complete guide to building a WhatsApp bot with @mataram/wa — from installation to pairing.

1. Installation

Requires Node.js 20+ (enforced by engines in package.json). Corepack recommended for Yarn 4 users.

npm install @mataram/wa

Or using Yarn:

yarn add @mataram/wa

2. Import & Setup Session

useMultiFileAuthState persists the authentication state (Signal keys, pairing tokens, etc.) to disk. You must call saveCreds on every creds.update event to keep the session alive across restarts.

useMultiFileAuthState Parameters

ParamTypeDescription
folderstringDirectory path to store auth files. Example: 'auth'

Return Value

PropertyTypeDescription
stateAuthenticationStateContains creds (login data) & keys (Signal key store)
saveCreds() => Promise<void>Callback to persist creds on update
import makeWASocket, { useMultiFileAuthState, Browsers, DisconnectReason } from '@mataram/wa'

const { state, saveCreds } = await useMultiFileAuthState('auth')
sock.ev.on('creds.update', saveCreds)

The auth/ directory will contain: creds.json, pre-key files, session files, sender-key files, app-state-sync-key files, etc. Never commit this folder to version control.

3. Create Connection

makeWASocket — All Config Options

OptionTypeDefaultDescription
authAuthenticationState(required)Auth state from useMultiFileAuthState or custom implementation
browser[string, string, string]macOS ChromeBrowser description. WhatsApp rejects unknown platform names
waWebSocketUrlstring | URLwss://web.whatsapp.com/ws/chatWebSocket URL for WA connection
connectTimeoutMsnumber20000Connection timeout in ms
defaultQueryTimeoutMsnumber | undefined60000Default timeout for queries in ms
keepAliveIntervalMsnumber30000Ping interval to server in ms
loggerILoggerpino childPino-compatible logger
emitOwnEventsbooleantrueEmit events for own socket actions (e.g., sent messages)
markOnlineOnConnectbooleantrueMark client as online on successful connection
syncFullHistorybooleantrueRequest full history sync from primary device
fireInitQueriesbooleantrueAutomatically execute initialization queries on connection open
countryCodestring'US'Two-letter country code for registration
linkPreviewImageThumbnailWidthnumber192Width for link preview thumbnails
generateHighQualityLinkPreviewbooleanfalseUpload thumbnail to WA for high-quality link previews
maxMsgRetryCountnumber5Maximum retry attempts for failed messages
enableRecentMessageCachebooleantrueCache recent messages for retry handling
getMessage(key) => Promise<proto.IMessage | undefined>async () => undefinedFetch message from your store for retry logic
cachedGroupMetadata(jid) => Promise<GroupMetadata | undefined>async () => undefinedCache group metadata to prevent redundant requests
shouldSyncHistoryMessage(msg) => booleanskip FULLFilter which history sync types to process
agentAgentundefinedProxy agent for WebSocket connection (e.g. SocksProxyAgent)
fetchAgentAgentundefinedProxy agent for HTTP fetch (media upload/download)

Valid Browser Descriptions

WA rejects unknown or spoofed platform names. Always use the Browsers helpers:

FunctionExampleResult
Browsers.macOS(name)Browsers.macOS('Chrome')['Chrome (Mac OS)', 'Chrome', '10.15.7']
Browsers.windows(name)Browsers.windows('Edge')['Edge (Windows)', 'Edge', '10.0']
Browsers.ubuntu(name)Browsers.ubuntu('Firefox')['Firefox (Ubuntu)', 'Firefox', '22.04']
Browsers.mataram(name)Browsers.mataram('App')['App (Mac OS)', 'App', '1.0.0']
Browsers.appropriate(name)Browsers.appropriate('Bot')Auto-detects OS from process.platform
const sock = makeWASocket({
  auth: state,
  browser: Browsers.macOS('Chrome'),
  emitOwnEvents: true,
  markOnlineOnConnect: true,
  syncFullHistory: true,
})

4. Handle Connection Events

connection.update — All Properties

PropertyTypeDescription
connection'open' | 'connecting' | 'close'Current connection state
lastDisconnect{ error: Boom | Error, date: Date }Error that caused disconnection. Check error.output.statusCode
isNewLoginbooleanWhether this is a fresh login (after successful pairing)
qrstring | undefinedQR code as Base64 string (for QR pairing)
receivedPendingNotificationsboolean | undefinedWhether pending offline notifications have been received
isOnlineboolean | undefinedWhether client is marked online on WA
reachoutTimeLockReachoutTimelockStateAccount restriction info (if restricted)
sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
  if (qr) console.log('QR:', qr)
  if (connection === 'open') console.log('Connected as', sock.user?.id)
  if (connection === 'close') {
    const code = lastDisconnect?.error?.output?.statusCode
    if (code === DisconnectReason.loggedOut) {
      console.log('Session expired. Delete auth/ and re-pair.')
    } else {
      setTimeout(start, 3000)
    }
  }
})

Connection State Flow

  1. connecting — Socket connecting, Noise Protocol handshake in progress
  2. connecting with receivedPendingNotifications: false — Waiting for offline notifications
  3. Server sends CB:ib,,offline — all pending offline notifications delivered
  4. receivedPendingNotifications: true — Ready for sync phase
  5. open — Connection is fully operational, events are flushed
  6. close — Connection terminated. Check lastDisconnect.error for reconnect logic

creds.update — Why You Must Save It

Creds contains all critical data: Signal keys (identity, pre-key, signed pre-key), pairing code, LID mappings, account sync state. If not saved, every restart requires re-pairing. Always attach this listener:

sock.ev.on('creds.update', saveCreds)

saveCreds is a callback from useMultiFileAuthState that writes auth/creds.json. It fires on pairing, key updates, sync completion, and other credential changes.

5. Pairing Code Login

requestPairingCode — Parameters

ParameterTypeDescription
phoneNumberstringTarget phone number. Format: 6281234567890 (no +, no spaces)
customPairingCode (optional)stringCustom 8-character pairing code. Overrides the auto-generated one

Return Value

Returns Promise<string> — the 8-character pairing code to enter in WhatsApp → Linked Devices → Pair a device.

if (!state.creds.registered) {
  const code = await sock.requestPairingCode('6281234567890')
  console.log('Pairing code (8 digits):', code)
}

Custom Pairing Code

const code = await sock.requestPairingCode('6281234567890', 'ABCD1234')

Note: requestPairingCode sets creds.me with the user ID and emits creds.update. After successful pairing, the server restarts the connection (stream error 515) — this is normal behavior.

6. Core Concepts

JID (Jabber ID / WhatsApp ID)

JID is the unique address for every entity on WhatsApp:

TypeJID FormatExample
User (Phone Number)number@s.whatsapp.net6281234567890@s.whatsapp.net
User (LID)id@lid1234567890@lid
Groupid@g.us1234567890-123456@g.us
Newsletterid@newsletter1234567890@newsletter
Status Broadcaststatus@broadcaststatus@broadcast
PSA0@broadcast0@broadcast

Helper functions:

import { jidDecode, jidNormalizedUser, areJidsSameUser, isJidUser, isJidGroup } from '@mataram/wa'

jidDecode('6281234567890@s.whatsapp.net')
// { user: '6281234567890', server: 's.whatsapp.net', device: undefined, agent: undefined }

jidNormalizedUser('6281234567890@s.whatsapp.net.0')
// '6281234567890@s.whatsapp.net' — strips device suffix

areJidsSameUser('a@s.whatsapp.net', 'a@s.whatsapp.net.3')
// true — same user, different device

MessageKey — Structure & How to Obtain

PropertyTypeDescription
idstringUnique message ID (format: BAE5xxxx or 3EBxxxx)
remoteJidstringChat JID where the message lives
fromMebooleanWhether the message is from us
participantstring | undefinedIn groups: sender JID. In 1-on-1: undefined

Use the MessageKey in sendMessage for reply, reaction, edit, delete, pin, and forward. Store the key when you receive a message so you can reference it later.

sock.ev.on('messages.upsert', ({ messages }) => {
  const msg = messages[0]
  const key = msg.key
  // Use key for reply, reaction, edit, delete, pin, forward
})

7. Complete Example

import makeWASocket, { useMultiFileAuthState, Browsers, DisconnectReason } from '@mataram/wa'

const start = async () => {
  const { state, saveCreds } = await useMultiFileAuthState('auth')

  const sock = makeWASocket({
    auth: state,
    browser: Browsers.macOS('Chrome'),
    getMessage: async key => undefined,
  })

  sock.ev.on('creds.update', saveCreds)

  sock.ev.on('connection.update', async ({ connection, lastDisconnect, qr }) => {
    if (qr) console.log('QR:', qr)
    if (connection === 'open') console.log('Connected as', sock.user?.id)
    if (connection === 'close') {
      const code = lastDisconnect?.error?.output?.statusCode
      const shouldReconnect = code !== DisconnectReason.loggedOut
      if (shouldReconnect) start()
    }
  })

  if (!state.creds.registered) {
    const code = await sock.requestPairingCode('6281234567890')
    console.log('Code:', code)
  }

  sock.ev.on('messages.upsert', async ({ messages, type }) => {
    if (type !== 'notify') return
    const msg = messages[0]
    if (msg.key.fromMe) return
    console.log('Message from', msg.key.remoteJid, msg.message?.conversation)
  })
}

start()
← Home Next: Send Messages →