Skip to main content
Version: v0.10.2

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.

Prerequisites
  • 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.

StatusMeaning
createdThe session record exists, but the WhatsApp engine has not started.
initializingThe engine is booting and opening the WhatsApp connection.
qr_readyA QR code (or pairing code) is available to scan or enter.
authenticatingThe link was accepted; the session is finishing the handshake.
readyConnected. The session can send and receive messages.
disconnectedStopped on purpose, or the connection dropped. Can be started again.
failedStartup 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.

Auto-reconnect and shutdown drain

When auto-reconnect is off (budget 0), a dropped link moves the session to disconnected with the reason Auto-reconnect is disabled — it no longer reports a misleading "reconnection failed after 0 attempts". A disconnect during a graceful shutdown drain (SIGTERM/SIGINT) is left at disconnected rather than scheduling a reconnect that would race the teardown; a later start or auto-restore re-initializes it cleanly.

Auto-reconnect mechanics

By default a dropped session retries without a limit: exponential backoff starting at 5 seconds, capped at 1 hour, with the attempt counter reset after 5 stable minutes. config.maxReconnectAttempts opts out — 0 disables reconnect entirely, and 120 caps the attempts (out-of-range values are clamped into that range).

A watchdog probes every ready engine every 60 seconds; two consecutive probe failures count as a disconnect, so a silently dead engine re-enters the reconnect pipeline within about 2 minutes. Every 5th consecutive attempt emits a session.reconnect_loop webhook event, a warning log, and a Prometheus counter tick (openwa_session_reconnect_loop_alerts_total) — wire one of those into your monitoring so a looping session does not go unnoticed.

On the Baileys engine, two close codes are terminal and never retried: 440 (connectionReplaced — another live instance holds the account) and 403 (forbidden — the account is banned or blocked).

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.

Duplicate names conflict

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" }

On the whatsapp-web.js engine, authenticated HTTP/HTTPS proxies now work — pass the credentials in the proxyUrl and the engine hands them to Chromium's proxyAuthentication (a plain --proxy-server ignores credentials). SOCKS proxies that require a username/password are not supported: Chromium cannot authenticate a SOCKS proxy at all, so OpenWA logs a warning instead of failing with an opaque navigation timeout. IP-authorized proxies (no credentials) work over every proxy type. The Baileys engine does not support proxies; proxyUrl/proxyType are ignored on it.

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.

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. A stale QR is no longer emitted while a whatsapp-web.js session is tearing down, so a buffered QR event can no longer flip a disconnecting session back to qr_ready. 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.

Watch status changes instead of polling

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.

LID-addressed contacts resolve transparently

WhatsApp has begun addressing some individual chats by privacy id (@lid) instead of the phone-number WID (@c.us). A send to an @lid recipient works without extra handling — the engine resolves the recipient to its current WhatsApp id before sending, across text, media, location, contact, and sticker messages, and falls back to the original id if resolution is unavailable. You do not need to detect or rewrite @lid addresses yourself.

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. A session that fails terminally or cannot re-initialize no longer strands its browser process or wedges at "already started" — the dead or half-built engine is evicted from the session registry and its Chromium is force-killed automatically, freeing the concurrency slot for a clean restart.

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"
Delete is destructive — and now purges credentials

Deleting a session removes its database record and tears down everything tied to it: the message history, bulk batches, webhooks, templates, and stored Baileys messages. It also purges the on-disk engine auth directory (data/baileys/<name> for Baileys, data/sessions/session-<name> for whatsapp-web.js) — keyed by session name, so recreating a session under the same name no longer reloads stale credentials and forces a fresh link. To pause an integration and resume it later with the same linked account, 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.

An optional MAX_CONCURRENT_SESSIONS env var caps how many WhatsApp engines may run or initialize at once (default 0 = unlimited). A session that fails terminally still holds its slot until you stop or delete it — stop failed sessions to free capacity. Below the cap, 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. For per-session message stats, GET /api/stats/sessions/:id returns top chats with a lastActive formatted as YYYY-MM-DD HH:MM:SS consistently on both SQLite and PostgreSQL.

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

StatusWhenFix
400 Bad RequestInvalid name, duplicate phoneNumber format, extra body field, QR fetched before qr_ready, or a malformed session :id on PostgreSQL (validated as a UUID at the boundary)Match the field rules; start the session before fetching the QR; pass the id returned by create
401 UnauthorizedMissing/invalid X-API-Key, or a key used outside its allowedSessions scopeSend a valid key scoped to this session
403 ForbiddenA 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 codeUse an operator (or higher) key for any of these
404 Not FoundThe session id does not exist (on SQLite a malformed id also returns 404, since it is treated as text)Check the id from the create response
409 ConflictA session with that name already exists (a name that loses a create race to an identical one also returns 409, not 500)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.status changes instead of polling.
  • First session — the end-to-end walkthrough from zero.
  • API reference — every session field, query parameter, and status code.