Simple WhatsApp OTP Backend
Backend WhatsApp OTP ringan untuk aplikasi internal delivery project. Project ini memakai Node.js, Express, dan @whiskeysockets/baileys tanpa Puppeteer, tanpa Chromium, tanpa browser/headless Chrome, dan tanpa whatsapp-web.js.
Fitur
- Login WhatsApp via QR dari log aplikasi
- Dashboard web di halaman utama dengan password admin
- QR tampil langsung di dashboard setelah password benar
- Form test kirim pesan dari dashboard saat WhatsApp sudah terhubung
- Session permanen memakai Baileys multi-file auth state di folder
sessions/ - Session Guard dengan backup otomatis ke folder
session-backups/ - Auto reconnect saat koneksi putus, dengan session lock dan cooldown anti reconnect loop
- REST API Express dengan API key sederhana
- OTP 6 digit, expired 5 menit
- Rate limit OTP per nomor
- Validasi nomor Indonesia otomatis:
08xxxxmenjadi628xxxx - Logging sederhana dengan Pino
- Cocok untuk Hostinger shared hosting yang mendukung Node.js
- PM2 config tetap tersedia jika nanti dipindah ke VPS
Struktur Folder
.
|-- server.js
|-- package.json
|-- package-lock.json
|-- ecosystem.config.js
|-- .env.example
|-- scripts/
|-- src/
| |-- config/
| |-- controllers/
| |-- middleware/
| |-- routes/
| |-- services/
| `-- utils/
`-- sessions/
Folder sessions/ dibuat otomatis saat bot dijalankan dan tidak boleh dihapus kecuali ingin login ulang.
Catatan Penting untuk Shared Hosting
Di Hostinger shared hosting, biasanya Anda tidak perlu dan tidak bisa setup PM2, Nginx, systemd, atau Certbot manual. Jalankan app lewat menu Node.js di hPanel.
Pastikan paket hosting Anda menyediakan Node.js versi 20 atau lebih baru. Baileys terbaru membutuhkan Node.js >=20.0.0. Jika hPanel hanya menyediakan Node.js versi lebih rendah, app ini tidak akan jalan stabil dan perlu upgrade paket/fitur Node.js atau pindah ke VPS.
Environment
Copy file environment jika Anda deploy via SSH:
cp .env.example .env
nano .env
Jika memakai hPanel Node.js App, Anda bisa isi variable dari menu Environment Variables.
Minimal isi yang wajib:
NODE_ENV=production
PORT=3000
API_KEY=ganti-dengan-secret-yang-panjang
ADMIN_PASSWORD=ganti-dengan-password-dashboard
SESSION_DIR=./sessions
SESSION_BACKUP_DIR=./session-backups
SESSION_BACKUP_ENABLED=true
SESSION_BACKUP_INTERVAL_MINUTES=30
SESSION_BACKUP_KEEP=10
SESSION_LOCK_ENABLED=true
SESSION_LOCK_STALE_MINUTES=10
CORS_ORIGIN=*
Catatan: beberapa shared hosting mengatur PORT otomatis. Jika Hostinger memberi port sendiri di hPanel, gunakan port dari hPanel atau biarkan environment Hostinger yang menentukan.
Deploy di Hostinger Shared Hosting
1. Upload Project
Upload semua file project ke folder aplikasi Node.js, misalnya:
/home/username/domains/wa.example.com/nodeapp/
Jangan upload folder node_modules/. Folder itu dibuat ulang oleh npm install.
File yang perlu ada di hosting:
server.js
package.json
package-lock.json
src/
scripts/
.env
.env.example
ecosystem.config.js
README.md
2. Buat Node.js App di hPanel
Di Hostinger hPanel:
1. Buka website/domain Anda 2. Masuk ke menu Advanced atau Node.js 3. Klik Create Application 4. Pilih Node.js versi 20 atau lebih baru 5. Application mode: Production 6. Application root: folder project, contoh nodeapp 7. Application startup file: server.js 8. Application URL: domain/subdomain yang dipakai, contoh https://wa.example.com
3. Set Environment Variables
Tambahkan variable berikut di hPanel Node.js App atau lewat file .env:
NODE_ENV=production
API_KEY=ganti-dengan-secret-yang-panjang
ADMIN_PASSWORD=ganti-dengan-password-dashboard
SESSION_DIR=./sessions
SESSION_BACKUP_DIR=./session-backups
SESSION_BACKUP_ENABLED=true
SESSION_BACKUP_INTERVAL_MINUTES=30
SESSION_BACKUP_KEEP=10
SESSION_LOCK_ENABLED=true
SESSION_LOCK_STALE_MINUTES=10
WHATSAPP_BROWSER_NAME=Internal Delivery Bot
OTP_EXPIRES_MINUTES=5
OTP_RATE_LIMIT_WINDOW_MINUTES=15
OTP_RATE_LIMIT_MAX=3
OTP_VERIFY_MAX_ATTEMPTS=5
REQUEST_BODY_LIMIT=1mb
LOG_LEVEL=info
BAILEYS_LOG_LEVEL=silent
RECONNECT_BASE_DELAY_MS=3000
RECONNECT_MAX_DELAY_MS=60000
RECONNECT_STABLE_AFTER_MS=60000
RECONNECT_UNSTABLE_THRESHOLD=5
RECONNECT_COOLDOWN_MS=300000
RECONNECT_ON_CONNECTION_REPLACED=false
4. Install Dependency
Di hPanel Node.js App, klik tombol Run NPM Install jika tersedia.
Jika Anda punya SSH:
cd ~/domains/wa.example.com/nodeapp
npm install
npm run check
5. Start atau Restart App
Di hPanel Node.js App, klik Start atau Restart.
Jika memakai SSH dan Hostinger menyediakan command app manager, tetap prioritaskan tombol restart dari hPanel karena proses Node.js shared hosting dikelola oleh Hostinger.
Dashboard Web
Buka domain/subdomain app:
https://wa.example.com/
Halaman awal akan meminta password admin dari environment variable:
ADMIN_PASSWORD=ganti-dengan-password-dashboard
Setelah password benar, dashboard akan menampilkan:
- Status koneksi WhatsApp
- QR login jika session belum tersambung
- Info user WhatsApp jika sudah
connected - Session Guard untuk melihat jumlah file session, backup manual, dan restore backup
- Form test kirim pesan ke nomor Indonesia
- Baileys Toolbox GUI untuk memilih kategori, memilih action, mengisi parameter, dan menjalankan action advanced
Password admin hanya untuk dashboard. Endpoint REST utama tetap memakai API_KEY.
Jika butuh health check JSON, gunakan:
https://wa.example.com/health
Baileys Toolbox
Backend ini juga punya toolbox advanced berbasis GUI dan REST API berdasarkan dokumentasi Baileys serta type definitions package yang terpasang. Toolbox ini berguna untuk operator internal yang ingin memakai kemampuan Baileys selain OTP.
Endpoint dengan API key:
GET /baileys/capabilities
POST /baileys/action
Endpoint dashboard dengan password admin:
GET /admin/api/baileys/capabilities
POST /admin/api/baileys/action
Kategori action yang tersedia:
- Account dan login pairing code
- Messages: text, media URL, document, sticker, location, contact, poll, reaction, delete, disappearing mode
- Presence dan read receipts
- Profile dan privacy
- Group management
- Business profile/catalog
- Newsletter/channel
- Chat modification, contacts, dan call link
Dokumentasi lengkap payload ada di BAILEYS_TOOLBOX.md.
Di dashboard, semua action tersebut muncul sebagai daftar kategori dan action. Parameter sederhana menjadi field GUI otomatis, sedangkan parameter kompleks seperti array/object tetap memakai textarea JSON agar cocok dengan struktur asli Baileys.
Cara Scan QR di Shared Hosting
Cara paling mudah adalah buka dashboard di /, masukkan ADMIN_PASSWORD, lalu scan QR yang tampil di halaman.
QR juga tetap dicetak ke log aplikasi karena Baileys memakai printQRInTerminal: true.
Di hPanel:
1. Buka menu Node.js App 2. Pilih aplikasi bot ini 3. Buka Application Logs atau Logs 4. Cari QR WhatsApp yang muncul saat app pertama kali start 5. Scan dari WhatsApp:
WhatsApp > Settings > Linked Devices > Link a Device
Setelah berhasil scan, session tersimpan di folder sessions/. Restart normal tidak perlu scan QR lagi.
Agar Session Tidak Hilang
Session WhatsApp disimpan di folder:
sessions/
Backup otomatis disimpan di:
session-backups/
Jangan hapus kedua folder ini saat upload ulang project ke Hostinger. Kalau upload lewat File Manager, upload file source saja dan biarkan folder sessions/ serta session-backups/ tetap ada.
Dashboard punya panel Session Guard untuk:
- melihat jumlah file session
- melihat session lock dan PID proses yang sedang memakai session
- melihat backup yang tersedia
- membuat backup manual
- restore backup jika session rusak atau terhapus
Setelah restore backup, restart aplikasi dari hPanel agar Baileys reconnect memakai session yang dipulihkan.
Jika Bot Down Up Terus
Kalau status terlihat down, lalu up 1 detik, lalu down lagi terus-menerus, penyebab paling sering adalah dua proses Node.js memakai folder sessions/ yang sama, atau WhatsApp menganggap koneksi lama digantikan koneksi baru.
App ini punya pengaman:
SESSION_LOCK_ENABLED=truemembuat file locksessions/.wa-session.lock- status
lockedberarti ada proses lain yang masih memakai session - status
connection_replacedberarti WhatsApp menutup koneksi karena session dipakai koneksi lain - status
cooldownberarti koneksi terlalu sering putus cepat, jadi bot menunggu sebelum reconnect lagi
Langkah perbaikan di Hostinger shared hosting:
1. Buka hPanel > Node.js App 2. Klik Stop 3. Tunggu 10-20 detik sampai proses lama benar-benar mati 4. Klik Start 5. Buka dashboard dan cek panel Status serta Session Guard
Jangan menjalankan app yang sama dari hPanel dan SSH secara bersamaan. Satu nomor WhatsApp dan satu folder sessions/ hanya boleh dipakai oleh satu proses aktif.
Jika QR tidak muncul:
1. Cek endpoint /status 2. Pastikan status bukan open 3. Restart app dari hPanel 4. Buka logs lagi 5. Jika status logged_out, hapus folder sessions/, lalu restart app
Restart Bot di Shared Hosting
Restart normal:
hPanel > Node.js App > Restart
Login ulang dari nol:
hPanel > Node.js App > Stop
hapus folder sessions/
hPanel > Node.js App > Start
buka logs dan scan QR baru
Jika punya SSH:
cd ~/domains/wa.example.com/nodeapp
rm -rf sessions
Lalu restart dari hPanel.
Melihat Logs di Shared Hosting
Gunakan:
hPanel > Node.js App > Logs
Jika tersedia SSH, lokasi log bisa berbeda tergantung konfigurasi Hostinger. Cara paling aman tetap lewat hPanel.
SSL dan Domain di Hostinger Shared Hosting
Untuk shared hosting, SSL tidak perlu di-setup dengan Certbot. Aktifkan dari hPanel:
hPanel > Websites > Manage > Security > SSL
Pastikan domain/subdomain aplikasi Node.js sudah memakai HTTPS sebelum dipakai dari Laravel atau aplikasi internal.
Reverse Proxy di Shared Hosting
Tidak perlu setup Nginx manual. hPanel Node.js App sudah menghubungkan URL aplikasi ke process Node.js.
Gunakan konfigurasi Nginx hanya kalau Anda pindah ke VPS.
Opsional: PM2 Jika Nanti Pindah ke VPS
File ecosystem.config.js sudah disediakan, tapi tidak dipakai di Hostinger shared hosting.
Jika nanti pindah ke VPS:
npm install -g pm2
pm2 start ecosystem.config.js
pm2 save
pm2 startup
Untuk logs PM2:
pm2 logs simple-wa-otp-backend
API Authentication
Semua endpoint API selain / wajib memakai header:
x-api-key: ganti-dengan-secret-yang-panjang
Atau:
Authorization: Bearer ganti-dengan-secret-yang-panjang
Endpoint
GET /status
curl -X GET https://wa.example.com/status \
-H "x-api-key: ganti-dengan-secret-yang-panjang"
Contoh response:
{
"success": true,
"message": "Status fetched",
"status": "open",
"connected": true,
"qrAvailable": false,
"user": {
"id": "628xxxx@s.whatsapp.net",
"name": "Bot"
},
"reconnectAttempts": 0,
"unstableDisconnects": 0,
"nextReconnectAt": null,
"sessionLock": {
"enabled": true,
"active": true,
"pid": 12345
},
"lastDisconnectReason": null
}
POST /send-otp
Body:
{
"phone": "628xxxx"
}
Nomor 08xxxx otomatis dinormalisasi menjadi 628xxxx.
Format pesan WhatsApp:
Kode OTP Anda: 123456. Berlaku 5 menit.
Request:
curl -X POST https://wa.example.com/send-otp \
-H "Content-Type: application/json" \
-H "x-api-key: ganti-dengan-secret-yang-panjang" \
-d '{"phone":"081234567890"}'
Response:
{
"success": true,
"message": "OTP sent",
"otp": "hidden"
}
POST /verify-otp
Body:
{
"phone": "628xxxx",
"otp": "123456"
}
Request:
curl -X POST https://wa.example.com/verify-otp \
-H "Content-Type: application/json" \
-H "x-api-key: ganti-dengan-secret-yang-panjang" \
-d '{"phone":"081234567890","otp":"123456"}'
Response:
{
"success": true,
"message": "OTP verified",
"verified": true
}
POST /send-message
Body:
{
"phone": "628xxxx",
"message": "Pesanan Anda sedang dikirim."
}
Request:
curl -X POST https://wa.example.com/send-message \
-H "Content-Type: application/json" \
-H "x-api-key: ganti-dengan-secret-yang-panjang" \
-d '{"phone":"081234567890","message":"Pesanan Anda sedang dikirim."}'
Response:
{
"success": true,
"message": "Message sent",
"to": "6281234567890",
"messageId": "ABCDEF123456"
}
Rate Limit OTP
Default:
OTP_RATE_LIMIT_WINDOW_MINUTES=15
OTP_RATE_LIMIT_MAX=3
Artinya satu nomor hanya bisa meminta OTP maksimal 3 kali per 15 menit.
OTP disimpan sementara dengan Map() memory cache. Jika process restart, OTP yang belum diverifikasi akan hilang. Ini sesuai untuk service ringan di satu shared hosting selama process hanya berjalan satu instance.
Contoh Integrasi Laravel
Tambahkan ke .env Laravel:
WA_OTP_BASE_URL=https://wa.example.com
WA_OTP_API_KEY=ganti-dengan-secret-yang-panjang
Tambahkan ke config/services.php:
'wa_otp' => [
'base_url' => env('WA_OTP_BASE_URL'),
'api_key' => env('WA_OTP_API_KEY'),
],
Kirim OTP:
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'x-api-key' => config('services.wa_otp.api_key'),
])->post(config('services.wa_otp.base_url') . '/send-otp', [
'phone' => $user->phone,
]);
if ($response->successful() && $response->json('success')) {
// OTP terkirim
}
Verifikasi OTP:
use Illuminate\Support\Facades\Http;
$response = Http::withHeaders([
'x-api-key' => config('services.wa_otp.api_key'),
])->post(config('services.wa_otp.base_url') . '/verify-otp', [
'phone' => $request->phone,
'otp' => $request->otp,
]);
if ($response->successful() && $response->json('verified')) {
// OTP valid
}
Kirim notifikasi sederhana:
use Illuminate\Support\Facades\Http;
Http::withHeaders([
'x-api-key' => config('services.wa_otp.api_key'),
])->post(config('services.wa_otp.base_url') . '/send-message', [
'phone' => $order->customer_phone,
'message' => 'Pesanan Anda sedang dikirim.',
]);
Catatan Production
- Gunakan nomor WhatsApp khusus operasional, bukan nomor pribadi utama.
- Jangan commit
.envdan foldersessions/. - Jangan commit atau hapus folder
session-backups/. - Jangan upload
node_modules/; jalankannpm installdi hosting. - Pastikan folder aplikasi bisa ditulis oleh process Node.js agar
sessions/bisa dibuat. - Gunakan HTTPS dari Hostinger SSL.
- Batasi akses endpoint dengan API key kuat dan domain internal jika memungkinkan.
- Backup folder
sessions/jika ingin memindahkan bot tanpa scan ulang. - Baileys bukan API resmi WhatsApp Business. Untuk kebutuhan compliance tinggi, gunakan WhatsApp Business Platform resmi.
Baileys Toolbox
Baileys Toolbox
File ini merangkum kemampuan Baileys yang diekspos oleh backend ini melalui whitelist action. Sumber utama:
- Baileys Socket docs: https://baileys.wiki/docs/category/socket/
- Connecting: https://baileys.wiki/docs/socket/connecting/
- Sending Messages: https://baileys.wiki/docs/socket/sending-messages/
- Presence and Receipts: https://baileys.wiki/docs/socket/presence-receipts/
- Group Management: https://baileys.wiki/docs/socket/group-management/
- Privacy: https://baileys.wiki/docs/socket/privacy/
- App State Updates: https://baileys.wiki/docs/socket/app-state-updates/
- Business Features: https://baileys.wiki/docs/socket/business-features/
Catatan: backend ini tidak mengekspos raw socket method seperti query, sendRawMessage, sendNode, atau relayMessage karena terlalu mudah merusak session atau melanggar batas WhatsApp. Semua action di bawah tetap butuh WhatsApp terhubung kecuali status tertentu.
Endpoint
REST API dengan API key:
GET /baileys/capabilities
POST /baileys/action
Dashboard admin dengan password:
GET /admin/api/baileys/capabilities
POST /admin/api/baileys/action
Di dashboard /, semua action juga tersedia sebagai GUI:
1. Buka halaman utama. 2. Login dengan ADMIN_PASSWORD. 3. Buka panel Baileys Toolbox. 4. Pilih kategori. 5. Pilih action. 6. Isi field yang muncul otomatis. 7. Jalankan action.
Field sederhana dirender sebagai input/select/checkbox. Field array atau object dirender sebagai textarea JSON karena struktur datanya mengikuti tipe Baileys seperti WAMessageKey, contacts, atau ChatModification.
Session WhatsApp sendiri diamankan lewat panel Session Guard di dashboard. Panel itu berada di luar Baileys Toolbox karena tugasnya menjaga folder multi-file auth state agar tidak hilang saat deploy atau restart.
Body standar action:
{
"action": "message.send",
"params": {
"to": "081234567890",
"type": "text",
"text": "Halo dari Baileys toolbox"
}
}
Account
account.status: status socket backend.account.onWhatsApp: cek nomor terdaftar di WhatsApp.account.requestPairingCode: request pairing code untuk login tanpa QR.account.logout: logout session WhatsApp.
Contoh cek nomor:
{
"action": "account.onWhatsApp",
"params": {
"phones": ["081234567890"]
}
}
Messages
message.send mendukung content builder berikut:
textimagevideoaudiodocumentstickerlocationcontactpollreactiondeletedisappearing- raw Baileys
contentobject
Text:
{
"action": "message.send",
"params": {
"to": "081234567890",
"type": "text",
"text": "Pesanan Anda sedang dikirim."
}
}
Image dari URL:
{
"action": "message.send",
"params": {
"to": "081234567890",
"type": "image",
"url": "https://example.com/image.jpg",
"caption": "Bukti pengiriman"
}
}
Document dari URL:
{
"action": "message.send",
"params": {
"to": "081234567890",
"type": "document",
"url": "https://example.com/invoice.pdf",
"mimetype": "application/pdf",
"fileName": "invoice.pdf",
"caption": "Invoice"
}
}
Location:
{
"action": "message.send",
"params": {
"to": "081234567890",
"type": "location",
"latitude": -8.6705,
"longitude": 115.2126,
"name": "Lokasi kurir",
"address": "Denpasar"
}
}
Poll:
{
"action": "message.send",
"params": {
"to": "081234567890",
"type": "poll",
"name": "Pilih jadwal antar",
"values": ["Pagi", "Siang", "Sore"],
"selectableCount": 1
}
}
Raw content object untuk format Baileys yang belum dibuatkan builder:
{
"action": "message.send",
"params": {
"to": "081234567890",
"content": {
"text": "Raw content tetap lewat sendMessage Baileys"
}
}
}
Action lain:
message.read: mark message read denganWAMessageKey[].message.receipt: kirim receipt.
Presence
presence.update:unavailable,available,composing,recording,paused.presence.subscribe: subscribe presence update chat.
Contoh typing:
{
"action": "presence.update",
"params": {
"type": "composing",
"to": "081234567890"
}
}
Profile
profile.pictureUrlprofile.fetchStatusprofile.fetchDisappearingDurationprofile.updateNameprofile.updateStatusprofile.updatePictureprofile.removePicture
Contoh ambil foto profil:
{
"action": "profile.pictureUrl",
"params": {
"jid": "081234567890",
"type": "image"
}
}
Privacy
privacy.fetchSettingsprivacy.fetchBlocklistprivacy.updateBlockStatusprivacy.updateLastSeenprivacy.updateOnlineprivacy.updateProfilePictureprivacy.updateStatusprivacy.updateReadReceiptsprivacy.updateGroupsAddprivacy.updateCallsprivacy.updateMessagesprivacy.updateLinkPreviewsprivacy.updateDefaultDisappearingMode
Contoh block:
{
"action": "privacy.updateBlockStatus",
"params": {
"jid": "081234567890",
"action": "block"
}
}
Groups
group.listgroup.metadatagroup.creategroup.leavegroup.updateSubjectgroup.updateDescriptiongroup.participantsUpdategroup.requestParticipantsListgroup.requestParticipantsUpdategroup.inviteCodegroup.revokeInvitegroup.acceptInvitegroup.getInviteInfogroup.toggleEphemeralgroup.settingUpdategroup.memberAddModegroup.joinApprovalMode
Contoh list group:
{
"action": "group.list",
"params": {}
}
Contoh tambah peserta:
{
"action": "group.participantsUpdate",
"params": {
"jid": "1203630xxxx@g.us",
"participants": ["081234567890"],
"action": "add"
}
}
Business
business.profilebusiness.catalogbusiness.collectionsbusiness.orderDetails
Contoh business profile:
{
"action": "business.profile",
"params": {
"jid": "081234567890"
}
}
Newsletter / Channel
newsletter.createnewsletter.metadatanewsletter.subscribersnewsletter.follownewsletter.unfollownewsletter.mutenewsletter.unmutenewsletter.updateNamenewsletter.updateDescriptionnewsletter.reactMessagenewsletter.fetchMessagesnewsletter.subscribeUpdatesnewsletter.adminCountnewsletter.delete
Contoh metadata:
{
"action": "newsletter.metadata",
"params": {
"type": "jid",
"key": "1203630xxxx@newsletter"
}
}
Chat, Contacts, Calls
chat.modify: archive, pin, mute, clear, mark read, delete, labels, quick replies sesuaiChatModificationBaileys.contact.addOrEditcontact.removecall.createLink
Contoh call link:
{
"action": "call.createLink",
"params": {
"type": "video"
}
}
Cara Cek Daftar Terbaru dari API
curl -X GET https://wa.example.com/baileys/capabilities \
-H "x-api-key: API_KEY_KAMU"
Response berisi daftar action, kategori, parameter ringkas, dan URL sumber dokumentasi.