I was running 50+ WhatsApp sessions on a single Node.js server.

Memory usage? 8GB. CPU? Constantly spiking. And every few hours, something would crash.

So I rewrote the whole thing in Rust. Now it handles 200+ sessions on 512MB RAM.

Here's the whole story.


The Problem With Every WhatsApp API Out There

Every WhatsApp gateway I found had the same issues:

  • Baileys + Node.js — Works great until you scale. Then memory leaks eat you alive.
  • Python solutions — Slow. Really slow.
  • Paid APIs — $50/month per number? Fuck that.

I needed something that could:

  • Handle hundreds of sessions simultaneously
  • Not crash every 6 hours
  • Actually be fast
  • Run on a $5 VPS

Nothing fit. So I built it.


Why Rust?

I know, I know. "Rust is hard." "Just use Go."

But here's the thing — Rust's memory safety isn't just a flex. When you're managing hundreds of WebSocket connections and encryption keys, you need guarantees.

Plus, the whatsapp-rust library exists. Someone already did the hard part of reverse-engineering the WhatsApp Web protocol. I just needed to wrap it in an API.


What I Built: WA-RS

WA-RS is a multi-session WhatsApp REST API gateway. One server, unlimited WhatsApp accounts.

The stack:

Component Tech
Runtime Rust (Nightly)
Web Framework Axum 0.8
Database PostgreSQL
Templates Askama
API Docs OpenAPI 3.0 / Swagger

Features That Actually Matter

✓ Multi-session — Run 100+ WhatsApp accounts on one server
✓ QR Code & Pair Code — Two ways to link devices
✓ Rich Messages — Text, images, video, audio, docs, stickers, location
✓ Webhooks — Real-time events with HMAC-SHA256 signatures
✓ Web Dashboard — Visual management, no CLI needed
✓ Swagger UI — Test endpoints without Postman

Getting Started (It's Stupid Simple)

Option 1: Docker (Recommended)

Pull from Docker Hub:

docker pull fdciabdul/wa-rs:latest

Option 2: Docker Compose (Full Stack)

Create a docker-compose.yml:

services:
  wa-rs:
    image: fdciabdul/wa-rs:latest
    ports:
      - "3451:3451"
    environment:
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=wagateway
      - JWT_SECRET=change-this-in-production
    volumes:
      - wa_sessions:/app/whatsapp_sessions
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=wagateway
    volumes:
      - pg_data:/var/lib/postgresql/data

volumes:
  wa_sessions:
  pg_data:

Run it:

docker compose up -d

That's it. Server running on http://localhost:3451.

Option 3: Build From Source

For the masochists who want to compile themselves:

# Clone the repo
git clone https://github.com/fdciabdul/wa-rs.git
cd wa-rs

# Install Rust nightly (required)
rustup default nightly

# Set up your .env
cp .env.example .env
# Edit .env with your Postgres credentials

# Build and run
cargo run --release

The API

All endpoints require authentication. Get your token first (see Authentication section below).

Create a Session

curl -X POST http://localhost:3451/api/v1/sessions \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "my-session",
    "name": "My WhatsApp",
    "webhook": {
      "url": "https://your-server.com/webhook",
      "secret": "webhook-secret",
      "events": ["message", "connected", "disconnected"]
    }
  }'

Connect a Session

curl -X POST http://localhost:3451/api/v1/sessions/my-session/connect \
  -H "Authorization: Bearer YOUR_TOKEN"

Get QR Code

curl http://localhost:3451/api/v1/sessions/my-session/qr \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns base64 PNG. Scan it with WhatsApp. Done.

Get Pair Code (Alternative to QR)

curl -X POST http://localhost:3451/api/v1/sessions/my-session/pair-code \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"phone": "628123456789"}'

Returns an 8-digit code. Enter it in WhatsApp → Settings → Linked Devices → Link with phone number.

Send a Text Message

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/text \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "text": "Hello from WA-RS!"
  }'

