User Manual & API Reference

Complete guide to the WA Gateway — workflows, API endpoints, and multi-number setup.

Add Number
System Overview

The WA Gateway is a self-hosted WhatsApp notification engine with three layers:

Node.js Engine
Manages WhatsApp connections via Baileys, exposes a REST API on port 3000, runs the outbound message queue with anti-ban throttling.
MySQL Database
Single source of truth for messages, contacts, and sessions. The API only inserts rows — the queue processor delivers them.
PHP Dashboard
Visual KPIs, message audit, manual send, session management, and this documentation — all secured by API key.
System Architecture
flowchart TB subgraph clients["🖥️ Client Layer"] A["PHP Dashboard"] B["External App / Script"] end subgraph engine["⚙️ Node.js Engine :3000"] C["Express REST API"] D["Queue Processor\n⏱ 8–22s throttle"] E["Baileys WA Socket(s)"] end subgraph db["🗄️ MySQL"] F[("messages")] G[("contacts")] H[("sessions")] end subgraph wa["📱 WhatsApp"] I["WA Servers"] J["Recipient Device"] end A -->|"X-API-Key"| C B -->|"X-API-Key"| C C -->|"INSERT pending"| F C <-->|"read / write"| G C <-->|"read / write"| H F -->|"poll every 5s"| D D -->|"check opt_in"| G D -->|"sendMessage"| E E <-->|"WA Protocol"| I I <-->| | J I -->|"delivery receipts"| E E -->|"status updates"| F
Outbound Message Lifecycle
sequenceDiagram participant App as Client App participant API as Node.js API participant DB as MySQL participant Q as Queue Processor participant WA as WhatsApp App->>API: POST /api/send
{phone, message} Note over API: Verify X-API-Key API->>DB: INSERT status='pending' API-->>App: {success:true, message_id:42} loop Every 5 seconds Q->>DB: SELECT pending (opt_in='yes') DB-->>Q: msg #42 Q->>Q: Random sleep 8–22 s Q->>WA: sock.sendMessage() Q->>DB: UPDATE status='sent' WA-->>Q: Delivery receipt (ACK 3) Q->>DB: UPDATE status='delivered' WA-->>Q: Read receipt (ACK 4) Q->>DB: UPDATE status='read' end
Stateless design: If the Node.js engine restarts at any point, the queue processor simply re-reads all pending rows from MySQL and continues. No messages are lost.
Inbound Message & Opt-in Flow
flowchart TD A["📱 User sends WhatsApp message"] --> B["Baileys: messages.upsert"] B --> C{"Contact in DB?"} C -->|No| D["Create contact\nopt_in = pending"] D --> E C -->|Yes| E{"opt_in status?"} E -->|"pending"| F["Send bilingual opt-in prompt\nReply 1 / YES or 2 / NO"] E -->|"yes"| G["Auto-reply:\nVisit wa.libralb.online"] E -->|"no"| H["No reply — contact opted out"] F --> I{"User replies"} I -->|"1 or YES or نعم"| J["opt_in = yes ✅\nSend confirmation"] I -->|"2 or NO or لا"| K["opt_in = no ❌\nSend opt-out confirmation"] B --> L[("Log to messages\ndirection = inbound")]
No spam guarantee: Once a contact replies NO, all future outbound messages to that number are automatically marked opted_out and never sent. The contact can re-subscribe at any time by replying YES.
Authentication

Every API request must include a shared secret key as an HTTP header:

X-API-Key: your_api_secret_here

The secret is set in two places — they must match exactly:

  • engine/.envAPI_SECRET=your_secret
  • dashboard/config.phpdefine('API_SECRET', 'your_secret')
Requests missing or with a wrong X-API-Key receive HTTP 401 Unauthorized.
API Endpoints

Base URL: https://wa.libralb.online (or http://127.0.0.1:3000 internally)

GET /api/status

Returns the status of the default WhatsApp session.

