---
name: whatsapp-connect
description: Drop-in WhatsApp connection module (QR code + text/image/video/audio sending) based on the proven marketing-command implementation. Use when adding WhatsApp to any Express project, fixing QR code issues, or setting up WhatsApp messaging with media support.
triggers:
  - "conectar WhatsApp"
  - "QR code WhatsApp"
  - "adicionar WhatsApp"
  - "whatsapp connection"
  - "whatsapp no projeto"
  - "enviar mensagem WhatsApp"
  - "enviar imagem WhatsApp"
  - "enviar video WhatsApp"
  - "enviar audio WhatsApp"
  - "whatsapp nao conecta"
  - "QR code nao aparece"
  - "whatsapp travando"
  - "whatsapp media"
---

# WhatsApp Connect — Drop-in Connection + Media Module

## WHAT THIS SKILL DOES

Adds WhatsApp connectivity to ANY Express/Node.js project with:
- QR code auth via SSE (real-time to frontend)
- Session persistence (scan once, stays connected)
- Send text, image, video, audio, documents
- Broadcast to multiple recipients
- Send to groups by name
- Frontend component (React) for QR display + test messages

## PROVEN IMPLEMENTATION

This is based on the **working production code** from `~/marketing-command/` (comando.iacomtata.com.br) which has been running without issues. The key insight: use `whatsapp-web.js` with `LocalAuth` strategy and Puppeteer headless.

---

## STEP 1: INSTALL DEPENDENCIES

```bash
npm install whatsapp-web.js@^1.34.6 qrcode@^1.5.4 qrcode-terminal@^0.12.0
```

**CRITICAL:** The project MUST have `"type": "module"` in package.json (ESM). If using CommonJS, adjust imports accordingly.

---

## STEP 2: WHATSAPP SERVICE (Backend Singleton)

Create `backend/src/services/whatsapp.js` (or equivalent path in the project):

