A type-safe WhatsApp gateway REST API built with Elysia and Bun, powered by whatsapp-web.js.
Features an anti-ban multi-strategy — two WhatsApp instances simulate organic conversation before delivering messages.
- Clone this repository
- Copy
.env.exampleto.envand fill in your values:
NODE_ENV=development
PORT=3000
SECRET_KEY= # See step 3
WA1_NUMBER=628xxx # Main WhatsApp number
WA2_NUMBER=628xxx # Secondary WhatsApp number- Generate a secret key and paste it into
.env:
bun run generate-key
# SECRET_KEY=81ac0289905ba97b6d55826325db52d06a2c2495282dcd36394504076709f522- Replace
logo.jpgin root with your own (used as WhatsApp profile picture in production)
bun installbun run devTwo QR codes will appear sequentially — scan each with their respective WhatsApp accounts.
src/
├── index.ts # Server entry, dual client init
├── modules/
│ └── messaging/
│ ├── index.ts # Routes (controller)
│ ├── orchestrator.ts # Anti-ban 3-phase pipeline
│ ├── service.ts # Direct send logic
│ └── model.ts # TypeBox validation schemas
├── plugins/
│ ├── wagate.ts # Dual WhatsApp client plugin
│ └── logger.ts # Winston logger plugin
├── helper/
│ ├── organic.ts # Warm-up message generator
│ ├── constant.ts # Status codes, WA versions
│ ├── error.ts # Custom error classes
│ ├── logger.ts # Winston config (JSON for pm2)
│ ├── success.ts # Success response helper
│ └── util.ts # Phone validation, delay
└── lib/
└── wwebjs.ts # WagateClient class
Every message goes through a 3-phase warm-up pipeline:
Phase 1: WA2 → WA1 (1-3 organic warm-up messages, 1-5s delay)
Phase 2: WA1 → WA2 (1-3 organic reply messages, 1-5s delay)
Phase 3: WA1 → Dest (actual payload, 1-10s delay)
Messages include realistic Indonesian casual texts with random emojis, occasional typos, and content fragments.
All endpoints require authentication via one of these headers:
x-api-key: <your-secret-key>
or
Authorization: Bearer <your-secret-key>
Missing or invalid keys return 401 Unauthorized.
Health check → { "message": "REST API is working" }
Send text message. Body (application/json):
| Field | Type | Required | Description |
|---|---|---|---|
| number | string | ✅ | Phone number (e.g. 628xxx...) |
| content | string | ✅ | Message text |
Send media file. Body (multipart/form-data):
| Field | Type | Required | Description |
|---|---|---|---|
| number | string | ✅ | Phone number (e.g. 628xxx...) |
| content | string | ❌ | Caption |
| file | file | ✅ | Media file |
{ "status": "success", "code": 200, "message": "Message queued for delivery", "data": { ... } }Health check:
curl http://localhost:3000/api/v1/ \
-H "x-api-key: $SECRET_KEY"Send text:
curl -X POST http://localhost:3000/api/v1/send/ \
-H "x-api-key: $SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"number":"628xxxxxxxxxx","content":"Hello from wagate"}'Send media (with caption):
curl -X POST http://localhost:3000/api/v1/send/media \
-H "Authorization: Bearer $SECRET_KEY" \
-F "number=628xxxxxxxxxx" \
-F "content=Invoice attached" \
-F "file=@./invoice.pdf"# System dependencies for Puppeteer/Chromium (Ubuntu 20+)
sudo apt-get update
# Detect Ubuntu version for renamed packages
UBUNTU_VER=$(lsb_release -rs | cut -d. -f1)
if [ "$UBUNTU_VER" -ge 24 ]; then
VERSIONED_PKGS="libgcc-s1 libasound2t64"
elif [ "$UBUNTU_VER" -ge 22 ]; then
VERSIONED_PKGS="libgcc-s1 libasound2"
else
# Ubuntu 20: old package names + gconf (removed in 22+)
VERSIONED_PKGS="libgcc1 libasound2 gconf-service libgconf-2-4"
fi
sudo apt-get install -y \
libgbm1 \
libatk1.0-0 \
libatk-bridge2.0-0 \
libc6 \
libcairo2 \
libcups2 \
libdbus-1-3 \
libexpat1 \
libfontconfig1 \
libgdk-pixbuf2.0-0 \
libglib2.0-0 \
libgtk-3-0 \
libnspr4 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libstdc++6 \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
ca-certificates \
fonts-liberation \
libappindicator3-1 \
libnss3 \
lsb-release \
xdg-utils \
wget \
$VERSIONED_PKGS
# Install Bun
curl -fsSL https://bun.sh/install | bash
# Install Node.js (needed for PM2 and npx)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
# Install PM2 globally
bun install -g pm2
# Install Chrome for Puppeteer
bunx puppeteer browsers install chromeRun locally first to authenticate both WhatsApp accounts:
bun run devScan both QR codes. Sessions are saved to:
.wwebjs_auth/session-client-1/.wwebjs_auth/session-client-2/
# Upload project
rsync -avz --exclude node_modules --exclude logs ./ server:/app/wagate/
# Or just the session files if project is already deployed
scp -r .wwebjs_auth/session-client-1 server:/app/wagate/.wwebjs_auth/
scp -r .wwebjs_auth/session-client-2 server:/app/wagate/.wwebjs_auth/ssh server
cd /app/wagate
# Install dependencies
bun install
# Set up environment
cp .env.example .env
# Edit .env with production values:
# NODE_ENV=production
# WA1_NUMBER=628xxx
# WA2_NUMBER=628xxx
# Generate and set a secret key
bun run generate-key
# Copy the output and set SECRET_KEY= in .envchmod +x scripts/setup-logrotate.sh
./scripts/setup-logrotate.shThis configures:
- Daily rotation at midnight
- 7-day retention (older logs auto-deleted)
- 100MB max size per file (force-rotates if exceeded)
- Compressed rotated logs (.gz)
# Start the application
pm2 start ecosystem.config.cjs
# Save PM2 process list (survives reboots)
pm2 save
# Enable PM2 startup on boot
pm2 startup# View live logs
pm2 logs wagate
# View structured JSON logs (production)
pm2 logs wagate --json
# Monitor dashboard
pm2 monit
# Check status
pm2 status
# Restart
pm2 restart wagate
# View log files directly
tail -f logs/pm2-out.log
tail -f logs/pm2-error.log| File | Content |
|---|---|
logs/pm2-out.log |
PM2 stdout (all info/debug) |
logs/pm2-error.log |
PM2 stderr |
logs/combined.log |
Winston combined log |
logs/error.log |
Winston errors only |
An alternative to the bare-metal + PM2 setup above.
Same as step 2 — run locally first to authenticate both accounts and generate the session files.
docker compose buildrsync -avz .wwebjs_auth/ server:/app/wagate/.wwebjs_auth/# Copy project files (without node_modules)
rsync -avz --exclude node_modules --exclude logs ./ server:/app/wagate/
ssh server
cd /app/wagate
# Set up environment
cp .env.example .env
# Edit .env: set SECRET_KEY, WA1_NUMBER, WA2_NUMBER
docker compose up -d# Live logs
docker compose logs -f wagate
# Status
docker compose ps
# Restart
docker compose restart wagateSessions and logs are persisted via bind mounts:
.wwebjs_auth/— WhatsApp session fileslogs/— application logs
- Delete
.wwebjs_authfolder - Delete
node_modulesandbun.lock - Logout linked devices on your WhatsApp
- Run
bun installagain - Re-scan QR codes
MIT