Quick Start
Send your first WhatsApp message through OpenWA in about five minutes. You'll run the gateway with Docker, grab your API key, link a WhatsApp account by scanning a QR code, and fire a real curl request that delivers a message.
This is the guaranteed happy path — one linear sequence, no branching. For production setups, custom backends, or running from source, follow the links in Next steps.
- Docker (
docker --versionshould work). Never installed it? The Install Docker page walks through each OS. - A phone with WhatsApp installed — you'll scan a QR code with it.
curlon the command line.- A second WhatsApp number to receive your test message (sending to yourself also works).
- A dedicated WhatsApp number for testing, not your daily driver — connecting any number to an unofficial gateway carries ban risk; see Ban risk & safe sending.
What you'll build
OpenWA targets version v0.23.5. Every example uses the local base URL http://localhost:2785/api and authenticates with the X-API-Key header.
1. Run OpenWA
The quickest start is the prebuilt multi-arch image on Docker Hub — docker pull takes seconds, where building from source spends several minutes downloading and installing Chromium. This path needs only Docker: no Compose, no clone. The dashboard is bundled into the API image, so one command brings up everything on port 2785.
mkdir -p data
docker run -d --name openwa-api -p 2785:2785 -v "$(pwd)/data:/app/data" rmyndharis/openwa:latest
This runs with zero-config defaults — SQLite for the database and local storage for media — so there's nothing to configure for a first run. The ./data directory is bind-mounted into the container, so the database, media, and the generated API key survive a restart.
New to Docker? See Install Docker first. If you would rather run the Compose stack, pin an image version, add PostgreSQL, Redis, or S3-compatible storage, or build from source, Installation covers each of those — every step below works the same on any of them.
Wait until the container is healthy, then confirm the API is up:
curl http://localhost:2785/api/health
{ "status": "ok", "timestamp": "2026-06-26T10:00:00.000Z" }
The endpoint is public; the version field is only included when the request carries a valid API key (v0.19.0+), so a keyless probe shows status and timestamp only.
Once it responds, these URLs are live:
| Surface | URL |
|---|---|
| Dashboard | http://localhost:2785 |
| API base | http://localhost:2785/api |
| Swagger (interactive API docs) | http://localhost:2785/api/docs (opt-in) |
The published image sets NODE_ENV=production, which keeps Swagger off. Add -e ENABLE_SWAGGER=true to the docker run command above to serve it; the full rule is in Configuration.
2. Get your API key
Every API request authenticates with a key sent in the X-API-Key header. On first boot, OpenWA generates a random admin key and writes the full value to data/.api-key inside the bind-mounted ./data directory. It is written owner-only (mode 0600) as the container's own user, so on Linux a host cat is refused. Read it through the container, which works the same on every platform:
docker exec openwa-api cat /app/data/.api-key
owa_k1_3f8c0a1b9d4e7f2a6c5b8e0d1a2f3c4b5d6e7f8091a2b3c4d5e6f70812a3b4c5
Export it so the commands below can reuse it:
export API_KEY="$(docker exec openwa-api cat /app/data/.api-key)"
You can also view and create keys in the dashboard at http://localhost:2785 under API Keys. The full plaintext of a key is shown only once, at creation — store it somewhere safe.
This default admin key has full access. It's fine for local development. Before exposing OpenWA to a network, put it behind TLS and mint a least-privilege, session-scoped key — see Authentication.
3. Create a session
A session is one linked WhatsApp account. Create one with POST /api/sessions — only name is required. It must be 3-50 characters, using only letters, digits, and hyphens; anything outside those bounds is rejected with 400 Bad Request:
curl -X POST http://localhost:2785/api/sessions \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{ "name": "my-bot" }'
The response (201 Created) returns the session, including its generated id and current status:
{
"id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
"name": "my-bot",
"status": "created",
"createdAt": "2026-06-26T09:00:00Z",
"updatedAt": "2026-06-26T09:00:00Z"
}
Save the id — every call below uses it. Export it for convenience:
export SESSION_ID="8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a"
Reusing a name that already exists returns 409 Conflict. Pick a unique name per account.
4. Start the session and scan the QR
Starting a session boots the WhatsApp engine, which produces a QR code you link with your phone. The session moves through these states:
Start the session:
curl -X POST http://localhost:2785/api/sessions/$SESSION_ID/start \
-H "X-API-Key: $API_KEY"
A few seconds later, fetch the QR code. It returns as a base64 PNG data URL:
curl http://localhost:2785/api/sessions/$SESSION_ID/qr \
-H "X-API-Key: $API_KEY"
{
"qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...",
"status": "qr_ready"
}
The simplest way to scan is the dashboard, which renders the QR image for you: open http://localhost:2785, select your session, and scan the displayed code.
On your phone, scan it with WhatsApp → Settings → Linked devices → Link a device.
After scanning, the status advances from qr_ready to authenticating to ready. Poll until it reports ready:
curl http://localhost:2785/api/sessions/$SESSION_ID \
-H "X-API-Key: $API_KEY"
{
"id": "8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a",
"name": "my-bot",
"status": "ready",
"phone": "628123456789",
"pushName": "John Doe",
"connectedAt": "2026-06-26T10:00:00Z",
"createdAt": "2026-06-26T09:00:00Z",
"updatedAt": "2026-06-26T10:00:00Z"
}
The QR code expires after a short window, and the engine replaces the cached code in place, so calling GET /qr again returns the current one. A 400 means there is no code to hand back: either the session is past the QR stage, or its connection dropped and the engine cleared the code (since v0.23.3). Check GET /api/sessions/{sessionId} to see which: at initializing the engine is reconnecting and a fresh QR follows, at ready the session is already linked.
5. Send your first message
Once the session reports ready, send a text message with POST /api/sessions/{sessionId}/messages/send-text.
The chatId is the recipient's number in international format — no +, no spaces — followed by @c.us. For example, the Indonesian number +62 812-3456-789 becomes 628123456789@c.us.
curl -X POST http://localhost:2785/api/sessions/$SESSION_ID/messages/send-text \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{
"chatId": "628123456789@c.us",
"text": "Hello from OpenWA!"
}'
A 201 Created with a messageId means the message is on its way:
{
"messageId": "true_628123456789@c.us_3EB0123456789",
"timestamp": 1782813600
}
Check the recipient's phone. A 201 says the gateway handed the message to WhatsApp, not that it arrived: a number that is not on WhatsApp still answers 201.
Or use the SDK
If you'd rather call OpenWA from JavaScript or TypeScript, the official SDK wraps these same endpoints. Install it with npm install @rmyndharis/openwa, then:
import { OpenWAClient } from '@rmyndharis/openwa';
const client = new OpenWAClient({
baseUrl: 'http://localhost:2785',
apiKey: process.env.API_KEY!,
});
const result = await client.messages.sendText(process.env.SESSION_ID!, {
chatId: '628123456789@c.us',
text: 'Hello from the OpenWA SDK!',
});
console.log(result.messageId);
The first argument to sendText is the session id (the UUID from step 3), not its name — the SDK builds the path /api/sessions/{sessionId}/messages/send-text, and the engine is resolved by id. Passing the name my-bot returns 400 "Session 'my-bot' is not active". Export the id alongside API_KEY so this snippet runs as-is:
export SESSION_ID="8f3c2b1a-9d4e-4c7a-8b2f-1e6d5a4c3b2a"
The SDK adds the /api prefix for you, so baseUrl is http://localhost:2785 (no /api). See the SDK overview for the full surface.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized on any call | Missing or wrong X-API-Key | Re-run export API_KEY="$(docker exec openwa-api cat /app/data/.api-key)" and resend the header. |
400 Validation failed (uuid is expected) on session calls | SESSION_ID is not a UUID (the session name was used instead of its id) | Use the id from step 3, not the name. |
404 Not found on session calls | A well-formed id with no session behind it | Confirm the id from step 3; list sessions with GET /api/sessions. |
409 Conflict creating a session | A session with that name already exists | Choose a different name. |
400 Bad Request creating a session | name fails validation — under 3 or over 50 characters, or has disallowed characters | Use a 3-50 character name of letters, digits, and hyphens only. |
GET /qr returns 400 | No code to hand back: the engine has not produced one yet, the connection dropped and cleared it, or the session is already linked | Wait a moment after start, then re-fetch; check GET /api/sessions/{sessionId} for which case you are in. |
400 on send-text | Session isn't ready, or the chatId is malformed | Poll the session until status is ready; use <number>@c.us with no + or spaces. |
409 on send-text | The engine is not connected: reconnecting, or WhatsApp Web reloading its page even while the status still reads ready | Retryable. Wait a few seconds and resend. |
Session stuck at authenticating after scanning | WhatsApp Web version mismatch on a slow first boot | See Troubleshooting. |
201 but nothing arrives | The recipient may not be on WhatsApp; a 201 is acceptance, not delivery | Check the number with GET /api/sessions/{sessionId}/contacts/check/{number}, then follow the message's status field. |
Next steps
You have a linked session sending messages. From here:
- Connect a session — session states, the QR flow, and lifecycle in depth.
- Installation — the Compose stacks, PostgreSQL/Redis/S3, and running from source.
- Configuration — environment variables and choosing database, storage, and cache backends.
- Sending messages — media, replies, reactions, and bulk sends.
- Webhooks — receive inbound messages and status events in real time.
- API reference — every endpoint, field, and status code.