Skip to content

jgalea/pigeon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

23 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Pigeon

Pigeon 🐦

License: MIT TypeScript Docker Self-hosted Stars Built by AgentVania

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.

Features

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.

Requirements

  • 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).

Quick start

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:

  1. POST /api/sessions/default/start
  2. Open the QR: GET /api/default/auth/qr (PNG), scan it from the phone under Linked Devices
  3. Poll GET /api/sessions/default until status is WORKING

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"}'

Local development

npm install
npm run build && npm start    # or: npm run dev
npm test                      # vitest

API

Every 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.

/v1

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

/api

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.

MCP server

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, read_contact, send_message, send_media, delete_message, mark_read, check_contact, list_groups, create_group, add_participants.

read_contact merges a person's messages across their real number and any privacy-masked @lid chat, since WhatsApp can split one contact across two chats. create_group and add_participants manage group membership.

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.

Draft-only mode

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.

Anti-spam guard

WhatsApp restricts accounts that message strangers in bursts, especially right after linking a new device. The send guard enforces sane limits on cold sends β€” first contact with a number that has never messaged you. Warm replies and group messages pass through untouched.

A cold send is blocked when it lands inside the post-link cooldown, too soon after the previous cold send, or over the hourly/daily caps. The send fails with a reason explaining which limit it hit. Warm replies (any number that has messaged you) and group/broadcast/newsletter sends are never gated.

The limits and their defaults:

Setting Default What it does
WA_SEND_GUARD on master switch; off disables all cold-send limits
WA_GUARD_POST_CONNECT_MS 120000 (2 min) after the session links or relinks, cold sends are paused for this long
WA_GUARD_COLD_MIN_GAP_MS 60000 (1 min) minimum gap between two cold sends
WA_GUARD_COLD_PER_HOUR 5 max cold sends per rolling hour
WA_GUARD_COLD_PER_DAY 20 max cold sends per rolling day

Two ways to override on a per-send basis:

  • Pass "force": true on a single send to bypass the guard for one deliberate, vetted message (works over the HTTP API and the MCP send tools).
  • Set WA_SEND_GUARD=off to turn the guard off entirely for the session.

Changing the limits

All five settings are read from the environment (falling back to .env in the project root). Edit .env, then restart so the new values load:

# .env β€” example: loosen to 10/hour, 40/day, 30s between cold sends
WA_GUARD_COLD_PER_HOUR=10
WA_GUARD_COLD_PER_DAY=40
WA_GUARD_COLD_MIN_GAP_MS=30000
# Docker: recreate the container so it picks up the new .env values
docker compose up -d

# Local (npm): restart the process
npm run build && npm start

To turn the guard off, or shorten the post-link cooldown to 30s:

# .env
WA_SEND_GUARD=off
# or keep it on but shorten the cooldown
WA_GUARD_POST_CONNECT_MS=30000

The docker-compose.yml passes each guard variable through from .env with the defaults above, so anything you don't set keeps its default.

Configuration

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
WA_SEND_GUARD on anti-spam guard on cold (first-contact) sends; off to disable
WA_GUARD_POST_CONNECT_MS 120000 pause cold sends for this long after the session links
WA_GUARD_COLD_MIN_GAP_MS 60000 minimum gap between two cold sends
WA_GUARD_COLD_PER_HOUR 5 max cold sends per rolling hour
WA_GUARD_COLD_PER_DAY 20 max cold sends per rolling day

How it works

HTTP (Fastify)
 |- /v1/*    versioned API ----+
 |- /api/*   flat endpoints ---+
                               v   one core
   SessionManager . MessageService . MediaService
   HistoryStore . WebhookDispatcher . SQLite auth state
                               |
                         Baileys socket(s)

Storage

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.

Disclaimer

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.

Scope

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.

Credits

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.

License

MIT. See LICENSE.

About

Self-hosted WhatsApp gateway. REST API + MCP server for messaging, media, groups, and webhooks.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages