WA Sender - Service Otomatisasi Pesan WhatsApp

12 Oktober 2025Microservice"Node.js", "Express", "dotenv", "whatsapp-web.js", "qrcode-terminal"
WA Sender - Service Otomatisasi Pesan WhatsApp

Deskripsi Proyek: Engine WhatsApp Otomatis

Proyek ini adalah sebuah backend service (engine) yang dibangun menggunakan **Node.js** dan **Express** untuk mengirim pesan dan file WhatsApp secara otomatis. Engine ini dirancang dengan fokus pada stabilitas dan keamanan, menggunakan standar modern seperti **ES Modules**. Untuk meminimalisir risiko pemblokiran oleh WhatsApp, engine ini tidak mengirim pesan secara langsung, melainkan menggunakan **sistem antrian (queue) sederhana dengan jeda waktu acak** antar pengiriman untuk mensimulasikan perilaku manusia. Selain itu, semua endpoint API diamankan dengan **API Key** untuk mencegah akses yang tidak sah.

Fitur Utama

Struktur Proyek: package.json
{
  "name": "wa-engine",
  "version": "1.0.0",
  "description": "Engine WhatsApp otomatis dengan keamanan dan antrian anti-ban",
  "main": "app.js",
  "type": "module",
  "scripts": {
    "start": "node app.js"
  },
  "keywords": [
    "whatsapp",
    "nodejs",
    "express",
    "api"
  ],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "qrcode-terminal": "^0.12.0",
    "whatsapp-web.js": "^1.23.0"
  }
}
Konfigurasi: .env.example
# Port untuk server Express
PORT=3000

# Kunci API rahasia untuk mengamankan endpoint
API_KEY=GANTI_DENGAN_KUNCI_RAHASIA_ANDA
Inisialisasi Client: whatsapp.js
import wweb from 'whatsapp-web.js';
const { Client, LocalAuth, MessageMedia } = wweb;
import qrcode from 'qrcode-terminal';

console.log("Mempersiapkan client WhatsApp...");

const client = new Client({
    authStrategy: new LocalAuth(),
    puppeteer: {
        headless: true,
        args: ['--no-sandbox', '--disable-setuid-sandbox']
    }
});

client.on('qr', (qr) => {
    console.log('QR CODE DITERIMA, SILAKAN SCAN:');
    qrcode.generate(qr, { small: true });
});

client.on('ready', () => {
    console.log('Client WhatsApp sudah siap!');
});

client.on('auth_failure', msg => {
    console.error('AUTENTIKASI GAGAL', msg);
});

client.initialize();

export { client, MessageMedia };
Strategi Anti-Ban: queue.js
import { client, MessageMedia } from './whatsapp.js';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const messageQueue = [];
let isProcessing = false;

const processQueue = async () => {
    if (isProcessing || messageQueue.length === 0) return;
    
    isProcessing = true;
    const job = messageQueue.shift();
    console.log(`[Queue] Memproses pekerjaan untuk ${job.data.chatId}...`);

    try {
        if (job.type === 'message') {
            await client.sendMessage(job.data.chatId, job.data.message);
        } else if (job.type === 'file') {
            const filePath = path.join(__dirname, 'public', 'files', job.data.filename);
            const media = MessageMedia.fromFilePath(filePath);
            await client.sendMessage(job.data.chatId, media, { caption: job.data.caption });
        }
        console.log(`[Queue] Pekerjaan untuk ${job.data.chatId} berhasil.`);
    } catch (error) {
        console.error(`[Queue] Gagal memproses:`, error.message);
    }

    const randomDelay = Math.floor(Math.random() * (7000 - 2000 + 1)) + 2000;
    console.log(`[Queue] Menunggu ${randomDelay / 1000} detik...`);
    
    setTimeout(() => {
        isProcessing = false;
        processQueue();
    }, randomDelay);
};

export const addToQueue = (job) => {
    messageQueue.push(job);
    console.log(`[Queue] Pekerjaan baru ditambahkan. Total antrian: ${messageQueue.length}`);
    processQueue();
};
Keamanan API: middleware/auth.js
export const apiKeyAuth = (req, res, next) => {
    const apiKey = req.headers['x-api-key'];

    if (!apiKey) {
        return res.status(401).json({ status: 'error', message: 'Akses ditolak: API Key tidak ada' });
    }

    if (apiKey !== process.env.API_KEY) {
        return res.status(403).json({ status: 'error', message: 'Akses ditolak: API Key tidak valid' });
    }

    next();
};
Logika Pengiriman: controllers/messageController.js
import { client } from '../whatsapp.js';
import { addToQueue } from '../queue.js';

const formatNumber = (number) => {
    if (number.startsWith('0')) {
        return `62${number.slice(1)}@c.us`;
    } else if (number.startsWith('62')) {
        return `${number}@c.us`;
    }
    return `${number}@c.us`;
}

