Skip to main content

Documentation Index

Fetch the complete documentation index at: https://zapo.to/llms.txt

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

This guide builds a minimal “ping → pong” bot. It connects, prints a QR code to pair, and replies pong whenever it receives ping.
1

Install the packages

npm install zapo-js @zapo-js/store-sqlite better-sqlite3 pino pino-pretty
2

Create the store and client

The store persists auth and Signal state. Here we use SQLite, writing to .auth/state.sqlite.
import { createPinoLogger, createStore, WaClient } from 'zapo-js'
import { createSqliteStore } from '@zapo-js/store-sqlite'

const logger = await createPinoLogger({ level: 'info', pretty: true })

const store = createStore({
  backends: {
    sqlite: createSqliteStore({ path: '.auth/state.sqlite', driver: 'auto' })
  },
  providers: {
    auth: 'sqlite',
    signal: 'sqlite',
    senderKey: 'sqlite',
    appState: 'sqlite',
    messages: 'sqlite',
    threads: 'sqlite',
    contacts: 'sqlite',
    privacyToken: 'sqlite'
  }
})

const client = new WaClient(
  {
    store,
    sessionId: 'default',
    connectTimeoutMs: 15_000,
    nodeQueryTimeoutMs: 30_000,
    history: { enabled: true, requireFullSync: true }
  },
  logger
)
3

Handle pairing

On a fresh session the client emits auth_qr. Render the value as a QR code (for example with the qrcode-terminal package) and scan it from WhatsApp → Linked devices.
client.on('auth_qr', ({ qr, ttlMs }) => {
  console.log('Scan this QR within', ttlMs, 'ms:')
  console.log(qr)
})

client.on('auth_paired', ({ credentials }) => {
  console.log('Paired as', credentials.meJid)
})
Prefer an 8-digit pairing code instead of a QR? See Authentication.
4

Reply to incoming messages

Listen for the message event and send a reply with client.message.send.
function extractText(message) {
  return (
    message?.conversation ??
    message?.extendedTextMessage?.text ??
    undefined
  )
}

client.on('message', async (event) => {
  const text = extractText(event.message)
  if (text?.trim().toLowerCase() !== 'ping') return

  const to = event.chatJid ?? event.senderJid
  if (!to) return

  await client.message.send(to, 'pong')
})
5

Connect

await client.connect()
connect() resolves once the socket is open. The first connection drives pairing; subsequent connections reuse the stored credentials.

Full example

import { createPinoLogger, createStore, WaClient } from 'zapo-js'
import { createSqliteStore } from '@zapo-js/store-sqlite'

const logger = await createPinoLogger({ level: 'info', pretty: true })

const store = createStore({
  backends: {
    sqlite: createSqliteStore({ path: '.auth/state.sqlite', driver: 'auto' })
  },
  providers: {
    auth: 'sqlite',
    signal: 'sqlite',
    senderKey: 'sqlite',
    appState: 'sqlite',
    messages: 'sqlite',
    threads: 'sqlite',
    contacts: 'sqlite',
    privacyToken: 'sqlite'
  }
})

const client = new WaClient({ store, sessionId: 'default' }, logger)

client.on('auth_qr', ({ qr }) => console.log(qr))
client.on('connection', (event) => console.log('connection:', event.status, event.reason))

client.on('message', async (event) => {
  const text =
    event.message?.conversation ?? event.message?.extendedTextMessage?.text
  if (text?.trim().toLowerCase() !== 'ping') return
  const to = event.chatJid ?? event.senderJid
  if (to) await client.message.send(to, 'pong')
})

await client.connect()

What’s next

Sending messages

Replies, mentions, link previews, and the full content union.

Receiving messages

Parse incoming events, send receipts, and decrypt addons.

Reconnection

zapo does not auto-reconnect — here’s the pattern to handle it.

Configuration

Sessions, history sync, timeouts, proxy, and more.
Last modified on May 27, 2026