```javascript
/**
 * WhatsApp Service — Drop-in connection + media module
 * Based on proven marketing-command implementation
 */
import pkg from 'whatsapp-web.js';
const { Client, LocalAuth, MessageMedia } = pkg;
import qrTerminal from 'qrcode-terminal';
import QRCode from 'qrcode';
import { EventEmitter } from 'events';
import { join } from 'path';
import { readFileSync, existsSync } from 'fs';

class WhatsAppService extends EventEmitter {
  constructor() {
    super();
    this.client = null;
    this.isReady = false;
    this.qrCode = null;
    this.qrDataUrl = null;
    this.phoneNumber = null;
    this.initPromise = null;
  }

  /**
   * Initialize WhatsApp client with LocalAuth
   * @param {object} opts
   * @param {string} opts.authPath - Where to store session (default: ~/.{appName}/wwebjs_auth)
   * @param {object} opts.db - Optional better-sqlite3 instance for status tracking
   */
  async initialize(opts = {}) {
    if (this.initPromise) return this.initPromise;

    const authPath = opts.authPath || join(process.env.HOME, '.whatsapp-session', 'wwebjs_auth');

    this.initPromise = new Promise((resolve) => {
      this.client = new Client({
        authStrategy: new LocalAuth({ dataPath: authPath }),
        puppeteer: {
          headless: true,
          args: [
            '--no-sandbox',
            '--disable-setuid-sandbox',
            '--disable-dev-shm-usage',
            '--disable-gpu',
            '--disable-extensions',
          ],
        },
      });

      // QR Code event — fires when no saved session
      this.client.on('qr', async (qr) => {
        this.qrCode = qr;
        this.isReady = false;
        try {
          this.qrDataUrl = await QRCode.toDataURL(qr, { width: 300, margin: 2 });
        } catch { this.qrDataUrl = null; }
        qrTerminal.generate(qr, { small: true });
        console.log('[WhatsApp] QR code generated — scan with your phone');
        this.emit('qr', qr);
      });

      // Ready event — connection established
      this.client.on('ready', () => {
        this.isReady = true;
        this.qrCode = null;
        this.qrDataUrl = null;
        this.phoneNumber = this.client.info?.wid?.user || 'unknown';
        console.log(`[WhatsApp] Connected as ${this.phoneNumber}`);

        // Update DB if provided
        if (opts.db) {
          try {
            opts.db.prepare(
              `UPDATE whatsapp_config SET is_connected = 1, phone_number = ?, updated_at = datetime('now') WHERE id = 1`
            ).run(this.phoneNumber);
          } catch {}
        }

        this.emit('ready');
        resolve(true);
      });

      // Disconnected
      this.client.on('disconnected', (reason) => {
        this.isReady = false;
        this.qrCode = null;
        console.log(`[WhatsApp] Disconnected: ${reason}`);

        if (opts.db) {
          try {
            opts.db.prepare(
              `UPDATE whatsapp_config SET is_connected = 0, updated_at = datetime('now') WHERE id = 1`
            ).run();
          } catch {}
        }

        this.emit('disconnected', reason);
      });

      // Auth failure
      this.client.on('auth_failure', (msg) => {
        console.error('[WhatsApp] Auth failure:', msg);
        this.emit('auth_failure', msg);
      });

      console.log('[WhatsApp] Initializing client...');
      this.client.initialize().catch(err => {
        console.error('[WhatsApp] Init error:', err.message);
        resolve(false);
      });
    });

    return this.initPromise;
  }

  // ─── NUMBER FORMATTING ──────────────────────────────────────
  _formatNumber(number) {
    let clean = number.replace(/[^0-9]/g, '');
    // Auto-prepend Brazil country code if missing
    if (!clean.startsWith('55')) clean = `55${clean}`;
    return `${clean}@c.us`;
  }

  // ─── TEXT MESSAGE ────────────────────────────────────────────
  async sendMessage(number, message) {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chatId = this._formatNumber(number);
    await this.client.sendMessage(chatId, message);
    console.log(`[WhatsApp] Text sent to ${chatId}`);
    return true;
  }

  // ─── IMAGE ───────────────────────────────────────────────────
  /**
   * Send image from file path or URL
   * @param {string} number - Phone number
   * @param {string} source - File path or URL
   * @param {string} [caption] - Optional caption
   */
  async sendImage(number, source, caption = '') {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chatId = this._formatNumber(number);
    let media;
    if (source.startsWith('http://') || source.startsWith('https://')) {
      media = await MessageMedia.fromUrl(source);
    } else if (existsSync(source)) {
      media = MessageMedia.fromFilePath(source);
    } else {
      throw new Error(`Image source not found: ${source}`);
    }
    await this.client.sendMessage(chatId, media, { caption });
    console.log(`[WhatsApp] Image sent to ${chatId}`);
    return true;
  }

  // ─── VIDEO ───────────────────────────────────────────────────
  /**
   * Send video from file path or URL
   * @param {string} number - Phone number
   * @param {string} source - File path or URL
   * @param {string} [caption] - Optional caption
   */
  async sendVideo(number, source, caption = '') {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chatId = this._formatNumber(number);
    let media;
    if (source.startsWith('http://') || source.startsWith('https://')) {
      media = await MessageMedia.fromUrl(source);
    } else if (existsSync(source)) {
      media = MessageMedia.fromFilePath(source);
    } else {
      throw new Error(`Video source not found: ${source}`);
    }
    await this.client.sendMessage(chatId, media, { caption, sendVideoAsGif: false });
    console.log(`[WhatsApp] Video sent to ${chatId}`);
    return true;
  }

  // ─── AUDIO ───────────────────────────────────────────────────
  /**
   * Send audio as voice message (PTT) or regular audio
   * @param {string} number - Phone number
   * @param {string} source - File path or URL (.ogg/.mp3/.wav)
   * @param {boolean} [ptt=true] - Send as voice message (Push-To-Talk)
   */
  async sendAudio(number, source, ptt = true) {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chatId = this._formatNumber(number);
    let media;
    if (source.startsWith('http://') || source.startsWith('https://')) {
      media = await MessageMedia.fromUrl(source);
    } else if (existsSync(source)) {
      media = MessageMedia.fromFilePath(source);
    } else {
      throw new Error(`Audio source not found: ${source}`);
    }
    await this.client.sendMessage(chatId, media, { sendAudioAsVoice: ptt });
    console.log(`[WhatsApp] Audio sent to ${chatId} (ptt=${ptt})`);
    return true;
  }

  // ─── DOCUMENT ────────────────────────────────────────────────
  /**
   * Send any file as document
   * @param {string} number - Phone number
   * @param {string} filePath - Absolute file path
   * @param {string} [filename] - Display filename
   */
  async sendDocument(number, filePath, filename) {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chatId = this._formatNumber(number);
    if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
    const media = MessageMedia.fromFilePath(filePath);
    await this.client.sendMessage(chatId, media, {
      sendMediaAsDocument: true,
      filename: filename || filePath.split('/').pop(),
    });
    console.log(`[WhatsApp] Document sent to ${chatId}`);
    return true;
  }

  // ─── BASE64 MEDIA ───────────────────────────────────────────
  /**
   * Send media from base64 data (useful for generated images/PDFs)
   * @param {string} number - Phone number
   * @param {string} base64Data - Base64-encoded data (without data: prefix)
   * @param {string} mimetype - e.g. 'image/png', 'application/pdf'
   * @param {string} [filename] - Display filename
   * @param {string} [caption] - Optional caption
   */
  async sendBase64(number, base64Data, mimetype, filename = 'file', caption = '') {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chatId = this._formatNumber(number);
    const media = new MessageMedia(mimetype, base64Data, filename);
    await this.client.sendMessage(chatId, media, { caption });
    console.log(`[WhatsApp] Base64 media sent to ${chatId}`);
    return true;
  }

  // ─── GROUP MESSAGE ───────────────────────────────────────────
  async sendToGroup(groupName, message) {
    if (!this.isReady) throw new Error('WhatsApp not connected');
    const chats = await this.client.getChats();
    const group = chats.find(c => c.isGroup && c.name.toLowerCase().includes(groupName.toLowerCase()));
    if (!group) throw new Error(`Group "${groupName}" not found`);
    await this.client.sendMessage(group.id._serialized, message);
    console.log(`[WhatsApp] Message sent to group "${group.name}"`);
    return true;
  }

  // ─── BROADCAST ───────────────────────────────────────────────
  /**
   * Send to multiple recipients with 1s delay between each
   * @param {string[]} numbers - Array of phone numbers
   * @param {string} message - Text message
   * @param {string} [groupName] - Optional group to also send to
   */
  async broadcast(numbers, message, groupName = null) {
    const results = [];
    for (const number of numbers) {
      try {
        await this.sendMessage(number, message);
        results.push({ number, success: true });
      } catch (err) {
        results.push({ number, success: false, error: err.message });
      }
      await new Promise(r => setTimeout(r, 1000));
    }
    if (groupName) {
      try {
        await this.sendToGroup(groupName, message);
        results.push({ group: groupName, success: true });
      } catch (err) {
        results.push({ group: groupName, success: false, error: err.message });
      }
    }
    return results;
  }

  // ─── STATUS ──────────────────────────────────────────────────
  getStatus() {
    return {
      isConnected: this.isReady,
      phoneNumber: this.phoneNumber,
      hasQR: !!this.qrCode,
      qrCode: this.qrCode,
      qrDataUrl: this.qrDataUrl,
    };
  }

  // ─── DESTROY ─────────────────────────────────────────────────
  async destroy() {
    if (this.client) {
      await this.client.destroy();
      this.isReady = false;
      this.client = null;
      this.initPromise = null;
    }
  }
}

// Singleton export
const whatsapp = new WhatsAppService();
export default whatsapp;
```

