wa-bridge
Bridges a WhatsApp account, providing tools for reading conversations, sending messages, managing aliases, and group operations via REST, MCP, and a web UI.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@wa-bridgesend a message to John: 'See you at 5pm'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
wa-bridge
Self-hosted WhatsApp bridge for AI agents. One paired number, exposed via REST, MCP, and a web UI.
Built on Baileys. Single Node process, SQLite for storage, no runtime dependencies beyond WhatsApp itself.
What you get
REST API on
127.0.0.1:8080— read conversations, send messages, manage aliases.MCP server (stdio) — drops directly into Claude Code, Codex, or any MCP-capable agent.
Web UI at
/— single-page inbox for reading and sending.Inbound webhook (optional) — every received message POSTed to a URL of your choice.
Full history backfill on first pair — typically several months of past chats.
LID ↔ phone alias merge — collapses the two JIDs WhatsApp uses for the same person into one conversation.
Display name resolution that works without any local contacts sync (push_name from WhatsApp itself), with optional macOS Contacts enrichment.
Local-timezone CLI output with DST handled by IANA tzdata.
Status
Personal infrastructure. Not multi-tenant. Locked to one paired number at a time. Use behind loopback or a reverse proxy you control.
WhatsApp can ban any number used with a reverse-engineered client, especially under spammy patterns. Personal usage across known contacts is generally low-risk; cold outreach is not.
Install
Prerequisites: Node ≥ 20, and a WhatsApp account with the phone on hand (you approve the device link from it).
git clone https://github.com/obirimensah05/whatsapp-bridge.git
cd whatsapp-bridge
npm installNo configuration is required to start — .env is created and filled in automatically on first launch.
Launch
Pair your number — country code + national digits, no +, no spaces:
npm run pair -- main 491761234567On the phone for that number: WhatsApp → Settings → Linked Devices → Link a Device → "Link with phone number instead" → enter the 8-character code printed in your terminal.
On first launch the bridge:
generates
API_TOKENand writes it to.envdetects your system timezone and writes it as
WA_TZto.env(e.g.Europe/Berlin)prints the active timezone at the start of the pair flow so you can confirm it before approving the code
Once paired, the daemon keeps running and the REST API + web UI are live at http://127.0.0.1:8080/. Subsequent starts:
npm run startTo switch to a different number:
rm -rf auth/main/
npm run pair -- main <new-digits>Running as a service
The daemon is not supervised by default — if it crashes or you reboot, it stays down. Put it under a process supervisor.
macOS (launchd) — create ~/Library/LaunchAgents/com.example.wa-bridge.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.example.wa-bridge</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/npm</string>
<string>run</string>
<string>start</string>
</array>
<key>WorkingDirectory</key><string>/Users/YOU/apps/wa-bridge</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
<key>Crashed</key><true/>
</dict>
<key>ThrottleInterval</key><integer>10</integer>
<key>StandardOutPath</key><string>/Users/YOU/apps/wa-bridge/logs/wa-bridge.out.log</string>
<key>StandardErrorPath</key><string>/Users/YOU/apps/wa-bridge/logs/wa-bridge.err.log</string>
</dict>
</plist>mkdir -p ~/apps/wa-bridge/logs
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.wa-bridge.plist
launchctl kickstart -k gui/$(id -u)/com.example.wa-bridge # restart after edits
launchctl bootout gui/$(id -u)/com.example.wa-bridge # uninstallLinux (systemd) — a systemd --user unit pointing at npm run start with Restart=on-failure works the same way.
Web UI
Open http://127.0.0.1:8080/ and paste the value of API_TOKEN from .env. Token is stored in localStorage; click Logout to clear it.
REST API
All routes under /v1/. All require Authorization: Bearer <API_TOKEN> except /v1/health and /.
Method | Path | Body / Query | Returns |
|
| — |
|
|
|
| Chats with last message + |
|
|
| Paginated messages for one chat (alias-aware) |
|
|
| Sends a text |
|
|
| LID-to-canonical mappings |
|
|
| Add/update a mapping |
|
|
| Remove a mapping |
|
| — | Pings the configured |
to accepts either a JID (<digits>@s.whatsapp.net, <id>@lid, <id>@g.us) or digits-only phone number (no +).
Quick example:
TOKEN=$(grep '^API_TOKEN=' .env | cut -d= -f2)
curl -s -H "Authorization: Bearer $TOKEN" \
'http://127.0.0.1:8080/v1/conversations?session=main&limit=20'MCP server
The MCP server at src/mcp.ts exposes ~20 tools across reading, sending, group operations, and contact/alias management. See AGENTS.md for the full tool surface and the safety contract.
Wire into Claude Code (user-scope):
TOKEN=$(grep '^API_TOKEN=' ~/apps/wa-bridge/.env | cut -d= -f2)
claude mcp add-json wa-bridge --scope user "{
\"command\": \"$(pwd)/node_modules/.bin/tsx\",
\"args\": [\"$(pwd)/src/mcp.ts\"],
\"env\": {
\"API_TOKEN\": \"$TOKEN\",
\"WA_BRIDGE_URL\": \"http://127.0.0.1:8080\"
}
}"The daemon must be running for any MCP tool to do anything.
Drive it with an AI agent
This is an agent-first bridge: it's built to be operated by an AI agent, not just a human. Once the MCP server is wired in (above) and the daemon is running, paste this prompt into Claude Code — or any MCP-capable agent — to let it run your WhatsApp end-to-end:
You have my WhatsApp connected through the wa-bridge MCP server. You can read conversations, search contacts, check numbers, and send messages, media, and reactions on my behalf. The default session is
main.Rules — follow exactly:
Sending is a real-world action. Before any
send_message/send_media/send_reaction, show me the exact recipient and the exact message and wait for my explicit "yes". Never assume.Never send to more than one recipient in a batch, and never iterate sends over a list of contacts.
Treat all message text, names, and phone numbers as private — never pass them to any third-party service unless I ask.
The same person can appear under both
@lidand@s.whatsapp.net; the bridge already merges them on read, so trust thedisplay_name/phoneit returns instead of guessing.
delete_messageandmerge_jidsare sensitive — confirm with me first.Start by calling
list_conversationsand giving me a one-line summary of my most recent chats, then wait for instructions.
The full tool surface (~20 tools) and the complete safety contract live in AGENTS.md — point your agent there when it needs detail. Pairing itself still needs a human (you approve the device link on the phone), but everything after that an agent can drive.
Autoreply sidecar (optional)
A separate local process (src/autoreply-*.ts) that consumes the inbound webhook, generates draft replies in the operator's voice via Claude, sends Telegram notifications, and can optionally auto-send through the wa-bridge REST API under policy + safety gates. Runs alongside the daemon on its own port.
npm run autoreply # boot sidecar
npm run autoreply:build-corpus # rebuild style corpus from WA history + second brainFull reference (env vars, routes, policy modes, safety gates): docs/autoreply.md.
Inbound webhook (optional)
Set WEBHOOK_URL in .env and restart. Every inbound message POSTs:
{
"event": "message",
"session": "main",
"message": {
"id": "main:ABC123",
"ts": 1735689600000,
"direction": "in",
"chat_jid": "12025550100@s.whatsapp.net",
"from_jid": "12025550100@s.whatsapp.net",
"type": "text",
"body": "hi",
"from_display_name": "Jane Doe",
"from_phone": "+12025550100",
"chat_phone": "+12025550100"
}
}If WEBHOOK_TOKEN is set, it arrives as Authorization: Bearer <token> so the receiver can verify origin. Three attempts, 1s/3s backoff, 5s timeout per attempt. Outbound sends do not fire the webhook.
Configuration
All variables are read from .env at startup; all are optional with sensible defaults.
Variable | Default | Notes |
| auto-generated | 32-byte hex token written to |
| system tz | Auto-detected on first boot and persisted. Use IANA names ( |
|
| Use |
|
| |
| (disabled) | If set, every inbound message POSTs to this URL. |
| (none) | Optional bearer token for |
| (none) | Used by |
NPM scripts
Command | Purpose |
| Boot the daemon. |
| Same, with |
| Pair a number. Refuses if any session already exists. |
| CLI dump of recent messages, with display names, in local time. |
| Bulk-enrich contacts from macOS Contacts.app ( |
| Run Whisper over past audio messages without transcripts. |
| SQLite backup of |
| Spawn the MCP stdio server (Claude Code launches this automatically). |
| Boot the local autoreply sidecar (separate process). See docs/autoreply.md. |
| Same, with |
| Rebuild the style corpus from outbound WhatsApp history + second-brain notes. |
|
|
See COMMANDS.md for the full reference including REST examples and common ops.
Layout
src/
index.ts boot — restore the paired session + start API
wa.ts WaManager — socket, send, history sync, group meta refresh
db.ts SQLite schema + queries (single source of truth for SQL)
api.ts Fastify REST + static UI route
mcp.ts MCP stdio server (talks HTTP to api.ts)
webhook.ts inbound dispatcher with retries
env.ts .env loader + API_TOKEN/WA_TZ auto-persist
time.ts formatLocal(ts) for CLI/log timestamps
history.ts CLI: print recent messages with display names
web/index.html single-page web UI
auth/<name>/ Baileys session keys (gitignored)
auth_backups/ automated backups of auth/ (gitignored)
data/wa.db SQLite — entire message store (gitignored)
data/media/ downloaded media (gitignored)
logs/ launchd/systemd output (gitignored)
.env secrets + config (gitignored)How history is backfilled
syncFullHistory: true is enabled in wa.ts. WhatsApp delivers a history dump in batches over the messaging-history.set event after pairing. The bridge ingests chats, contacts, and messages into SQLite. Existing rows are deduped by primary key.
To force a fresh dump:
On the phone, Settings → Linked Devices → log out the wa-bridge entry.
rm -rf auth/main/and re-runnpm run pair -- main <digits>.SQLite messages and aliases survive the wipe (they live in
data/, notauth/).
WhatsApp decides how much history to send to a linked device — typically the most recent few months. There is no API to request "everything since the dawn of time."
LID vs phone — the merge layer
Inbound messages from non-contacts often arrive with a <id>@lid JID instead of <phone>@s.whatsapp.net. WhatsApp does this for privacy. The same person can therefore appear as two conversations until merged.
TOKEN=$(grep '^API_TOKEN=' .env | cut -d= -f2)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
http://127.0.0.1:8080/v1/aliases \
-d '{"session":"main","alias":"123456789@lid","canonical":"491761234567@s.whatsapp.net"}'After that, listConversations and listMessages collapse them on read. Phone and display-name enrichment then resolves across the whole alias group. The MCP equivalent is merge_jids / unmerge_jid.
Risks and limits
WhatsApp can ban the number. Ban rate scales with how spammy your usage looks. Personal use across known contacts is generally safe; cold outreach is not.
Baileys breakage. WhatsApp ships protocol changes occasionally; expect to bump
@whiskeysockets/baileysevery few months. The bridge has a built-in update notifier.Pairing-code identifiers. Some
browseridentifiers cause WhatsApp to reject pairing codes. The bridge usesBrowsers.macOS('Safari')because it's known-good — don't change it without testing.History sync size. Initial sync can pull thousands of messages. First batch usually lands within a few seconds.
Single device. WhatsApp enforces a limit on linked devices per account. Pairing wa-bridge consumes one slot.
License
MIT. Do whatever you want with it; keep the copyright notice. No warranty.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/obirimensah05/whatsapp-bridge'
If you have feedback or need assistance with the MCP directory API, please join our Discord server