Response
{"name":"default","status":"online","phone":"9611234567","last_seen":"2025-05-11 14:00:00","qr_code":null}
curl -H "X-API-Key: YOUR_SECRET" https://wa.libralb.online/api/status
$result = apiCall('GET', '/api/status');
// $result['status'] === 'online' | 'offline' | 'connecting'
const res  = await fetch('/api/status', { headers: { 'X-API-Key': SECRET } });
const data = await res.json();
// data.status, data.phone, data.last_seen
GET /api/sessions

Returns all configured WhatsApp sessions (supports multi-number).

Response
[{"name":"default","status":"online","phone":"9611234567","last_seen":"..."},
 {"name":"support","status":"offline","phone":null,"last_seen":null}]
curl -H "X-API-Key: YOUR_SECRET" https://wa.libralb.online/api/sessions
$sessions = apiCall('GET', '/api/sessions');
foreach ($sessions as $s) echo $s['name'] . ' → ' . $s['status'];
const sessions = await fetch('/api/sessions', { headers:{'X-API-Key':SECRET} }).then(r=>r.json());
sessions.forEach(s => console.log(s.name, s.status));
POST /api/send

Queues a text message. The engine delivers it with 8–22 s throttling.

Parameters
FieldTypeRequiredDescription
phone string Yes Recipient phone number with country code, digits only. E.g. 9611234567
message string Yes Message text (supports Unicode, emoji, Arabic)
session string No Session name to send from. Default: "default"
Response
{"success":true,"message_id":42}
curl -X POST https://wa.libralb.online/api/send \
  -H "X-API-Key: YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"phone":"9611234567","message":"Hello from the gateway!"}'
$result = apiCall('POST', '/api/send', [
    'phone'   => '9611234567',
    'message' => 'Hello from the gateway!',
    'session' => 'default',          // optional
]);
echo $result['message_id']; // 42
const res = await fetch('/api/send', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-API-Key': SECRET },
  body: JSON.stringify({ phone: '9611234567', message: 'Hello!' })
});
const { message_id } = await res.json();
POST /api/send-image

Queues an image message. Image is auto-compressed (max 1280px, JPEG 78 %) before sending.

Parameters
FieldTypeRequiredDescription
phone string Yes Recipient phone number
file file Yes Image file (JPG/PNG/GIF/WEBP, max 10 MB) — multipart/form-data
caption string No Optional image caption
session string No Session name. Default: "default"
Response
{"success":true,"message_id":43}
curl -X POST https://wa.libralb.online/api/send-image \
  -H "X-API-Key: YOUR_SECRET" \
  -F "phone=9611234567" \
  -F "caption=Check this out!" \
  -F "file=@/path/to/photo.jpg"
$result = apiCall('POST', '/api/send-image',
    ['phone' => '9611234567', 'caption' => 'Hello!'],
    '/var/www/uploads/photo.jpg'   // absolute path — passed as CURLFile
);
const form = new FormData();
form.append('phone',   '9611234567');
form.append('caption', 'Check this!');
form.append('file',    fileInput.files[0]);
const res = await fetch('/api/send-image', {
  method: 'POST', headers: { 'X-API-Key': SECRET }, body: form
});
POST /api/resend/:id

Resets a failed or opted_out message back to pending so the queue re-attempts delivery.

Parameters
FieldTypeRequiredDescription
:id integer Yes Message ID from the messages table (visible in Message Audit log)
Response
{"success":true}
curl -X POST https://wa.libralb.online/api/resend/42 \
  -H "X-API-Key: YOUR_SECRET"
$result = apiCall('POST', '/api/resend/42');
await fetch('/api/resend/42', { method:'POST', headers:{'X-API-Key':SECRET} });
POST /api/regenerate-qr

Forces a session disconnect and initiates a new QR code. Use when the dashboard shows "System Offline".

Parameters
FieldTypeRequiredDescription
session string No Session name to reconnect. Default: "default"
Response
{"success":true,"message":"Reconnection initiated for session \"default\""}
curl -X POST https://wa.libralb.online/api/regenerate-qr \
  -H "X-API-Key: YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"session":"default"}'