---

## STEP 3: DATABASE TABLE (if using better-sqlite3)

Add this to the project's database setup:

```javascript
db.exec(`
  CREATE TABLE IF NOT EXISTS whatsapp_config (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    is_connected INTEGER DEFAULT 0,
    phone_number TEXT,
    session_data TEXT,
    updated_at TEXT DEFAULT (datetime('now'))
  );
  INSERT OR IGNORE INTO whatsapp_config (id) VALUES (1);

  CREATE TABLE IF NOT EXISTS recipients (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL,
    phone TEXT NOT NULL,
    type TEXT DEFAULT 'individual',
    created_at TEXT DEFAULT (datetime('now'))
  );
`);
```

---

## STEP 4: SSE ENDPOINT (Real-time QR code to frontend)

Add this to `server.js` BEFORE the static file serving and BEFORE any catch-all route. This endpoint does NOT require auth (QR must be scannable immediately):

```javascript
import whatsapp from './services/whatsapp.js';

// SSE endpoint for WhatsApp QR code + connection status
app.get('/api/whatsapp/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  const sendEvent = (event, data) => {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
  };

  // Send current status immediately
  sendEvent('status', whatsapp.getStatus());

  const onQR = (qr) => sendEvent('qr', { qr, qrDataUrl: whatsapp.qrDataUrl });
  const onReady = () => sendEvent('status', { isConnected: true, phoneNumber: whatsapp.phoneNumber });
  const onDisconnected = (reason) => sendEvent('status', { isConnected: false, reason });

  whatsapp.on('qr', onQR);
  whatsapp.on('ready', onReady);
  whatsapp.on('disconnected', onDisconnected);

  req.on('close', () => {
    whatsapp.off('qr', onQR);
    whatsapp.off('ready', onReady);
    whatsapp.off('disconnected', onDisconnected);
  });
});
```

