Use when reading, sending, or managing personal WhatsApp messages. Triggers: 'check WhatsApp', 'send WhatsApp to [person]', 'read messages from [person]', 'what did X send me', 'reply to X on WhatsApp', 'my unread WhatsApp messages'.
Read and send WhatsApp messages via a local whatsapp-web.js bridge running on localhost:${WHATSAPP_BRIDGE_PORT}.
cd ${WHATSAPP_BRIDGE_DIR} && npm install
Start as persistent daemon (recommended):
npm install -g pm2
pm2 start ${WHATSAPP_BRIDGE_DIR}/ecosystem.config.js
pm2 save && pm2 startup # auto-start on login
Or run manually in a terminal:
node ${WHATSAPP_BRIDGE_DIR}/scripts/wa-bridge.js
On first run: scan QR with Phone → Linked Devices → Link a Device.
Session persists in ${WHATSAPP_BRIDGE_DIR}/.wwebjs_auth/ — no re-scan on restart.
curl -s http://localhost:${WHATSAPP_BRIDGE_PORT}/status
| Response | Action |
|---|---|
{"status":"ready"} | Proceed |
{"status":"qr"} | Tell user to scan QR in the bridge terminal |
{"status":"initializing"} | Wait 5s, retry once |
| Connection refused | Tell user to start bridge: pm2 start ${WHATSAPP_BRIDGE_DIR}/ecosystem.config.js |
curl -s http://localhost:${WHATSAPP_BRIDGE_PORT}/unread | jq '.'
curl -s "http://localhost:${WHATSAPP_BRIDGE_PORT}/search?query=Name" | jq '.'
Returns id, name, unreadCount, isGroup. Use id for all subsequent calls — never construct IDs manually.
curl -s "http://localhost:${WHATSAPP_BRIDGE_PORT}/messages?chatId=CHAT_ID&limit=20" | jq '.'
fromMe: true = sent by usertime field is ISO 8601 (human-readable); timestamp is raw Unix epochhasMedia: true = media message; body may be empty (image/video) or caption onlytype: "ptt" = voice note — audio content not accessible via bodyauthor is only set in group messages (shows sender); null in direct chatscurl -s "http://localhost:${WHATSAPP_BRIDGE_PORT}/chats?limit=20" | jq '.'
curl -s -X POST http://localhost:${WHATSAPP_BRIDGE_PORT}/mark-read \
-H "Content-Type: application/json" \
-d '{"chatId":"CHAT_ID"}' | jq '.'
ALWAYS show draft to the user and get explicit confirmation before sending.
curl -s -X POST http://localhost:${WHATSAPP_BRIDGE_PORT}/send \
-H "Content-Type: application/json" \
-d '{"chatId":"CHAT_ID","message":"MESSAGE_TEXT"}' | jq '.'
curl -s -X POST http://localhost:${WHATSAPP_BRIDGE_PORT}/reply \
-H "Content-Type: application/json" \
-d '{"messageId":"MSG_ID","chatId":"CHAT_ID","message":"MESSAGE_TEXT"}' | jq '.'
| Goal | Command |
|---|---|
| Bridge status | GET /status |
| Unread chats | GET /unread |
| Find chat by name | GET /search?query=name |
| Read messages | GET /messages?chatId=X&limit=N |
| All recent chats | GET /chats?limit=N |
| Mark as read | POST /mark-read {chatId} |
| Send | POST /send {chatId, message} |
| Reply to message | POST /reply {messageId, chatId, message} |
Set in your .env:
WHATSAPP_BRIDGE_PORT=3210
WHATSAPP_BRIDGE_DIR=~/.pai/skills/whatsapp
| Issue | Fix |
|---|---|
| Bridge not running | pm2 start ${WHATSAPP_BRIDGE_DIR}/ecosystem.config.js or run manually |
| QR needed | Phone → Linked Devices → Link a Device. Scan in bridge terminal |
| Chat not found in search | Try partial name, nickname, or first name only |
| Media message body empty | Normal — image/video content not accessible via text body |
| 503 from endpoint | Bridge not ready yet — check /status |
/search. IDs vary (@c.us, @lid, @g.us for groups) — never guess.