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 | OPENWA_BASE_URL | http://0.0.0.0:2785 |
| API key | --api-key | OPENWA_API_KEY | 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(notAuthorization). Send it on every call exceptGET /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")
chatIdformat: individuals<countrycode><number>@c.us(e.g.628123456789@c.us); 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":"628123456789@c.us","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":"628123456789@c.us","text":"Hello *world*"}'
# Image (with caption)
curl -s -X POST "$SEND/send-image" "${AUTH[@]}" \
-d '{"chatId":"628123456789@c.us","image":{"url":"https://example.com/pic.jpg"},"caption":"Check this out!"}'
# Video
curl -s -X POST "$SEND/send-video" "${AUTH[@]}" \
-d '{"chatId":"628123456789@c.us","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":"628123456789@c.us","audio":{"url":"https://example.com/vn.ogg"},"ptt":true}'
# Document
curl -s -X POST "$SEND/send-document" "${AUTH[@]}" \
-d '{"chatId":"628123456789@c.us","document":{"url":"https://example.com/invoice.pdf"},"filename":"invoice.pdf","caption":"Your invoice"}'
# Location
curl -s -X POST "$SEND/send-location" "${AUTH[@]}" \
-d '{"chatId":"628123456789@c.us","latitude":-6.2088,"longitude":106.8456,"description":"Jakarta HQ"}'
# Contact card
curl -s -X POST "$SEND/send-contact" "${AUTH[@]}" \
-d '{"chatId":"628123456789@c.us","contact":{"name":"Jane","phone":"628987654321"}}'
Bulk send
# Queue a batch
curl -s -X POST "$SEND/send-bulk" "${AUTH[@]}" -d '{
"messages":[
{"chatId":"628111111111@c.us","text":"Hi A"},
{"chatId":"628222222222@c.us","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/628123456789@c.us/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/628123456789@c.us/profile-picture" "${AUTH[@]}"
Groups
curl -s "$BASE/sessions/$SID/groups" "${AUTH[@]}" # list
curl -s "$BASE/sessions/$SID/groups/120363000000000000@g.us" "${AUTH[@]}" # info + participants
# Create
curl -s -X POST "$BASE/sessions/$SID/groups" "${AUTH[@]}" \
-d '{"name":"Project Team","participants":["628111111111@c.us","628222222222@c.us"]}'
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
/apiprefix — 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 — a401almost always means wrong header name or key.- chatId hygiene — strip
+/spaces; suffix@c.us(person) or@g.us(group). Usecontacts/checkbefore sending to unknown numbers. - Debug a call — add
-ito see status + headers, or-w '\n%{http_code}\n'for just the code. - WebSocket for realtime:
wss://<host>/ws?apiKey=$OPENWA_API_KEY, then send{"type":"subscribe","payload":{"sessionId":"...","events":[...]}}. - Reach for
jqto extract fields, e.g.... | jq -r '.sessionId'to capture and reuse.