A self-hosted WhatsApp automation system. Send and receive WhatsApp messages over a simple REST interface, on a server you control — or plug it into AI tools through the built-in MCP server. Built on Baileys, runs in Docker, stores everything in SQLite.
| Send anything | Text, images, documents, video, voice notes, locations, contacts, and polls. |
| Message actions | React, edit, delete, and forward. |
| Conversations | Presence and typing indicators, read receipts, status posting, and channels. |
| Groups & profile | Create and manage groups (participants, invite links, settings); set your name, status, and picture; block and unblock. |
| Receive & react | Incoming messages persisted to SQLite, with per-session webhooks (retry, backoff, optional HMAC signing) and a Server-Sent Events stream. |
| Multi-session | Run several numbers from one instance. QR or phone-code pairing over HTTP. |
| MCP server | Built-in MCP server so AI agents (Claude Code, Claude Desktop, and friends) can read chats and send messages as native tools. |
| Built to run | Auto-reconnect, per-session send queue with light rate limiting, one Docker container with two mounted volumes. |
- A running Docker engine with
docker compose. On macOS that's Docker Desktop, OrbStack, or colima; on Linux, Docker Engine. The daemon must actually be running before you start. - A phone with WhatsApp, to pair the number once via QR.
- Node 22+ only if you want to run it outside Docker (local development).
cp .env.example .env # set WA_API_KEY to any secret string
docker compose up -d --build
curl -s localhost:4000/api/health # {"status":"ok"}The container listens inside on port 3000 and is published to 127.0.0.1:4000 by the compose file.
Pair a number:
POST /api/sessions/default/start- Open the QR:
GET /api/default/auth/qr(PNG), scan it from the phone under Linked Devices - Poll
GET /api/sessions/defaultuntilstatusisWORKING
Send a document:
curl -X POST localhost:4000/api/sendFile \
-H "x-api-key: $WA_API_KEY" -H 'content-type: application/json' \
-d '{"session":"default","chatId":"15551234567@s.whatsapp.net",
"file":{"data":"<base64>","mimetype":"application/pdf","filename":"report.pdf"},
"caption":"here you go"}'npm install
npm run build && npm start # or: npm run dev
npm test # vitestEvery route except /api/health requires X-Api-Key: <WA_API_KEY>. There are two route groups: a clean, versioned /v1 API (recommended for new work) and a set of flatter /api endpoints with the session in the request body, convenient for simple clients.
| Method | Path | Body / notes |
|---|---|---|
| GET / POST | /v1/sessions |
list / create {name} |
| GET / DELETE | /v1/sessions/:name |
status (with qr) / logout |
| POST | /v1/sessions/:name/start|stop|restart |
lifecycle |
| POST | /v1/sessions/:name/messages |
{chatId, type, text?, caption?, media?, location?, contact?, poll?} |
| POST | /v1/sessions/:name/react |
{chatId, msgId, emoji, fromMe?} |
| POST | /v1/sessions/:name/edit |
{chatId, msgId, text} |
| POST | /v1/sessions/:name/delete |
{chatId, msgId, fromMe?} |
| POST | /v1/sessions/:name/forward |
{toChatId, fromChatId, msgId} |
| POST | /v1/sessions/:name/presence |
{type, chatId?} |
| GET | /v1/sessions/:name/chats |
recent chats |
| GET | /v1/sessions/:name/chats/:chatId/messages?limit= |
history |
| GET | /v1/sessions/:name/contacts/check?phone= |
is on WhatsApp |
| GET | /v1/sessions/:name/contacts/:chatId/picture |
profile picture url |
| POST | /v1/sessions/:name/contacts/:chatId/block |
{blocked} |
| PUT | /v1/sessions/:name/profile/name|status |
{name} / {status} |
| POST/GET | /v1/sessions/:name/groups |
create {subject, participants} / list |
| GET | /v1/sessions/:name/groups/:groupId |
metadata |
| POST | /v1/sessions/:name/groups/:groupId/participants |
{participants, action} |
| PUT | /v1/sessions/:name/groups/:groupId/subject|description |
update |
| GET/POST | /v1/sessions/:name/groups/:groupId/invite |
get / invite/revoke |
| POST | /v1/sessions/:name/groups/accept |
{code} |
| POST | /v1/sessions/:name/status |
{text?, media?, statusJidList?} |
| POST/GET/DELETE | /v1/sessions/:name/channels |
create / metadata / follow / delete |
| POST | /v1/sessions/:name/auth/pairing-code |
{phone} (alternative to QR) |
| PUT | /v1/sessions/:name/webhooks |
{urls:[...]} |
| GET | /v1/events |
Server-Sent Events stream |
Flatter endpoints with the session in the body: sendText, sendImage, sendFile, sendVoice, sendVideo, sendLocation, sendContact, sendPoll, sendSeen, reaction, startTyping/stopTyping, session lifecycle, :session/auth/qr, :session/chats/:chatId/messages, :session/groups, contacts/check-exists.
Media takes {data} (base64) or {url} (Pigeon fetches it server-side). Chat ids are <number>@s.whatsapp.net for people and <id>@g.us for groups.
Pigeon ships an MCP (Model Context Protocol) server so AI tools like Claude Code can use WhatsApp directly. It runs over stdio and talks to a running Pigeon instance via the REST API.
Tools: session_status, list_chats, read_messages, send_message, send_media, mark_read, check_contact.
Register it with your MCP client, e.g. in a .mcp.json:
{
"mcpServers": {
"pigeon": {
"command": "node",
"args": ["/path/to/pigeon/dist/mcp.js"]
}
}
}It reads WA_API_KEY (and optional WA_API_URL, WA_SESSION) from the environment, falling back to the .env in the project root. Build first with npm run build.
Set WA_MCP_READONLY=true to run the MCP server in draft-only mode. send_message and send_media stop hitting WhatsApp: instead of sending, they return the composed draft (sent: false) for you to review. The read tools (session_status, list_chats, read_messages, check_contact) keep working. Use this when you want an assistant to draft replies but never send on its own. There's no per-call override, so it's a hard guarantee for that session.
| Variable | Default | Purpose |
|---|---|---|
WA_API_KEY |
(required) | shared API key for the X-Api-Key header |
WA_PORT |
4000 |
listen port |
WA_HOST |
127.0.0.1 |
bind address |
WA_DATA_DIR |
./data |
SQLite location (sessions + history) |
WA_MEDIA_DIR |
./media |
downloaded media |
WA_MEDIA_LIFETIME_DAYS |
180 |
media cleanup window |
WA_LOG_LEVEL |
info |
pino log level |
WA_WEBHOOK_SECRET |
(unset) | if set, sign webhooks with an x-pigeon-signature HMAC |
WA_API_URL |
http://127.0.0.1:4000 |
Pigeon base URL (MCP server only) |
WA_SESSION |
default |
session the MCP server operates on |
WA_MCP_READONLY |
false |
MCP server only; when true, send tools draft instead of sending |
HTTP (Fastify)
|- /v1/* versioned API ----+
|- /api/* flat endpoints ---+
v one core
SessionManager . MessageService . MediaService
HistoryStore . WebhookDispatcher . SQLite auth state
|
Baileys socket(s)
SQLite under WA_DATA_DIR holds session auth state and message history. Media lives under WA_MEDIA_DIR with a lifetime cleanup. Both are mounted volumes, so they survive container rebuilds.
Pigeon is not affiliated with, endorsed by, or connected to WhatsApp or Meta. It uses Baileys, an unofficial library, to automate a WhatsApp account. This is not officially supported, and accounts using automation can be restricted. Use it at your own risk, on a number you control.
Pigeon is built for personal automation, prototyping, and development. It has no bulk-send features by design. Message people who have agreed to hear from you, respect local law, and for business or marketing messaging at scale use the official WhatsApp Business Platform.
Built by Jean Galea.
If you want WhatsApp, or any messaging, done properly for a business on the official APIs, that's what AgentVania does.
MIT. See LICENSE.