---

## STEP 5: API ROUTES (Status + Test + Media)

```javascript
import { Router } from 'express';
import whatsapp from '../services/whatsapp.js';
import { authMiddleware } from '../auth.js'; // use project's auth

const router = Router();
router.use(authMiddleware);

// GET /api/whatsapp/status
router.get('/status', (req, res) => {
  res.json(whatsapp.getStatus());
});

// POST /api/whatsapp/send — Send text
router.post('/send', async (req, res) => {
  const { number, message } = req.body;
  if (!number || !message) return res.status(400).json({ error: 'number and message required' });
  try {
    await whatsapp.sendMessage(number, message);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/whatsapp/send-image — Send image
router.post('/send-image', async (req, res) => {
  const { number, source, caption } = req.body;
  if (!number || !source) return res.status(400).json({ error: 'number and source required' });
  try {
    await whatsapp.sendImage(number, source, caption);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/whatsapp/send-video — Send video
router.post('/send-video', async (req, res) => {
  const { number, source, caption } = req.body;
  if (!number || !source) return res.status(400).json({ error: 'number and source required' });
  try {
    await whatsapp.sendVideo(number, source, caption);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/whatsapp/send-audio — Send audio/voice
router.post('/send-audio', async (req, res) => {
  const { number, source, ptt = true } = req.body;
  if (!number || !source) return res.status(400).json({ error: 'number and source required' });
  try {
    await whatsapp.sendAudio(number, source, ptt);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/whatsapp/send-document — Send file
router.post('/send-document', async (req, res) => {
  const { number, filePath, filename } = req.body;
  if (!number || !filePath) return res.status(400).json({ error: 'number and filePath required' });
  try {
    await whatsapp.sendDocument(number, filePath, filename);
    res.json({ success: true });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// POST /api/whatsapp/broadcast — Send to multiple
router.post('/broadcast', async (req, res) => {
  const { numbers, message, groupName } = req.body;
  if (!numbers?.length || !message) return res.status(400).json({ error: 'numbers[] and message required' });
  try {
    const results = await whatsapp.broadcast(numbers, message, groupName);
    res.json({ success: true, results });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

export default router;
```

Register in server.js:
```javascript
import whatsappRoutes from './routes/whatsapp.js';
app.use('/api/whatsapp', whatsappRoutes); // auth-protected routes
// SSE endpoint goes BEFORE this (without auth)
```

---

## STEP 6: INITIALIZE ON SERVER START

In `server.js`, after `app.listen()`:

```javascript
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);

  // Initialize WhatsApp with project-specific auth path
  whatsapp.initialize({
    authPath: join(process.env.HOME, '.YOUR-APP-NAME', 'wwebjs_auth'),
    db: db, // optional: pass better-sqlite3 instance for status tracking
  }).then(ok => {
    if (ok) console.log('WhatsApp ready');
    else console.log('WhatsApp awaiting QR scan');
  });
});
```

**IMPORTANT:** Replace `.YOUR-APP-NAME` with the project's config directory name.

---

## STEP 7: FRONTEND COMPONENT (React)

### API Client functions