Send an Image

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/image \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "url": "https://example.com/image.jpg",
    "caption": "Check this out!"
  }'

Send a Video

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/video \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "url": "https://example.com/video.mp4",
    "caption": "Watch this"
  }'

Send Audio

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/audio \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "url": "https://example.com/audio.mp3",
    "ptt": true
  }'

Set ptt: true for voice note style, false for regular audio file.

Send a Document

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/document \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "url": "https://example.com/document.pdf",
    "filename": "invoice.pdf",
    "caption": "Here is the invoice"
  }'

Send a Sticker

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/sticker \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "url": "https://example.com/sticker.webp"
  }'

Send Location

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/location \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "latitude": -6.2088,
    "longitude": 106.8456,
    "name": "Jakarta",
    "address": "Jakarta, Indonesia"
  }'

Send Contact Card

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/contact \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "contact": {
      "display_name": "John Doe",
      "phones": [
        {"number": "+1234567890", "phone_type": "CELL"}
      ]
    }
  }'

Edit a Message

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/edit \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "message_id": "3EB0ABC123...",
    "text": "Updated message text"
  }'

React to a Message

curl -X POST http://localhost:3451/api/v1/sessions/my-session/messages/react \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789",
    "message_id": "3EB0ABC123...",
    "emoji": "👍"
  }'

Disconnect a Session

curl -X POST http://localhost:3451/api/v1/sessions/my-session/disconnect \
  -H "Authorization: Bearer YOUR_TOKEN"

Delete a Session

curl -X DELETE http://localhost:3451/api/v1/sessions/my-session \
  -H "Authorization: Bearer YOUR_TOKEN"

The Dashboard

I hate CLIs for management. So I built a web dashboard.

Go to http://localhost:3451/dashboard and you get:

  • Session overview — See all accounts, connection status at a glance
  • Create sessions — Point and click, no curl commands needed
  • QR codes — Scan right from the browser
  • Pair codes — Link with phone number instead of QR
  • Webhook config — Set up integrations visually
  • Settings — View your API token, endpoints, version info

The design is dark mode only. Because light mode is a war crime.

Dashboard Routes

All public, no auth needed:

Route What it shows
/dashboard Main overview with stats
/dashboard/sessions List all sessions
/dashboard/sessions/new Create new session form
/dashboard/sessions/:id Session detail, QR code, actions
/dashboard/settings API token, endpoints, version

Authentication

Getting Your Token

On first startup, the server generates a superadmin token. Two ways to find it:

Option 1: Check the logs

docker compose logs wa-rs | grep "SUPERADMIN"

You'll see something like:

┌─────────────────────────────────────────────────────────────┐
│  SUPERADMIN TOKEN                                            │
├─────────────────────────────────────────────────────────────┤
│  eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdXBlcmFkbWluIiwicm9sZSI... │
├─────────────────────────────────────────────────────────────┤
│  Tip: Set SUPERADMIN_TOKEN in .env to use a fixed token     │
└─────────────────────────────────────────────────────────────┘

Option 2: Dashboard

Go to http://localhost:3451/dashboard/settings — the token is right there.

Using the Token

Include it in every API request:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \
  http://localhost:3451/api/v1/sessions

Using Swagger UI

  1. Go to http://localhost:3451/swagger-ui
  2. Click the Authorize button (top right)
  3. Paste your token
  4. Now all requests include the token automatically

Fixed Token (Production)

Don't want a random token every restart? Set it in your environment:

SUPERADMIN_TOKEN=your-fixed-token-here

Webhooks

Real-time events. When someone messages your WhatsApp, your server knows instantly.

Supported Events

Event When it fires
message Incoming message received
connected Session connected to WhatsApp
disconnected Session disconnected
receipt Message delivered/read
presence Contact online/offline status
qr_code New QR code generated

Webhook Payload Example

