Skip to content

Sidecar HTTP API

The Node sidecar (sidecar/index.js, ~300 LOC) exposes a small HTTP API that Laravel's WebClient talks to. You can also call it directly — useful for debugging or wiring up non-PHP clients.

Auth

All routes (except /health) require Authorization: Bearer <WHATSAPP_WEB_TOKEN> if the env var is set on the sidecar. Mismatch returns 401.

Base URL

Default http://127.0.0.1:3000. See Configuration for host/port env vars.

Routes

GET /health

Public. Returns { ok: true, sessions: N }.

GET /sessions

List of currently-tracked sessions:

json
{ "sessions": [{ "id": "main", "status": "ready" }, ] }

POST /sessions/:id/start

Boots a whatsapp-web.js client for :id. Idempotent — already-running session just returns its current state.

json
{ "id": "main", "status": "qr", "qr": "data:image/png;base64,…" }

status progresses: qrauthenticatingready.

POST /sessions/:id/stop

Disconnects the client but keeps the auth state. Next start reconnects without QR.

DELETE /sessions/:id

Disconnects AND wipes storage/app/whatsapp-sidecar/sessions/session-:id/. Next start needs a fresh QR scan.

GET /sessions/:id/qr

Returns the latest QR data URL for sessions still in qr status. 404 if session is ready or unknown.

GET /sessions/:id/status

json
{ "id": "main", "status": "ready" }

GET /sessions/:id/info

Once ready:

json
{ "pushname": "Munir", "wid": "9665XXX@c.us", "phone": "9665XXX", "platform": "iphone" }

POST /sessions/:id/messages

Send a message — body shape depends on type:

Text:

json
{ "type": "text", "to": "+9665XXX", "body": "Hello" }

Image / video / audio / document / sticker (URL):

json
{ "type": "image", "to": "+9665XXX", "url": "https://…/pic.jpg", "caption": "look" }

Or base64:

json
{
  "type": "document",
  "to": "+9665XXX",
  "base64": "<…>",
  "mimeType": "application/pdf",
  "filename": "report.pdf",
  "caption": "Q3"
}

Reply — add quotedMessageId:

json
{ "type": "text", "to": "+9665XXX", "body": "Following up", "quotedMessageId": "<wid>" }

Reaction:

json
{ "type": "reaction", "messageId": "<wid>", "emoji": "👍" }

Returns the sent message envelope (id, ack, timestamp, …). Returns 409 if session not ready.

GET /sessions/:id/messages/:messageId/media

Streams media bytes for a given message ID. Headers set Content-Type from the WhatsApp message metadata. 404 if the message has no media (or hasn't been downloaded yet).

POST /sessions/:id/messages/:messageId/edit

json
{ "body": "Hi! [edited]" }

WhatsApp only allows editing within ~15 minutes.

POST /sessions/:id/messages/:messageId/delete

json
{ "forEveryone": true }

forEveryone only works within ~1 hour. Falls back to local delete otherwise.

GET /sessions/:id/chats

json
[
  {
    "id": "9665XXX@c.us",
    "name": "Contact name",
    "lastMessage": { "body": "…", "timestamp": 1740000000, "fromMe": false },
    "unreadCount": 2
  },

]

Cached by the Laravel side for WHATSAPP_UI_CHATS_CACHE_SECONDS (default 3s).

GET /sessions/:id/groups

json
[
  { "id": "<gid>@g.us", "subject": "Project X", "participantCount": 12, "isAdmin": true },

]

POST /sessions/:id/groups

Create a group:

json
{ "subject": "Project X", "participants": ["9665XXX@c.us", "9665YYY@c.us"] }

Returns the group's gid._serialized.

POST /sessions/:id/groups/:groupId/participants/add

POST /sessions/:id/groups/:groupId/participants/remove

json
{ "participants": ["9665ZZZ@c.us"] }

POST /sessions/:id/groups/:groupId/leave

No body.

PUT /sessions/:id/groups/:groupId/subject

json
{ "subject": "New name" }

GET /sessions/:id/contacts

json
[
  {
    "id": "9665XXX@c.us",
    "name": "Saved name",
    "pushname": "Profile name",
    "number": "9665XXX",
    "isMyContact": true,
    "isWAContact": true,
    "isBusiness": false,
    "isBlocked": false
  },

]

GET /sessions/:id/contacts/:contactId

Single contact lookup. 404 if not found.

GET /sessions/:id/contacts/:number/exists

json
{ "number": "9665XXX", "exists": true }

The number is normalized — pass with or without +, with or without @c.us.

GET /sessions/:id/contacts/:contactId/picture

Streams the JPEG bytes of the contact's profile picture. 204 if no picture available (cached by Laravel for 30 minutes — both hits AND misses).

POST /sessions/:id/status

Post to Status / Stories:

Text:

json
{
  "type": "text",
  "body": "Just shipped",
  "options": { "backgroundColor": "#075E54", "font": 2 }
}

Image / video (URL or base64, same shape as messages):

json
{ "type": "image", "url": "https://…/promo.jpg", "caption": "New launch" }

GET /sessions/:id/events

Server-Sent Events stream. Long-lived. Used by php artisan whatsapp:web:listen.

event: message
data: {"id":"…","from":"…","body":"…",…}

event: message_ack
data: {"id":"…","ack":2}

event: qr
data: "data:image/png;base64,…"

event: ready
data: {"pushname":"…","wid":"…","phone":"…"}

event: disconnected
data: {"reason":"…"}

Keepalive comments every 30s. Server-side reconnects on backend hiccups.

ID normalization

POST /sessions/:id/messages and /exists accept any of these for to:

InputNormalized to
+9665XXXXXXXX9665XXXXXXXX@c.us
9665XXXXXXXX9665XXXXXXXX@c.us
9665XXXXXXXX@c.us(used as-is)
<gid>@g.us(used as-is — group)
status@broadcast(used as-is — status)

Errors

JSON body shape on error:

json
{ "error": "session not ready", "code": "session_not_ready" }

HTTP status codes:

StatusMeaning
200OK
204OK, no content (avatar miss)
400Validation (missing field, bad type)
401Bearer token mismatch
404Unknown session / message / contact
409Session not ready (still booting whatsapp-web.js)
500WhatsApp Web internal error

Kstmostofa\LaravelWhatsApp\Exceptions\SidecarException wraps these on the Laravel side — $e->getCode() is the HTTP status.

Direct curl usage

Useful for debugging:

bash
TOKEN=$(grep WHATSAPP_WEB_TOKEN .env | cut -d= -f2)

curl -s http://127.0.0.1:3000/sessions \
  -H "Authorization: Bearer $TOKEN"

curl -X POST http://127.0.0.1:3000/sessions/main/start \
  -H "Authorization: Bearer $TOKEN"

curl -X POST http://127.0.0.1:3000/sessions/main/messages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"text","to":"+9665XXX","body":"hello"}'

# Watch the event stream
curl -N http://127.0.0.1:3000/sessions/main/events \
  -H "Authorization: Bearer $TOKEN"

Released under the MIT License.