```javascript
// In api/client.js or equivalent
export const whatsappApi = {
  getStatus: () => fetch('/api/whatsapp/status', { headers: authHeaders() }).then(r => r.json()),
  sendText: (number, message) => fetch('/api/whatsapp/send', {
    method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
    body: JSON.stringify({ number, message }),
  }).then(r => r.json()),
  sendImage: (number, source, caption) => fetch('/api/whatsapp/send-image', {
    method: 'POST', headers: { ...authHeaders(), 'Content-Type': 'application/json' },
    body: JSON.stringify({ number, source, caption }),
  }).then(r => r.json()),
};
```

### WhatsApp Connection Component

```jsx
import { useState, useEffect } from 'react';

export default function WhatsAppConnection({ apiBaseUrl = '' }) {
  const [status, setStatus] = useState({ isConnected: false });
  const [qrDataUrl, setQrDataUrl] = useState(null);
  const [testNumber, setTestNumber] = useState('');
  const [testMessage, setTestMessage] = useState('');
  const [sending, setSending] = useState(false);

  useEffect(() => {
    // SSE for real-time QR + status
    const es = new EventSource(`${apiBaseUrl}/api/whatsapp/events`);

    es.addEventListener('qr', (e) => {
      try {
        const data = JSON.parse(e.data);
        setQrDataUrl(data.qrDataUrl || null);
        setStatus(prev => ({ ...prev, isConnected: false, hasQR: true }));
      } catch {}
    });

    es.addEventListener('status', (e) => {
      try {
        const data = JSON.parse(e.data);
        setStatus(data);
        if (data.isConnected) setQrDataUrl(null);
      } catch {}
    });

    return () => es.close();
  }, [apiBaseUrl]);

  const handleSendTest = async () => {
    if (!testNumber.trim()) return alert('Informe o numero');
    setSending(true);
    try {
      const res = await fetch(`${apiBaseUrl}/api/whatsapp/send`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ number: testNumber, message: testMessage || 'Teste WhatsApp' }),
      });
      const data = await res.json();
      if (data.success) alert('Mensagem enviada!');
      else alert('Erro: ' + data.error);
    } catch (err) {
      alert('Erro: ' + err.message);
    }
    setSending(false);
  };

  return (
    <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
      {/* Header */}
      <div className="flex items-center justify-between mb-6">
        <div>
          <h2 className="text-lg font-semibold text-slate-800">WhatsApp</h2>
          <p className="text-sm text-slate-500">
            {status.isConnected ? `Conectado: ${status.phoneNumber}` : 'Desconectado'}
          </p>
        </div>
        <span className={`px-3 py-1.5 rounded-full text-xs font-medium ${
          status.isConnected ? 'bg-emerald-100 text-emerald-700' : 'bg-red-100 text-red-700'
        }`}>
          {status.isConnected ? 'Online' : 'Offline'}
        </span>
      </div>

      {/* QR Code Display */}
      {!status.isConnected && qrDataUrl && (
        <div className="bg-slate-50 rounded-xl p-6 flex flex-col items-center mb-6">
          <p className="text-sm font-medium text-slate-700 mb-4">
            Escaneie o QR Code com seu WhatsApp
          </p>
          <div className="bg-white p-4 rounded-2xl shadow-sm border border-slate-200">
            <img src={qrDataUrl} alt="WhatsApp QR Code" className="w-[280px] h-[280px]" />
          </div>
          <p className="text-xs text-slate-400 mt-3">
            WhatsApp &gt; Configuracoes &gt; Aparelhos conectados &gt; Conectar
          </p>
        </div>
      )}

      {/* Waiting for QR */}
      {!status.isConnected && !qrDataUrl && (
        <div className="bg-slate-50 rounded-xl p-6 text-center mb-6">
          <p className="text-sm text-slate-600">Aguardando QR Code...</p>
          <p className="text-xs text-slate-400 mt-1">O servidor esta inicializando o WhatsApp</p>
        </div>
      )}

      {/* Test Message (when connected) */}
      {status.isConnected && (
        <div className="bg-slate-50 rounded-xl p-5">
          <h3 className="text-sm font-medium text-slate-700 mb-3">Enviar Mensagem de Teste</h3>
          <div className="space-y-3">
            <input
              type="text"
              value={testNumber}
              onChange={e => setTestNumber(e.target.value)}
              placeholder="5515988410337"
              className="w-full px-4 py-2.5 text-sm border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-violet-500 bg-white"
            />
            <input
              type="text"
              value={testMessage}
              onChange={e => setTestMessage(e.target.value)}
              placeholder="Teste WhatsApp"
              className="w-full px-4 py-2.5 text-sm border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-violet-500 bg-white"
            />
            <button
              onClick={handleSendTest}
              disabled={sending}
              className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium text-white bg-violet-600 hover:bg-violet-700 rounded-xl transition-colors disabled:opacity-50"
            >
              {sending ? 'Enviando...' : 'Enviar Teste'}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
```