{
  "event": "message",
  "session_id": "my-session",
  "timestamp": 1704067200,
  "data": {
    "from": "628123456789@s.whatsapp.net",
    "message_id": "3EB0ABC123...",
    "type": "text",
    "body": "Hello!"
  }
}

Signature Verification

Every webhook includes an X-Signature header — HMAC-SHA256 of the payload using your secret.

import hmac
import hashlib

def verify_signature(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Verify it. Don't trust unverified webhooks.

Register Webhook via API

curl -X POST http://localhost:3451/api/v1/sessions/my-session/webhooks \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhook",
    "secret": "your-webhook-secret",
    "events": ["message", "connected"]
  }'

Contacts & Groups

Check if Numbers are on WhatsApp

curl -X POST http://localhost:3451/api/v1/sessions/my-session/contacts/check \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phones": ["628123456789", "628987654321"]
  }'

Get Contact Info

curl -X POST http://localhost:3451/api/v1/sessions/my-session/contacts/info \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phones": ["628123456789"]
  }'

Get Profile Picture

curl http://localhost:3451/api/v1/sessions/my-session/contacts/628123456789@s.whatsapp.net/picture \
  -H "Authorization: Bearer YOUR_TOKEN"

List Groups

curl http://localhost:3451/api/v1/sessions/my-session/groups \
  -H "Authorization: Bearer YOUR_TOKEN"

Get Group Info

curl http://localhost:3451/api/v1/sessions/my-session/groups/123456789@g.us/info \
  -H "Authorization: Bearer YOUR_TOKEN"

Presence & Chat State

Set Online/Offline Status

curl -X POST http://localhost:3451/api/v1/sessions/my-session/presence/set \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "available"
  }'

Options: available, unavailable

Send Typing Indicator

curl -X POST http://localhost:3451/api/v1/sessions/my-session/chatstate/typing \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789@s.whatsapp.net"
  }'

Send Chat State

curl -X POST http://localhost:3451/api/v1/sessions/my-session/chatstate/send \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "628123456789@s.whatsapp.net",
    "state": "composing"
  }'

Options: composing, recording, paused


Blocking

Get Block List

curl http://localhost:3451/api/v1/sessions/my-session/blocking/list \
  -H "Authorization: Bearer YOUR_TOKEN"

Block a Contact

curl -X POST http://localhost:3451/api/v1/sessions/my-session/blocking/block \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jid": "628123456789@s.whatsapp.net"
  }'

Unblock a Contact

curl -X POST http://localhost:3451/api/v1/sessions/my-session/blocking/unblock \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jid": "628123456789@s.whatsapp.net"
  }'

Check if Blocked

curl http://localhost:3451/api/v1/sessions/my-session/blocking/check/628123456789@s.whatsapp.net \
  -H "Authorization: Bearer YOUR_TOKEN"

Performance

Real numbers from production:

Metric Node.js (Baileys) Rust (WA-RS)
Memory (50 sessions) ~4GB ~200MB
Memory (200 sessions) Crashes ~800MB
Startup time 3-5 seconds <1 second
Message latency 100-300ms 20-50ms
CPU usage (idle) 5-15% <1%

Rust isn't just faster. It's a different league.


Full API Reference

Every endpoint at a glance:

Sessions

Method Endpoint Description
POST /api/v1/sessions Create session
GET /api/v1/sessions List all sessions
GET /api/v1/sessions/:id Get session info
DELETE /api/v1/sessions/:id Delete session
GET /api/v1/sessions/:id/status Get session status
POST /api/v1/sessions/:id/connect Start connection
POST /api/v1/sessions/:id/disconnect Disconnect
GET /api/v1/sessions/:id/qr Get QR code
POST /api/v1/sessions/:id/pair Get pair code
GET /api/v1/sessions/:id/device Get device info

Messages

