Sessions & multi-session
Drive a WhatsApp account from OpenWA by running it as a session: a linked WhatsApp number with its own connection, chats, and message history. This guide takes a session through its full lifecycle — create, start, link, monitor, stop, delete — and shows how to run many sessions on one instance and address each one by id.
- A running OpenWA instance reachable at
http://localhost:2785/api(see Installation). - An API key with the operator role for write actions (create, start, stop, delete) and for linking — fetching the QR (
GET /qr) and requesting a pairing code both require an operator key too. Plain read routes (list/get a session, stats) accept any valid key. See Authentication. - A phone with WhatsApp installed, to scan the QR or enter a pairing code.
All examples use the local base URL http://localhost:2785/api and send the key in the X-API-Key header. In production, swap in your own domain over TLS — the /api prefix is unchanged. Replace YOUR_API_KEY with a real key; see Authentication for how to obtain one.
The session lifecycle
A session moves through a fixed set of states. The status field is always lowercase on the wire.
| Status | Meaning |
|---|---|
created | The session record exists, but the WhatsApp engine has not started. |
initializing | The engine is booting and opening the WhatsApp connection. |
qr_ready | A QR code (or pairing code) is available to scan or enter. |
authenticating | The link was accepted; the session is finishing the handshake. |
ready | Connected. The session can send and receive messages. |
disconnected | Stopped on purpose, or the connection dropped. Can be started again. |
failed | Startup or authentication failed. lastError carries the reason. |
The status values are the same regardless of the WhatsApp engine in use (Baileys or whatsapp-web.js); the engine is selected per instance, not per session.
1. Create a session
POST /api/sessions registers a new session. The only required field is a unique name: 3–50 characters, letters, numbers, and hyphens only. A new session starts in created and is not connected yet.
curl -X POST "http://localhost:2785/api/sessions" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "my-bot" }'
Response 201 Created — this route returns the raw session entity, so it also echoes config, proxyUrl, and proxyType (read routes strip those):
{
"id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
"name": "my-bot",
"status": "created",
"phone": null,
"pushName": null,
"config": {},
"proxyUrl": null,
"proxyType": null,
"connectedAt": null,
"lastActiveAt": null,
"createdAt": "2026-06-25T09:00:00.000Z",
"updatedAt": "2026-06-25T09:00:00.000Z"
}
Save the id. Every later call for this session uses it in the path.
Creating a session with a name that already exists returns 409 Conflict. A name that breaks the format rule (or any extra, undeclared body field) returns 400 Bad Request.
To route a session's WhatsApp traffic through a proxy, pass proxyUrl and proxyType (http, https, socks4, or socks5) at create time:
{ "name": "my-bot", "proxyUrl": "http://proxy.example.com:8080", "proxyType": "http" }
2. Start the session
Creating a session does not connect it. Call POST /api/sessions/:id/start to boot the engine:
curl -X POST "http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a/start" \
-H "X-API-Key: YOUR_API_KEY"
Response 200 — the status moves to initializing, then to qr_ready once a code is available:
{
"id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
"name": "my-bot",
"status": "initializing",
"phone": null,
"pushName": null,
"connectedAt": null,
"lastActive": null,
"createdAt": "2026-06-25T09:00:00.000Z",
"updatedAt": "2026-06-25T09:05:00.000Z",
"lastError": null
}
Starting a session that is already running (or already starting) returns 400 Bad Request.
3. Link the account
Link the WhatsApp account in one of two ways. Both require the session to be in qr_ready. Use a QR code for a quick manual link, or a pairing code when you cannot show a QR.
Option A — QR code
Fetch the QR as a PNG data URL with GET /api/sessions/:id/qr, render it, and scan it from your phone under WhatsApp → Linked Devices → Link a device:
curl "http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a/qr" \
-H "X-API-Key: YOUR_API_KEY"
Response 200:
{
"qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"status": "qr_ready"
}
Drop qrCode straight into an <img src="…"> to display it. If the session has not reached qr_ready yet (or is already authenticated), the route returns 400 Bad Request — start the session first, then retry. The Dashboard renders the QR for you on its Sessions page.
Option B — pairing code
Request an 8-character pairing code tied to a phone number, then enter it on the phone under Linked Devices → Link with phone number. The number must be digits only in international format (country code + number, no +, spaces, or dashes), 6–15 digits:
curl -X POST "http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a/pairing-code" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phoneNumber": "628123456789" }'
Response 201:
{ "pairingCode": "ABCD1234", "status": "qr_ready" }
After the link is accepted, the session transitions through authenticating to ready. Poll the session (next step) to confirm.
4. Confirm the session is ready
Read a single session with GET /api/sessions/:id:
curl "http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a" \
-H "X-API-Key: YOUR_API_KEY"
Response 200 — once status is ready, the linked phone and pushName are populated and the session can send and receive:
{
"id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
"name": "my-bot",
"status": "ready",
"phone": "6281234567890",
"pushName": "My Bot",
"connectedAt": "2026-06-25T08:14:02.000Z",
"lastActive": "2026-06-25T09:01:55.000Z",
"createdAt": "2026-06-20T11:30:00.000Z",
"updatedAt": "2026-06-25T09:01:55.000Z",
"lastError": null
}
lastError is non-null only when status is failed. Read routes strip config, proxyUrl, and proxyType, and rename the internal lastActiveAt field to lastActive.
Rather than polling on a loop, subscribe to live session.status, session.qr, session.authenticated, and session.disconnected events through Webhooks. The status values in those events match the lowercase enum above.
Once the session is ready, send your first message — see Sending Messages.
5. Stop or delete a session
Stop to disconnect WhatsApp but keep the session record, so you can start it again later without re-linking:
curl -X POST "http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a/stop" \
-H "X-API-Key: YOUR_API_KEY"
The status becomes disconnected. If a session is wedged and will not stop cleanly, force it down with POST /api/sessions/:id/force-kill, which SIGKILLs the stuck engine and tears it down.
Delete to remove the session entirely with DELETE /api/sessions/:id. It returns 204 No Content with an empty body:
curl -X DELETE "http://localhost:2785/api/sessions/8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a" \
-H "X-API-Key: YOUR_API_KEY"
Deleting a session removes its record and stored data. To pause an integration and resume it later, stop the session instead of deleting it.
Running multiple sessions
One OpenWA instance runs several sessions concurrently — there is nothing to enable. Create each session with its own unique name, start it, and link it to a different WhatsApp number. Each session is fully isolated: its own connection, chats, message history, and webhooks.
There is no enforced session cap in the code. The practical limit is the host's memory: the live engine for each session is held in-process, so plan capacity by RAM. The whatsapp-web.js engine is heavier (it drives a headless Chromium) than the browser-free Baileys engine. For provisioning guidance, see Scaling.
Address a session by id
Every session-scoped route is keyed by the session id in the path. The same call against two different ids acts on two different WhatsApp accounts:
GET /api/sessions/{id}
POST /api/sessions/{sessionId}/messages/send-text
GET /api/sessions/{sessionId}/groups
POST /api/sessions/{sessionId}/webhooks
So to send from a specific number, put that session's id in the URL. To restrict a key to a subset of sessions, set its allowedSessions list (see Authentication) — a scoped key only ever touches the sessions it should, and a request outside that scope is rejected with 401.
Monitor every session at once
GET /api/sessions/stats/overview returns an aggregate view across the key's sessions — useful for a health panel when many run on one instance:
curl "http://localhost:2785/api/sessions/stats/overview" \
-H "X-API-Key: YOUR_API_KEY"
Response 200:
{
"total": 4,
"active": 2,
"ready": 2,
"disconnected": 1,
"byStatus": { "ready": 2, "disconnected": 1, "created": 1 },
"memoryUsage": { "heapUsed": 142, "heapTotal": 210, "rss": 318 }
}
active counts running engines; byStatus is keyed by the lowercase status values; memoryUsage values are in megabytes. A scoped key sees only the sessions in its allowedSessions. To list every session (not just counts), use GET /api/sessions, which returns a bare array ordered by createdAt descending.
Lifecycle with the SDK
The @rmyndharis/openwa SDK wraps the same routes. Construct the client with your gateway's base URL (no /api suffix — the SDK adds it):
import { OpenWAClient } from '@rmyndharis/openwa';
const client = new OpenWAClient({
baseUrl: 'http://localhost:2785',
apiKey: 'YOUR_API_KEY',
});
const session = await client.sessions.create({ name: 'my-bot' });
await client.sessions.start(session.id);
// Show the QR, then poll until the session reports ready.
const { qrCode } = await client.sessions.getQrCode(session.id);
console.log('Scan this QR:', qrCode);
let status = (await client.sessions.get(session.id)).status;
while (status !== 'ready' && status !== 'failed') {
await new Promise((r) => setTimeout(r, 2000));
status = (await client.sessions.get(session.id)).status;
}
console.log('Session status:', status); // "ready"
To link by phone instead of QR, call client.sessions.requestPairingCode(session.id, { phoneNumber: '628123456789' }). Aggregate stats are client.sessions.stats(). See SDK usage for the full surface.
Common errors
| Status | When | Fix |
|---|---|---|
400 Bad Request | Invalid name, duplicate phoneNumber format, extra body field, or QR fetched before qr_ready | Match the field rules; start the session before fetching the QR |
401 Unauthorized | Missing/invalid X-API-Key, or a key used outside its allowedSessions scope | Send a valid key scoped to this session |
403 Forbidden | A valid key whose role is below operator on an operator-gated route — the write actions (create, start, stop, delete) plus fetching the QR (GET /qr) and requesting a pairing code | Use an operator (or higher) key for any of these |
404 Not Found | The session id does not exist | Check the id from the create response |
409 Conflict | A session with that name already exists | Pick a unique name, or reuse the existing session |
For a wider catalogue of failures and recovery steps, see Troubleshooting.
Next steps
- Sending messages — text, media, and the recipient JID format.
- Webhooks — react to inbound messages and
session.statuschanges instead of polling. - First session — the end-to-end walkthrough from zero.
- API reference — every session field, query parameter, and status code.