A lightweight, self-hosted WhatsApp unofficial API with a built-in dashboard. Supports multi-device, multi-session, and session-scoped incoming message webhooks out of the box.
Built with Hono, Baileys, and React.
One command to install and run:
curl -fsSL https://storage.iniadil.dev/wa-porta/install.sh | shThe script clones the repo, asks for your dashboard credentials, and starts the containers. Done.
Or set it up manually in 4 steps:
1. Run
git clone https://github.com/iniadil/waporta.git
cd waporta
cp .env.example .env # set DASHBOARD_USERNAME and DASHBOARD_PASSWORD
docker compose up -d2. Get an API key
Option A — set a static key in .env before starting:
DEFAULT_API_KEY=wap_your_static_key_hereOption B — generate one from the dashboard: open http://localhost:3000/dashboard → log in → API Keys → enter a name → Generate → copy the key (shown once).
3. Connect WhatsApp
Open Sessions in the dashboard → create a session → scan the QR code or use a pairing code. You can also add HTTPS webhook URLs per session to receive incoming message events.
4. Send a message
curl -X POST http://localhost:3000/api/whatsapp/send/text \
-H "X-API-Key: wap_your_key_here" \
-H "Content-Type: application/json" \
-d '{"sessionId": "my-session", "to": "6281234567890", "text": "Hello!"}'- Multi-device — uses the latest WhatsApp multi-device protocol via Baileys; no phone needs to stay online
- Multi-session — manage multiple WhatsApp numbers from one server
- Lightweight — minimal dependencies, fast startup, low memory footprint
- Dashboard included — manage sessions, send messages, webhooks, and check numbers from the browser
- REST API — integrate messaging, session management, and session-scoped webhooks with any backend or automation tool
- Session webhooks — register multiple HTTPS URLs per session for incoming WhatsApp message events
- Retry & notifications — automatic retry with exponential backoff for failed deliveries, optional email/webhook alerts
git clone https://github.com/iniadil/waporta.git
cd waporta
cp .env.example .envEdit .env:
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=your-secure-password
# Optional: skip dashboard key generation by setting a static API key
DEFAULT_API_KEY=wap_your_static_key_heredocker compose up -dDashboard and API available at http://localhost:3000.
Common commands
PORT=8080 docker compose up -d # custom port
docker compose logs -f # view logs
docker compose down # stop
git pull && docker compose up -d --build # upgradePersistent data (all under ./data/ on the host)
| Path | Contents |
|---|---|
baileys_store.db |
SQLite session store |
wa_credentials/ |
WhatsApp credential files |
api_keys.json |
API keys |
webhook_urls.json |
Session webhook URLs |
Back up
./data/to preserve sessions and API keys across migrations.
Without Docker Compose
docker build -t waporta .
docker run -d \
--name waporta \
-p 3000:3000 \
-v $(pwd)/data/baileys_store.db:/app/baileys_store.db \
-v $(pwd)/data/wa_credentials:/app/wa_credentials \
-v $(pwd)/data:/app/data \
-e NODE_ENV=production \
-e DASHBOARD_USERNAME=admin \
-e DASHBOARD_PASSWORD=your-secure-password \
--restart unless-stopped \
waportagit clone https://github.com/iniadil/waporta.git
cd waporta
npm install
cp .env.example .env
# edit .env with your credentialsnpm run dev:all # dev: backend + dashboard (hot reload)
npm run start # production- Backend:
http://localhost:3000 - Dashboard (dev):
http://localhost:5173 - Dashboard (prod):
http://localhost:3000/dashboard
waporta uses a dual-auth system:
| Caller | Header | How to get |
|---|---|---|
| Dashboard | Authorization: Bearer <token> |
Issued on login, stored in browser |
| REST API / external | X-API-Key: <key> |
Set DEFAULT_API_KEY in .env, or generate from dashboard → API Keys |
All /api/whatsapp/* endpoints accept either. Requests without a valid credential receive 401 Unauthorized.
| Page | Description |
|---|---|
| Overview | Session stats + quick actions |
| Sessions | Create sessions (QR / Pairing Code), manage webhook URLs, delete sessions |
| Messaging | Send text, image, or document messages |
| Checker | Check if a number is registered on WhatsApp |
| API Keys | Generate and revoke API keys for external integrations |
QR codes are polled automatically every 2 seconds.
Base URL: http://localhost:3000/api/whatsapp
Interactive docs: https://waporta.net or http://localhost:3000/doc
| Method | Path | Description |
|---|---|---|
GET |
/sessions |
List all sessions |
POST |
/sessions/:sessionId |
Start a new session |
POST |
/sessions/:sessionId/pairing-code |
Start via pairing code |
GET |
/sessions/:sessionId |
Get session status |
GET |
/sessions/:sessionId/qr |
Get QR code |
DELETE |
/sessions/:sessionId |
Delete and logout session |
| Method | Path | Description |
|---|---|---|
POST |
/send/text |
Send a text message |
POST |
/send/image |
Send an image |
POST |
/send/document |
Send a document/file |
| Method | Path | Description |
|---|---|---|
GET |
/check?sessionId=&to= |
Check if a number is on WhatsApp |
Session webhooks deliver incoming WhatsApp message events to HTTPS endpoints that you control.
Each webhook URL belongs to one session, and incoming messages are delivered only to webhook URLs registered for the matching sessionId.
Multiple webhook URLs can be registered for the same session.
All webhook management endpoints use the same authentication as other /api/whatsapp/* endpoints: Authorization: Bearer <token> or X-API-Key: <key>.
| Method | Path | Description |
|---|---|---|
POST |
/sessions/{sessionId}/webhooks |
Register an HTTPS webhook URL |
GET |
/sessions/{sessionId}/webhooks |
List webhook URLs for one session |
DELETE |
/sessions/{sessionId}/webhooks/{id} |
Delete one webhook URL by record id |
Create request
{
"url": "https://example.com/whatsapp"
}url must be an absolute HTTPS URL, up to 2048 characters, with no fragment.
Webhook URL record
{
"id": "a1b2c3d4e5f6a7b8",
"sessionId": "my-session",
"url": "https://example.com/whatsapp",
"normalizedUrl": "https://example.com/whatsapp",
"enabled": true,
"createdAt": "2024-01-01T00:00:00.000Z",
"updatedAt": "2024-01-01T00:00:00.000Z"
}Webhook management responses do not include API keys, request headers, WhatsApp credentials, or other authentication secrets.
If the same normalized URL already exists for the session, create returns 409:
{
"error": "duplicate_webhook_url",
"existingId": "a1b2c3d4e5f6a7b8"
}Outbound WebhookMessagePayload
Each delivery is a POST request with a JSON body containing:
| Field | Description |
|---|---|
event |
Currently message.received |
sessionId |
Session that received the incoming message |
messageId |
Message identifier when available |
sender |
WhatsApp sender JID when available |
recipient |
WhatsApp recipient JID when available |
timestamp |
Message timestamp as a number or string |
messageType |
Message type such as text, image, document |
content |
Message content metadata with secrets redacted |
raw |
Raw event data with secrets redacted |
Example payload:
{
"event": "message.received",
"sessionId": "my-session",
"messageId": "ABCDEF123456",
"sender": "6281234567890@s.whatsapp.net",
"recipient": "6289876543210@s.whatsapp.net",
"timestamp": 1700000000,
"messageType": "text",
"content": {
"text": "Hello from WhatsApp"
},
"raw": {
"event": "redacted raw event data"
}
}# Start a session
curl -X POST http://localhost:3000/api/whatsapp/sessions/my-session \
-H "X-API-Key: wap_your_key_here"
# Send text
curl -X POST http://localhost:3000/api/whatsapp/send/text \
-H "X-API-Key: wap_your_key_here" \
-H "Content-Type: application/json" \
-d '{"sessionId": "my-session", "to": "6281234567890", "text": "Hello!"}'
# Send image
curl -X POST http://localhost:3000/api/whatsapp/send/image \
-H "X-API-Key: wap_your_key_here" \
-H "Content-Type: application/json" \
-d '{"sessionId": "my-session", "to": "6281234567890", "media": "https://example.com/image.jpg", "text": "Caption"}'
# Send document
curl -X POST http://localhost:3000/api/whatsapp/send/document \
-H "X-API-Key: wap_your_key_here" \
-H "Content-Type: application/json" \
-d '{"sessionId": "my-session", "to": "6281234567890", "media": "https://example.com/file.pdf", "filename": "document.pdf"}'
# Check number
curl "http://localhost:3000/api/whatsapp/check?sessionId=my-session&to=6281234567890" \
-H "X-API-Key: wap_your_key_here"
# Pairing code
curl -X POST http://localhost:3000/api/whatsapp/sessions/my-session/pairing-code \
-H "X-API-Key: wap_your_key_here" \
-H "Content-Type: application/json" \
-d '{"phoneNumber": "628123456789"}'
# Delete session
curl -X DELETE http://localhost:3000/api/whatsapp/sessions/my-session \
-H "X-API-Key: wap_your_key_here"
# Create a session webhook URL
curl -X POST http://localhost:3000/api/whatsapp/sessions/my-session/webhooks \
-H "X-API-Key: wap_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/whatsapp"}'
# List session webhook URLs
curl http://localhost:3000/api/whatsapp/sessions/my-session/webhooks \
-H "X-API-Key: wap_your_key_here"
# Delete a session webhook URL
curl -X DELETE http://localhost:3000/api/whatsapp/sessions/my-session/webhooks/a1b2c3d4e5f6a7b8 \
-H "X-API-Key: wap_your_key_here"When a message fails to send (e.g. session disconnected), waporta automatically retries up to 3 times with exponential backoff (1s, 2s, 4s). If all retries fail, it returns a 502 response and optionally notifies you via email or webhook.
| Error type | Example | Behavior |
|---|---|---|
| Retryable | Session disconnected, timeout, connection reset | Retry up to 3 times |
| Non-retryable | Session not found, invalid media, validation error | Fail immediately |
{
"error": "delivery_failed",
"message": "All 3 attempts failed: Session with ID: \"my-session\" Not Ready!",
"attempts": 3
}Add these to your .env to receive email alerts when delivery fails after all retries:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=you@gmail.com
SMTP_PASS=your_app_password
SMTP_FROM=waporta@yourdomain.com
NOTIFY_EMAIL=admin@yourdomain.comSet a webhook URL to receive a POST request with failure details:
NOTIFY_WEBHOOK_URL=https://your-endpoint.com/waportaWebhook payload:
{
"sessionId": "my-session",
"to": "6281234567890",
"messageType": "text",
"error": "All 3 attempts failed: Session with ID: \"my-session\" Not Ready!",
"attempts": 3,
"timestamp": "2026-03-26T15:10:27.000Z"
}Both notification channels are optional and can be used together. If neither is configured, retry still works — you just won't get notified. This failure notification webhook is separate from session webhooks, which deliver incoming WhatsApp message events per session.
- Phone numbers: country code without
+, e.g.6281234567890 - Group messages: add
"isGroup": trueto the request body - Session data is stored in SQLite (
baileys_store.db) - API keys are stored in
data/api_keys.json - Session webhook URLs are stored in
data/webhook_urls.json DEFAULT_API_KEYin.envworks without creating a key from the dashboard
- Website & docs: waporta.net
- Found a bug or have a feature request? Open an issue on GitHub.
- For direct inquiries, reach out at me@iniadil.dev.
