nullclaw-channel-whatsmeow-bridge
March 17, 2026 ยท View on GitHub
Standalone HTTP bridge for nullclaw WhatsApp integrations built on top of
whatsmeow.
This repository is intentionally separate from nullclaw core. It owns the
actual WhatsApp login session, QR and pairing-code UX, websocket lifecycle, and
simple HTTP endpoints that an external nullclaw channel plugin can talk to.
Why This Exists
The clean split is:
nullclawgeneric ExternalChannel hostnullclaw-channel-whatsmeow-bridgeWhatsApp session and bridge API- optional adapter plugin a tiny stdio JSON-RPC layer if you want to keep the bridge HTTP-based
That keeps Go code out of the main nullclaw repository while still allowing
WhatsApp to be integrated cleanly.
Endpoints
GET /healthGET /qrPOST /pair-codePOST /pollPOST /sendPOST /editPOST /deletePOST /reactionPOST /read
Configuration
Environment variables:
NULLCLAW_WHATSMEOW_BRIDGE_LISTENdefault127.0.0.1:3301NULLCLAW_WHATSMEOW_BRIDGE_STATE_DIRdefault./stateNULLCLAW_WHATSMEOW_BRIDGE_TOKENoptional Bearer token required on every request when setNULLCLAW_WHATSMEOW_BRIDGE_DISPLAY_NAMEdefaultChrome (Linux)
Quick Start
Build:
go build ./...
Run locally:
NULLCLAW_WHATSMEOW_BRIDGE_LISTEN=127.0.0.1:3301 \
NULLCLAW_WHATSMEOW_BRIDGE_STATE_DIR=./state \
./nullclaw-channel-whatsmeow-bridge
Run with bridge auth enabled:
NULLCLAW_WHATSMEOW_BRIDGE_LISTEN=127.0.0.1:3301 \
NULLCLAW_WHATSMEOW_BRIDGE_STATE_DIR=./state \
NULLCLAW_WHATSMEOW_BRIDGE_TOKEN=change-me \
./nullclaw-channel-whatsmeow-bridge
Docker Compose example:
cd deploy
docker compose up -d
systemd example:
Operator Flow
QR flow
- Start the bridge.
- Open
GET /qruntil it returnsevent=code. - Render or copy the QR code value into your preferred UI.
- Link the device from the WhatsApp phone app.
GET /healthbegins returninglogged_in=true.
Example:
curl http://127.0.0.1:3301/qr
Pair-code flow
- Start the bridge.
- Call
POST /pair-codewithphone_number. - Show the returned pairing code to the operator.
- Complete linked-device flow on the phone.
GET /healthbegins returninglogged_in=true.
Example:
curl \
-X POST \
-H 'Content-Type: application/json' \
http://127.0.0.1:3301/pair-code \
-d '{"phone_number":"551199999999","display_name":"Chrome (Linux)"}'
If NULLCLAW_WHATSMEOW_BRIDGE_TOKEN is set, add:
-H 'Authorization: Bearer change-me'
The bridge persists its real WhatsApp session in state/whatsmeow.db.
Production
For system hardening and deploy notes, see:
Included assets:
- Dockerfile
- deploy/docker-compose.yml
- deploy/systemd/nullclaw-channel-whatsmeow-bridge.service
- .env.example
Full Operator CJM
- Start the bridge with a persistent
NULLCLAW_WHATSMEOW_BRIDGE_STATE_DIR. - Complete QR or pairing-code login.
- Verify health:
curl http://127.0.0.1:3301/health
Expected shape:
{
"ok": true,
"connected": true,
"logged_in": true
}
- Validate inbound queue:
curl \
-X POST \
-H 'Content-Type: application/json' \
http://127.0.0.1:3301/poll \
-d '{"account_id":"wa-main","cursor":"0"}'
- Validate outbound:
curl \
-X POST \
-H 'Content-Type: application/json' \
http://127.0.0.1:3301/send \
-d '{"account_id":"wa-main","to":"551199999999@s.whatsapp.net","text":"hello from bridge"}'
- Once bridge traffic is healthy, wire it into
nullclaw.
nullclaw Integration
This bridge is designed to be used behind a small external-channel plugin.
The existing example contract from nullclaw expects:
GET /healthPOST /pollPOST /send
This bridge implements those directly and also exposes edit/delete/reaction/read endpoints for future richer adapters.
Example nullclaw config when used behind the existing HTTP adapter:
{
"channels": {
"external": {
"accounts": {
"wa-web": {
"runtime_name": "whatsapp_web",
"transport": {
"command": "/opt/nullclaw/plugins/nullclaw-plugin-whatsapp-web",
"timeout_ms": 10000
},
"config": {
"bridge_url": "http://127.0.0.1:3301",
"api_key": "change-me",
"allow_from": ["*"]
}
}
}
}
}
}
Auth split:
- bridge auth is
Authorization: Bearer <token>on HTTP requests - WhatsApp auth is QR or pairing-code login handled inside this bridge
nullclawnever sees the real WhatsApp linked-device credentials
Payload Shapes
POST /poll
Request:
{
"account_id": "wa-main",
"cursor": "0"
}
Response:
{
"next_cursor": "12",
"messages": [
{
"id": "base64url-message-ref",
"from": "551199999999@s.whatsapp.net",
"text": "hello",
"chat_id": "551199999999@s.whatsapp.net",
"is_group": false
}
]
}
POST /send
Request:
{
"account_id": "wa-main",
"to": "551199999999@s.whatsapp.net",
"text": "hello"
}
POST /edit
{
"message_id": "base64url-message-ref",
"text": "edited text"
}
POST /delete
{
"message_id": "base64url-message-ref"
}
POST /reaction
{
"message_id": "base64url-message-ref",
"emoji": "โ
"
}
POST /read
{
"message_id": "base64url-message-ref"
}
POST /pair-code
Request:
{
"phone_number": "551199999999",
"display_name": "Chrome (Linux)"
}
Response:
{
"pairing_code": "ABCD-EFGH"
}
Message ID Format
Inbound messages[].id is a base64url-encoded JSON envelope containing:
- WhatsApp message ID
- chat JID
- sender JID
- whether the message was sent by the current account
- timestamp
That allows later edit/delete/reaction/read actions to target the right message without maintaining a separate lookup table in the plugin.
Build
go build ./...
Validation
go test ./...
go build ./...
Troubleshooting
logged_instays false: complete QR or pair-code flow first; the bridge is correct to stay unhealthy before that.- Empty
/pollresponses: make sure a real inbound text message reached the linked device after login. - 401 responses:
verify the Bearer token matches
NULLCLAW_WHATSMEOW_BRIDGE_TOKEN. - Lost session after restart:
confirm
NULLCLAW_WHATSMEOW_BRIDGE_STATE_DIRis persistent and writable.