---

## COMMON ISSUES & FIXES

### QR Code not appearing
1. **Check Puppeteer:** Ensure `chromium` or `chrome` is installed. On VPS: `apt install -y chromium-browser`
2. **Memory:** whatsapp-web.js needs ~150MB RAM. Check with `pm2 monit`
3. **Auth path permissions:** Ensure the auth directory is writable
4. **Port conflict:** Only ONE WhatsApp instance per phone number across all servers

### QR Code appears but scan fails
1. **Phone internet:** Ensure phone has stable internet
2. **Time sync:** Server clock must be synchronized (`timedatectl`)
3. **Stale session:** Delete the auth folder and restart: `rm -rf ~/.your-app/wwebjs_auth && pm2 restart your-app`

### Disconnections
- whatsapp-web.js does NOT auto-reconnect. After disconnect:
  - The `disconnected` event fires
  - To reconnect: restart the server process (`pm2 restart`)
  - Consider adding auto-restart in PM2 config

### VPS Puppeteer dependencies (Ubuntu/Debian)
```bash
apt install -y chromium-browser libgbm1 libasound2 libatk1.0-0 libcups2 libdbus-1-3 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libpango-1.0-0 libcairo2 libnss3
```

### Multiple WhatsApp instances
Each project MUST use a DIFFERENT auth path. Never share the same `wwebjs_auth` folder between projects. Each project = different phone number OR different session.

---

## USAGE EXAMPLES (from within any agent/service)

```javascript
import whatsapp from './services/whatsapp.js';

// Text
await whatsapp.sendMessage('5515988410337', 'Hello from my app!');

// Image from URL
await whatsapp.sendImage('5515988410337', 'https://example.com/photo.jpg', 'Look at this!');

// Image from file
await whatsapp.sendImage('5515988410337', '/tmp/report.png', 'Daily report');

// Video
await whatsapp.sendVideo('5515988410337', '/tmp/tutorial.mp4', 'Watch this');

// Audio as voice message
await whatsapp.sendAudio('5515988410337', '/tmp/audio.ogg', true);

// Audio as regular file
await whatsapp.sendAudio('5515988410337', '/tmp/music.mp3', false);

// PDF document
await whatsapp.sendDocument('5515988410337', '/tmp/report.pdf', 'relatorio-mensal.pdf');

// Base64 (useful for generated content)
const pngBase64 = readFileSync('/tmp/chart.png').toString('base64');
await whatsapp.sendBase64('5515988410337', pngBase64, 'image/png', 'chart.png', 'Performance chart');

// Broadcast text to multiple
await whatsapp.broadcast(['5515988410337', '5515988284219'], 'Announcement!');

// Group message
await whatsapp.sendToGroup('Team Marketing', 'Meeting in 10 minutes');

// Check status
const status = whatsapp.getStatus(); // { isConnected, phoneNumber, hasQR, qrDataUrl }
```

---

## CHECKLIST FOR NEW PROJECT

When adding WhatsApp to any new Express project:

- [ ] `npm install whatsapp-web.js@^1.34.6 qrcode@^1.5.4 qrcode-terminal@^0.12.0`
- [ ] Copy `whatsapp.js` service file
- [ ] Add `whatsapp_config` table to database (if using SQLite)
- [ ] Add SSE endpoint `/api/whatsapp/events` in server.js (NO auth)
- [ ] Add WhatsApp API routes (with auth)
- [ ] Initialize in `app.listen()` callback with unique auth path
- [ ] Add `WhatsAppConnection` component to frontend
- [ ] On VPS: install Puppeteer dependencies
- [ ] Test: start server → scan QR → send test message
- [ ] Verify session persists after restart (no QR needed on second boot)