export const sendMessage = async (req, res) => {
    const { number, message } = req.body;
    if (!number || !message) {
        return res.status(400).json({ status: 'error', message: 'Nomor dan pesan wajib diisi' });
    }

    const chatId = formatNumber(number);

    try {
        const userExists = await client.isRegisteredUser(chatId);
        if (!userExists) {
            return res.status(404).json({ status: 'error', message: 'Nomor tidak terdaftar di WhatsApp' });
        }

        addToQueue({ type: 'message', data: { chatId, message } });
        
        res.status(202).json({ status: 'success', message: 'Pesan telah dimasukkan ke dalam antrian untuk dikirim.' });
    } catch (error) {
        console.error(error);
        res.status(500).json({ status: 'error', message: 'Gagal memvalidasi nomor', error: error.message });
    }
};

export const sendFile = async (req, res) => {
    const { number, caption, filename } = req.body;
    if (!number || !filename) {
        return res.status(400).json({ status: 'error', message: 'Nomor dan nama file wajib diisi' });
    }

    const chatId = formatNumber(number);
    
    try {
        const userExists = await client.isRegisteredUser(chatId);
        if (!userExists) {
            return res.status(404).json({ status: 'error', message: 'Nomor tidak terdaftar di WhatsApp' });
        }

        addToQueue({ type: 'file', data: { chatId, caption: caption || '', filename } });
        
        res.status(202).json({ status: 'success', message: 'File telah dimasukkan ke dalam antrian untuk dikirim.' });
    } catch (error) {
        console.error(error);
        res.status(500).json({ status: 'error', message: 'Gagal memvalidasi nomor', error: error.message });
    }
};
Definisi Endpoint: routes/messageRoutes.js
import express from 'express';
import { sendMessage, sendFile } from '../controllers/messageController.js';

const router = express.Router();

router.post('/send-message', sendMessage);
router.post('/send-file', sendFile);

export default router;
Server Utama: app.js
import 'dotenv/config';
import express from 'express';
import './whatsapp.js';
import messageRoutes from './routes/messageRoutes.js';
import { apiKeyAuth } from './middleware/auth.js';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.get('/', (req, res) => {
    res.send('<h1>WhatsApp Engine API (v2)</h1><p>Status: Running</p>');
});

// Semua rute di bawah /api akan dilindungi oleh middleware apiKeyAuth
app.use('/api', apiKeyAuth, messageRoutes);

app.listen(PORT, () => {
    console.log(`Server berjalan di port ${PORT}`);
    console.log('Tunggu client WhatsApp siap sebelum mengirim pesan...');
});
Contoh Penggunaan (REST Client / .http)
# Definisikan variabel untuk penggunaan berulang
@hostname = http://localhost:3000
@api_path = /api
@target_number = 6281234567890
@api_key = GANTI_DENGAN_KUNCI_RAHASIA_ANDA

### Kirim Pesan Teks
POST {{hostname}}{{api_path}}/send-message
Content-Type: application/json
x-api-key: {{api_key}}

{
    "number": "{{target_number}}",
    "message": "Halo! Ini pesan dari engine yang aman. 😎"
}

### Kirim File (pastikan file ada di folder public/files)
POST {{hostname}}{{api_path}}/send-file
Content-Type: application/json
x-api-key: {{api_key}}

{
    "number": "{{target_number}}",
    "caption": "Ini adalah file yang dikirim dengan jeda acak.",
    "filename": "contoh-gambar.jpg"
}

Tips Keamanan & Praktik Terbaik

- **Jangan Spam**: Gunakan engine ini secara bertanggung jawab. Kirim pesan hanya kepada kontak yang sudah setuju (opt-in) untuk menerima pesan dari Anda. - **Variasi Pesan**: Jika mengirim pesan massal, variasikan isi pesan (misalnya dengan menyisipkan nama) untuk menghindari deteksi spam. - **Pemanasan Nomor (Warm-up)**: Jika menggunakan nomor WhatsApp baru, jangan langsung mengirim ratusan pesan. Mulailah dari puluhan, lalu tingkatkan secara bertahap setiap harinya. - **Gunakan PM2**: Untuk menjalankan aplikasi di production, gunakan manajer proses seperti `pm2` agar aplikasi tetap berjalan stabil dan otomatis restart jika terjadi crash. - **Backup Sesi**: Folder `.wwebjs_auth` berisi sesi login Anda. Backup folder ini secara berkala agar Anda tidak perlu sering scan QR code. - **Alternatif Resmi**: Untuk kebutuhan bisnis skala besar dan kritis, sangat disarankan untuk menggunakan **WhatsApp Business Platform API (Official)** yang lebih stabil dan terjamin.

Hasil Eksperimen

Eksperimen berjalan stabil dengan penerapan jeda acak antar pesan, terbukti efektif mencegah blokir nomor. Sistem berhasil diuji di lingkungan produksi (VPS) menggunakan PM2, menunjukkan kinerja yang andal dan efisien. Seluruh fungsi, termasuk pengiriman teks dan file, berjalan tanpa error. Sesi login tetap terjaga bahkan setelah server restart, menghilangkan kebutuhan untuk scan QR berulang. Engine ini terbukti menjadi solusi yang solid untuk otomatisasi skala kecil hingga menengah.