Method Endpoint Description
POST /api/v1/sessions/:id/messages/text Send text
POST /api/v1/sessions/:id/messages/image Send image
POST /api/v1/sessions/:id/messages/video Send video
POST /api/v1/sessions/:id/messages/audio Send audio
POST /api/v1/sessions/:id/messages/document Send document
POST /api/v1/sessions/:id/messages/sticker Send sticker
POST /api/v1/sessions/:id/messages/location Send location
POST /api/v1/sessions/:id/messages/contact Send contact
POST /api/v1/sessions/:id/messages/edit Edit message
POST /api/v1/sessions/:id/messages/react React to message

Contacts

Method Endpoint Description
POST /api/v1/sessions/:id/contacts/check Check on WhatsApp
POST /api/v1/sessions/:id/contacts/info Get contact info
GET /api/v1/sessions/:id/contacts/:jid/picture Get profile picture
POST /api/v1/sessions/:id/contacts/users Get user info

Groups

Method Endpoint Description
GET /api/v1/sessions/:id/groups List groups
GET /api/v1/sessions/:id/groups/:jid Get group
GET /api/v1/sessions/:id/groups/:jid/info Get group info

Presence & Chat State

Method Endpoint Description
POST /api/v1/sessions/:id/presence/set Set presence
POST /api/v1/sessions/:id/chatstate/send Send chat state
POST /api/v1/sessions/:id/chatstate/typing Send typing

Blocking

Method Endpoint Description
GET /api/v1/sessions/:id/blocking/list Get block list
POST /api/v1/sessions/:id/blocking/block Block contact
POST /api/v1/sessions/:id/blocking/unblock Unblock contact
GET /api/v1/sessions/:id/blocking/check/:jid Check if blocked

Media

Method Endpoint Description
POST /api/v1/sessions/:id/media/upload Upload media

Webhooks

Method Endpoint Description
GET /api/v1/sessions/:id/webhooks List webhooks
POST /api/v1/sessions/:id/webhooks Register webhook
DELETE /api/v1/sessions/:id/webhooks/:webhook_id Unregister webhook

Other

Method Endpoint Description
GET /health Health check
GET /swagger-ui Swagger UI
GET /api-docs/openapi.json OpenAPI spec

Environment Variables

Variable Default Description
POSTGRES_HOST localhost PostgreSQL host
POSTGRES_PORT 5432 PostgreSQL port
POSTGRES_USER postgres PostgreSQL user
POSTGRES_PASSWORD postgres PostgreSQL password
POSTGRES_DB wagateway Database name
JWT_SECRET (generated) Token signing secret
SUPERADMIN_TOKEN (generated) Fixed admin token
WHATSAPP_STORAGE_PATH ./whatsapp_sessions Session data directory
RUST_LOG info Log level (debug, info, warn, error)

Troubleshooting

Session won't connect

  1. Check if the session exists: GET /api/v1/sessions/:id
  2. Try disconnecting first: POST /api/v1/sessions/:id/disconnect
  3. Then reconnect: POST /api/v1/sessions/:id/connect
  4. Check logs: docker compose logs wa-rs

QR code not appearing

  1. Make sure session is in connecting state
  2. Wait a few seconds after calling connect
  3. Refresh the QR endpoint

Webhooks not firing

  1. Verify the webhook URL is accessible from the server
  2. Check if events are configured correctly
  3. Verify signature validation isn't failing

Database connection failed

  1. Check if PostgreSQL is running
  2. Verify credentials in environment variables
  3. Make sure the database exists

What's Next

This is open source. MIT license. Do whatever you want with it.

GitHub: https://github.com/fdciabdul/wa-rs

Documentation: https://wa-rs.imtaqin.id/

Docker Hub: https://hub.docker.com/r/fdciabdul/wa-rs


Final Thoughts

I spent way too long fighting with Node.js memory leaks before I made this switch.

If you're running WhatsApp automation at scale, stop torturing yourself. Rust handles it better. Period.

The learning curve is real. But the payoff is worth it.

Star the repo if this helped. Open an issue if something's broken. PRs welcome.


Built by @taqin. Written in Rust because life's too short for garbage collection.