$result = apiCall('POST', '/api/regenerate-qr', ['session' => 'default']);
await fetch('/api/regenerate-qr', {
  method:'POST', headers:{'Content-Type':'application/json','X-API-Key':SECRET},
  body: JSON.stringify({ session: 'default' })
});
POST /api/sessions

Creates a new WhatsApp session (adds a second/third number). The engine starts the connection immediately and a QR code appears in Sessions.

Parameters
FieldTypeRequiredDescription
name string Yes Unique session name — alphanumeric, dash, underscore, max 40 chars. E.g. "support", "sales"
Response
{"success":true,"message":"Session \"support\" created — check dashboard for QR code"}
curl -X POST https://wa.libralb.online/api/sessions \
  -H "X-API-Key: YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"name":"support"}'
$result = apiCall('POST', '/api/sessions', ['name' => 'support']);
await fetch('/api/sessions', {
  method:'POST', headers:{'Content-Type':'application/json','X-API-Key':SECRET},
  body: JSON.stringify({ name: 'support' })
});
DELETE /api/sessions/:name

Disconnects and removes a session. The default session cannot be deleted.

Parameters
FieldTypeRequiredDescription
:name string Yes Session name to remove
Response
{"success":true}
curl -X DELETE https://wa.libralb.online/api/sessions/support \
  -H "X-API-Key: YOUR_SECRET"
$result = apiCall('DELETE', '/api/sessions/support');
await fetch('/api/sessions/support', { method:'DELETE', headers:{'X-API-Key':SECRET} });
Multi-Number Support
Yes — your system supports multiple WhatsApp numbers. Each number is a "session" with its own connection, auth state, and message queue.
How it works
  • Each session has a unique name (e.g. default, support, sales).
  • Each session maintains its own Baileys socket and stores auth state in engine/auth_state/<name>/.
  • When you send a message, include "session": "support" to choose which number to use.
  • Messages are queued per-session — each number has its own 8–22 s throttled queue.
  • Opt-in contacts are shared across sessions (contacts table is global).
Adding a second number — Step by step
  1. Go to Sessions in the sidebar and click Add New Session.
  2. Enter a name (e.g. support) and click Add.
  3. A QR code appears — scan it with the second WhatsApp account.
  4. That's it. The engine starts a new socket for that number immediately.
Sending from a specific number
# API
curl -X POST https://wa.libralb.online/api/send \
  -H "X-API-Key: YOUR_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"phone":"9611234567","message":"From support line","session":"support"}'

# PHP (dashboard send.php selects session from a dropdown)
Architecture with 3 numbers
flowchart LR Q["Queue Processor"] --> S1["Session: default\n📱 +961-1-000001"] Q --> S2["Session: support\n📱 +961-1-000002"] Q --> S3["Session: sales\n📱 +961-1-000003"] S1 & S2 & S3 --> WA["WhatsApp Network"]
Important: Each WhatsApp number must be linked through a fresh QR scan. You cannot reuse a QR. If a number is logged out, delete the auth_state/<name> folder and regenerate from the Sessions page.
Troubleshooting

Check that the Node.js engine is running: pm2 status or pm2 logs wa-gateway. Ensure the port 3000 is not blocked by a firewall. Verify API_SECRET matches in both .env and config.php.

The queue waits for the contact opt_in to be "yes". If the contact has never messaged in, their status is "pending" and the queue skips them. You can manually set opt_in='yes' for internal contacts in the database, or trigger an inbound message from them.

The contact previously replied NO to the opt-in prompt. You can reset their status by running UPDATE contacts SET opt_in='pending', opt_in_at=NULL WHERE phone='9611234567' in MySQL.

Run npm install inside the engine/ folder. Sharp requires native binaries that may need to be rebuilt: npm rebuild sharp.

The engine updates the sessions table every time it connects. If the last_seen is old, the socket may have silently disconnected. Click "Generate New QR" to force a fresh connection cycle.

Delivery receipts require the recipient's phone to be online. If the contact uses an old WhatsApp version or has read receipts disabled, you may only ever see "sent" status.