Complete guide to building a WhatsApp bot with @mataram/wa — from installation to pairing.
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
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.
| Param | Type | Description |
|---|---|---|
folder | string | Directory path to store auth files. Example: 'auth' |
| Property | Type | Description |
|---|---|---|
state | AuthenticationState | Contains 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.
| Option | Type | Default | Description |
|---|---|---|---|
auth | AuthenticationState | (required) | Auth state from useMultiFileAuthState or custom implementation |
browser | [string, string, string] | macOS Chrome | Browser description. WhatsApp rejects unknown platform names |
waWebSocketUrl | string | URL | wss://web.whatsapp.com/ws/chat | WebSocket URL for WA connection |
connectTimeoutMs | number | 20000 | Connection timeout in ms |
defaultQueryTimeoutMs | number | undefined | 60000 | Default timeout for queries in ms |
keepAliveIntervalMs | number | 30000 | Ping interval to server in ms |
logger | ILogger | pino child | Pino-compatible logger |
emitOwnEvents | boolean | true | Emit events for own socket actions (e.g., sent messages) |
markOnlineOnConnect | boolean | true | Mark client as online on successful connection |
syncFullHistory | boolean | true | Request full history sync from primary device |
fireInitQueries | boolean | true | Automatically execute initialization queries on connection open |
countryCode | string | 'US' | Two-letter country code for registration |
linkPreviewImageThumbnailWidth | number | 192 | Width for link preview thumbnails |
generateHighQualityLinkPreview | boolean | false | Upload thumbnail to WA for high-quality link previews |
maxMsgRetryCount | number | 5 | Maximum retry attempts for failed messages |
enableRecentMessageCache | boolean | true | Cache recent messages for retry handling |
getMessage | (key) => Promise<proto.IMessage | undefined> | async () => undefined | Fetch message from your store for retry logic |
cachedGroupMetadata | (jid) => Promise<GroupMetadata | undefined> | async () => undefined | Cache group metadata to prevent redundant requests |
shouldSyncHistoryMessage | (msg) => boolean | skip FULL | Filter which history sync types to process |
agent | Agent | undefined | Proxy agent for WebSocket connection (e.g. SocksProxyAgent) |
fetchAgent | Agent | undefined | Proxy agent for HTTP fetch (media upload/download) |
WA rejects unknown or spoofed platform names. Always use the Browsers helpers:
| Function | Example | Result |
|---|---|---|
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, })
| Property | Type | Description |
|---|---|---|
connection | 'open' | 'connecting' | 'close' | Current connection state |
lastDisconnect | { error: Boom | Error, date: Date } | Error that caused disconnection. Check error.output.statusCode |
isNewLogin | boolean | Whether this is a fresh login (after successful pairing) |
qr | string | undefined | QR code as Base64 string (for QR pairing) |
receivedPendingNotifications | boolean | undefined | Whether pending offline notifications have been received |
isOnline | boolean | undefined | Whether client is marked online on WA |
reachoutTimeLock | ReachoutTimelockState | Account 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) } } })
connecting — Socket connecting, Noise Protocol handshake in progressconnecting with receivedPendingNotifications: false — Waiting for offline notificationsCB:ib,,offline — all pending offline notifications deliveredreceivedPendingNotifications: true — Ready for sync phaseopen — Connection is fully operational, events are flushedclose — Connection terminated. Check lastDisconnect.error for reconnect logicCreds 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.
| Parameter | Type | Description |
|---|---|---|
phoneNumber | string | Target phone number. Format: 6281234567890 (no +, no spaces) |
customPairingCode (optional) | string | Custom 8-character pairing code. Overrides the auto-generated one |
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) }
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.
JID is the unique address for every entity on WhatsApp:
| Type | JID Format | Example |
|---|---|---|
| User (Phone Number) | number@s.whatsapp.net | 6281234567890@s.whatsapp.net |
| User (LID) | id@lid | 1234567890@lid |
| Group | id@g.us | 1234567890-123456@g.us |
| Newsletter | id@newsletter | 1234567890@newsletter |
| Status Broadcast | status@broadcast | status@broadcast |
| PSA | 0@broadcast | 0@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
| Property | Type | Description |
|---|---|---|
id | string | Unique message ID (format: BAE5xxxx or 3EBxxxx) |
remoteJid | string | Chat JID where the message lives |
fromMe | boolean | Whether the message is from us |
participant | string | undefined | In 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 })
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()