penwa — OpenWA API via curl
Talk to an OpenWA WhatsApp server over its REST API using curl. Spec: docs/06-api-specification.md.
Connection
| Setting |
Flag |
Env var |
Default |
| Base URL |
--base-url |
OPENWABASEURL |
http://0.0.0.0:2785 |
| API key |
--api-key |
OPENWAAPIKEY |
dev-admin-key |
- All API paths are prefixed with
/api → e.g. http://0.0.0.0:2785/api/sessions.
- Auth header is
X-API-Key (not Authorization). Send it on every call except GET /health.
- Set up the shell once, then every snippet below works as-is:
export OPENWA_BASE_URL="${OPENWA_BASE_URL:-http://0.0.0.0:2785}"
export OPENWA_API_KEY="${OPENWA_API_KEY:-dev-admin-key}"
BASE="$OPENWA_BASE_URL/api"
AUTH=(-H "X-API-Key: $OPENWA_API_KEY" -H "Content-Type: application/json")
chatId format: individuals <countrycode><number>@c.us (e.g. [email protected]); groups <id>@g.us. No +, spaces, or dashes.
Quick start: verify, log in, send
# 1. Is the server up? (public, no key)
curl -s "$BASE/health"
# 2. Is my key valid?
curl -s -X POST "$BASE/auth/validate" "${AUTH[@]}"
# 3. Create a session
curl -s -X POST "$BASE/sessions" "${AUTH[@]}" -d '{"name":"main"}'
# -> note the returned sessionId, e.g. sess_abc123
# 4. Get the QR (scan in WhatsApp > Linked devices). Returns an image.
SID=sess_abc123
curl -s "$BASE/sessions/$SID/qr" -H "X-API-Key: $OPENWA_API_KEY" -o qr.png && open qr.png
# 5. Poll until connected
curl -s "$BASE/sessions/$SID" "${AUTH[@]}" # watch for status: connected
# 6. Send your first message
curl -s -X POST "$BASE/sessions/$SID/messages/send-text" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","text":"Hello from OpenWA!"}'
Add -H "X-Request-ID: req_$(date +%s)" to any write call for traceable logs. Pipe responses through | jq to read them.
Sessions
# List (optional ?status= ?page= ?limit=)
curl -s "$BASE/sessions?status=connected&limit=20" "${AUTH[@]}"
# Create with a webhook attached
curl -s -X POST "$BASE/sessions" "${AUTH[@]}" \
-d '{"name":"sales-bot","webhook":"https://example.com/hook"}'
# Details / QR / logout / delete
curl -s "$BASE/sessions/$SID" "${AUTH[@]}"
curl -s "$BASE/sessions/$SID/qr" -H "X-API-Key: $OPENWA_API_KEY" -o qr.png
curl -s -X POST "$BASE/sessions/$SID/logout" "${AUTH[@]}"
curl -s -X DELETE "$BASE/sessions/$SID" "${AUTH[@]}"
Sending messages
All send endpoints are POST /sessions/:sessionId/messages/send-<type>. Media accepts either {"url":"..."} or a base64 {"data":"...","mimetype":"..."} object — {"url":...} shown.
SEND="$BASE/sessions/$SID/messages"
# Text
curl -s -X POST "$SEND/send-text" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","text":"Hello *world*"}'
# Image (with caption)
curl -s -X POST "$SEND/send-image" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","image":{"url":"https://example.com/pic.jpg"},"caption":"Check this out!"}'
# Video
curl -s -X POST "$SEND/send-video" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","video":{"url":"https://example.com/clip.mp4"},"caption":"demo"}'
# Audio / voice note (ptt:true = push-to-talk bubble)
curl -s -X POST "$SEND/send-audio" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","audio":{"url":"https://example.com/vn.ogg"},"ptt":true}'
# Document
curl -s -X POST "$SEND/send-document" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","document":{"url":"https://example.com/invoice.pdf"},"filename":"invoice.pdf","caption":"Your invoice"}'
# Location
curl -s -X POST "$SEND/send-location" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","latitude":-6.2088,"longitude":106.8456,"description":"Jakarta HQ"}'
# Contact card
curl -s -X POST "$SEND/send-contact" "${AUTH[@]}" \
-d '{"chatId":"[email protected]","contact":{"name":"Jane","phone":"628987654321"}}'
Bulk send
# Queue a batch
curl -s -X POST "$SEND/send-bulk" "${AUTH[@]}" -d '{
"messages":[
{"chatId":"[email protected]","text":"Hi A"},
{"chatId":"[email protected]","text":"Hi B"}
]
}'
# -> returns a batchId
BID=batch_xyz
curl -s "$SEND/batch/$BID" "${AUTH[@]}" # status/progress
curl -s -X POST "$SEND/batch/$BID/cancel" "${AUTH[@]}" # cancel
Message history
curl -s "$BASE/sessions/$SID/chats/[email protected]/messages?limit=50" "${AUTH[@]}"
# ?before=<messageId> to paginate older
Contacts
curl -s "$BASE/sessions/$SID/contacts" "${AUTH[@]}"
# Does this number have WhatsApp?
curl -s "$BASE/sessions/$SID/contacts/check/628123456789" "${AUTH[@]}"
# Avatar
curl -s "$BASE/sessions/$SID/contacts/[email protected]/profile-picture" "${AUTH[@]}"
Groups
curl -s "$BASE/sessions/$SID/groups" "${AUTH[@]}" # list
curl -s "$BASE/sessions/$SID/groups/[email protected]" "${AUTH[@]}" # info + participants
# Create
curl -s -X POST "$BASE/sessions/$SID/groups" "${AUTH[@]}" \
-d '{"name":"Project Team","participants":["[email protected]","[email protected]"]}'
Webhooks
# Register
curl -s -X POST "$BASE/sessions/$SID/webhooks" "${AUTH[@]}" -d '{
"url":"https://example.com/wa-hook",
"events":["message.received","message.ack","session.status"],
"secret":"shhh"
}'
curl -s "$BASE/sessions/$SID/webhooks" "${AUTH[@]}" # list
curl -s -X DELETE "$BASE/sessions/$SID/webhooks/wh_123" "${AUTH[@]}" # delete
API key management (admin role)
# Create a scoped key
curl -s -X POST "$BASE/auth/api-keys" "${AUTH[@]}" \
-d '{"name":"n8n Integration","role":"operator"}'
# Optional fields: allowedIps[], allowedSessions[], expiresAt (ISO)
curl -s "$BASE/auth/api-keys" "${AUTH[@]}" # list
curl -s "$BASE/auth/api-keys/key_123" "${AUTH[@]}" # details
curl -s -X PUT "$BASE/auth/api-keys/key_123" "${AUTH[@]}" -d '{"role":"viewer"}'
curl -s -X POST "$BASE/auth/api-keys/key_123/revoke" "${AUTH[@]}"
curl -s -X DELETE "$BASE/auth/api-keys/key_123" "${AUTH[@]}"
Health
curl -s "$BASE/health" # public, no key
curl -s "$BASE/health/detailed" "${AUTH[@]}" # auth required
Tips & gotchas
/api prefix — endpoints in the spec tables omit it; always prepend it (already baked into $BASE).
- QR is binary — write to a file with
-o qr.png; don't dump it to the terminal.
X-API-Key, not Bearer — a 401 almost always means wrong header name or key.
- chatId hygiene — strip
+/spaces; suffix @c.us (person) or @g.us (group). Use contacts/check before sending to unknown numbers.
- Debug a call — add
-i to see status + headers, or -w '\n%{http_code}\n' for just the code.
- WebSocket for realtime:
wss://<host>/ws?apiKey=$OPENWAAPIKEY, then send {"type":"subscribe","payload":{"sessionId":"...","events":[...]}}.
- Reach for
jq to extract fields, e.g. ... | jq -r '.sessionId' to capture and reuse.