For Humans & LLMs: This document covers the WhatsApp CLI tool: installation, usage, the API reference, examples, architecture, and troubleshooting. Humans and large language models can both read it.
Version: 1.3.2 Repository: https://github.com/vicentereig/whatsapp-cli License: MIT Language: Go 1.24+
- Overview
- Installation
- Quick Start
- Complete Command Reference
- JSON Response Format
- Usage Examples
- Integration with LLMs/AI Tools
- Data Model
- Storage & Persistence
- Authentication & Security
- Architecture
- Error Handling
- Troubleshooting
- Development
- FAQ
- Credits
WhatsApp CLI is a standalone command-line tool for WhatsApp. It uses the WhatsApp Web multidevice protocol. Every command returns structured JSON output. Use it for:
- Automation: Shell scripts, cron jobs, CI/CD pipelines
- AI Integration: Codex, Claude Code, GPT-based tools
- Data Analysis: Extract and analyze WhatsApp conversations
- Custom Applications: Build tools on top of WhatsApp
| Feature | Description |
|---|---|
| Zero Dependencies | Single compiled binary (21MB), no runtime required |
| JSON Output | All commands return structured JSON for easy parsing |
| Persistent Sessions | Authenticate once via QR code, auto-reconnect for ~20 days |
| Local Storage | SQLite database, no cloud dependencies |
| Full Messaging | Send, receive, search messages; manage contacts & chats |
| Group Support | Send/receive messages in group chats |
| TDD Implementation | 100% test coverage, production-ready |
- Operating System: Linux, macOS, Windows
- Architecture: x86_64, ARM64
- Go Version: 1.24+ (for building from source)
- Storage: ~50MB for binary + variable for message database
- Network: Internet connection required
- WhatsApp Account: Active WhatsApp account with smartphone
brew install vicentereig/tap/whatsapp-cliOr tap first, then install:
brew tap vicentereig/tap
brew install whatsapp-cli# Linux (x86_64)
curl -LO https://github.com/vicentereig/whatsapp-cli/releases/latest/download/whatsapp-cli-linux-amd64.tar.gz
tar -xzf whatsapp-cli-linux-amd64.tar.gz
sudo mv whatsapp-cli-linux-amd64 /usr/local/bin/whatsapp-cli
# macOS (ARM64 - M1/M2/M3)
curl -LO https://github.com/vicentereig/whatsapp-cli/releases/latest/download/whatsapp-cli-darwin-arm64.tar.gz
tar -xzf whatsapp-cli-darwin-arm64.tar.gz
sudo mv whatsapp-cli-darwin-arm64 /usr/local/bin/whatsapp-cli
# macOS (Intel)
curl -LO https://github.com/vicentereig/whatsapp-cli/releases/latest/download/whatsapp-cli-darwin-amd64.tar.gz
tar -xzf whatsapp-cli-darwin-amd64.tar.gz
sudo mv whatsapp-cli-darwin-amd64 /usr/local/bin/whatsapp-cli
# Windows (x86_64) - download and extract whatsapp-cli-windows-amd64.zip from releases page# Clone repository
git clone https://github.com/vicentereig/whatsapp-cli.git
cd whatsapp-cli
# Install dependencies
go mod download
# Build
go build -o whatsapp-cli .
# Install (optional)
sudo mv whatsapp-cli /usr/local/bin/
# Verify installation
whatsapp-cli --helpgo install github.com/vicentereig/whatsapp-cli@latest- Version tags use semantic versioning (
vMAJOR.MINOR.PATCH). For reproducible builds, use a specific tag, for examplev1.0.0, withgo install github.com/vicentereig/whatsapp-cli@v1.0.0. - The GitHub Releases page has pre-built artifacts for Linux, macOS, and Windows. Each archive has a matching SHA-256 entry in
checksums.txt. Before you install, runshasum -a 256 -c checksums.txt --ignore-missingto verify the archive. - Binaries use the name
whatsapp-cli-<os>-<arch>(Windows adds.exe). After you extract the file, mark it executable withchmod +xand place it in yourPATH, for example/usr/local/bin/whatsapp-cli. - Each uploaded file, including
checksums.txt, also has a Sigstore cosign signature (.sig) and certificate (.pem). To verify a file, runcosign verify-blob --certificate <file>.pem --signature <file>.sig <file>. GitHub Actions uses OIDC identities, so you can enforce provenance during verification. - To build from source, follow Method 3. Use this method to audit the code, change compilation flags, or test changes before you tag a release.
- Maintainers can follow the steps in
docs/RELEASE.md. It coversgo install, source builds, and GitHub Release automation.
whatsapp-cli authWhat happens:
- QR code appears in terminal
- Open WhatsApp on your phone → Settings → Linked Devices → Link a Device
- Scan the QR code
- Session saved to
./store/whatsapp.db
Output:
{
"success": true,
"data": {
"authenticated": true,
"message": "Successfully authenticated"
},
"error": null
}Session Duration: ~20 days before re-authentication required
Before you list or search messages, sync them from WhatsApp:
# Start syncing messages (run this in the background or a separate terminal)
whatsapp-cli sync
# Press Ctrl+C when done syncingWhat happens:
- Connects to WhatsApp and stays connected
- Downloads message history from WhatsApp servers
- Receives new messages in real-time
- Stores everything in
./store/messages.db - Runs until you press Ctrl+C
Tip: Run sync in a tmux or screen session, or as a background service, so it keeps receiving messages.
# List your chats
whatsapp-cli chats list --limit 10
# Search for a contact
whatsapp-cli contacts search --query "John"
# Send a message
whatsapp-cli send --to 1234567890 --message "Hello from CLI!"
# Search messages
whatsapp-cli messages search --query "meeting"whatsapp-cli versionExample Output:
{
"success": true,
"data": {
"version": "v1.1.0"
},
"error": null
}All commands support these global flags:
| Flag | Type | Default | Description |
|---|---|---|---|
--store |
string | ./store |
Directory for session and message databases |
Example:
whatsapp-cli --store /var/lib/whatsapp chats listAuthenticate with WhatsApp via QR code.
Syntax:
whatsapp-cli authParameters: None
Returns:
{
"success": true,
"data": {
"authenticated": boolean,
"message": string
},
"error": null
}Behavior:
- If already authenticated: Returns success immediately
- If not authenticated: Displays QR code and waits for scan
- Timeout: 5 minutes
- Creates
store/whatsapp.dbwith session data
Example:
whatsapp-cli auth
# Scan QR code with phone
# ✓ Successfully authenticated!messages list and messages search return empty results.
Sync messages from WhatsApp to the local database continuously. This command:
- Downloads message history from WhatsApp servers
- Receives new messages in real time
- Stores all messages in the SQLite database
- Runs until you press Ctrl+C
Syntax:
whatsapp-cli syncParameters: None
Returns: (on exit via Ctrl+C)
{
"success": true,
"data": {
"synced": true,
"messages_count": 1234
},
"error": null
}Behavior:
- Connects to WhatsApp (authenticates if needed)
- Registers event handlers for incoming messages and history sync
- Processes
*events.Messagefor real-time messages - Processes
*events.HistorySyncfor message history batches - Stores all messages in
store/messages.db - Sends progress to stderr (does not affect JSON output)
- Runs until interrupted (Ctrl+C)
- Disconnects on exit
Progress Output (stderr):
🚀 Starting WhatsApp sync...
✓ Connected to WhatsApp
🔄 Listening for messages... (Press Ctrl+C to stop)
📜 Processing history sync (42 conversations)...
💬 Synced 1234 messages...
^C
✓ Sync completed. Total messages synced: 1234
Examples:
# Basic sync - run in foreground
whatsapp-cli sync
# Run in background (recommended for continuous syncing)
whatsapp-cli sync > sync.json 2> sync.log &
# Run in tmux/screen session
tmux new -s whatsapp
whatsapp-cli sync
# Detach with Ctrl+B D
# Run with custom storage directory
whatsapp-cli --store /var/lib/whatsapp sync
# Stop sync gracefully
kill -INT <pid>
# Or press Ctrl+C in foregroundUse Cases:
- Initial Setup: Run once to download all message history
- Continuous Sync: Run as background service to receive messages
- Periodic Sync: Run via cron to update messages on a schedule
- Development: Run in terminal while testing queries
Notes:
- Message history sync can take time. The duration depends on the number of messages.
- SQLite PRIMARY KEY constraints prevent duplicate messages.
- The CLI does not download media files. It stores only metadata: type, filename, and URL.
- The connection stays open until you interrupt it.
- You can restart the sync safely. It does not create duplicate messages.
List messages from all chats or a specific chat.
Syntax:
whatsapp-cli messages list [OPTIONS]Parameters:
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--chat |
string | No | - | Filter by chat JID (e.g., 1234567890@s.whatsapp.net) |
--limit |
int | No | 20 | Maximum number of messages to return |
--page |
int | No | 0 | Page number for pagination (0-indexed) |
Returns:
{
"success": true,
"data": [
{
"id": "msg_unique_id",
"chat_jid": "1234567890@s.whatsapp.net",
"chat_name": "John Doe",
"sender": "1234567890",
"content": "Message text content",
"timestamp": "2025-10-26T10:30:00Z",
"is_from_me": false,
"media_type": ""
}
],
"error": null
}Examples:
# List 50 most recent messages across all chats
whatsapp-cli messages list --limit 50
# List messages from specific chat
whatsapp-cli messages list --chat 1234567890@s.whatsapp.net
# Pagination: Get second page of results
whatsapp-cli messages list --limit 20 --page 1
# Get JID first, then list messages
JID=$(whatsapp-cli contacts search --query "Alice" | jq -r '.data[0].jid')
whatsapp-cli messages list --chat "$JID" --limit 100Sorting: Messages returned in reverse chronological order (newest first)
Search messages by content across all chats.
Syntax:
whatsapp-cli messages search --query TEXT [OPTIONS]Parameters:
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--query |
string | Yes | - | Search term (case-insensitive, partial match) |
--limit |
int | No | 20 | Maximum number of results |
--page |
int | No | 0 | Page number for pagination |
Returns: Same format as messages list
Examples:
# Search for messages containing "meeting"
whatsapp-cli messages search --query "meeting"
# Search with more results
whatsapp-cli messages search --query "project" --limit 100
# Case-insensitive search
whatsapp-cli messages search --query "URGENT" # Finds "urgent", "Urgent", etc.Search Behavior:
- Case-insensitive
- Partial word matching
- Searches message content only (not sender names)
- Returns messages from all chats
Search contacts by name or phone number.
Syntax:
whatsapp-cli contacts search --query TEXTParameters:
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--query |
string | Yes | - | Search term for name or phone number |
Returns:
{
"success": true,
"data": [
{
"phone_number": "1234567890",
"name": "John Doe",
"jid": "1234567890@s.whatsapp.net"
}
],
"error": null
}Examples:
# Search by name
whatsapp-cli contacts search --query "John"
# Search by partial phone number
whatsapp-cli contacts search --query "5551234"
# Extract JID for further operations
JID=$(whatsapp-cli contacts search --query "Alice" | jq -r '.data[0].jid')
echo "Alice's JID: $JID"Behavior:
- Returns maximum 50 results
- Excludes group chats (only individual contacts)
- Sorted alphabetically by name
- Partial matching on both name and JID
List all chats sorted by recent activity.
Syntax:
whatsapp-cli chats list [OPTIONS]Parameters:
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--query |
string | No | - | Filter chats by name or JID |
--limit |
int | No | 20 | Maximum number of chats |
--page |
int | No | 0 | Page number for pagination |
Returns:
{
"success": true,
"data": [
{
"jid": "1234567890@s.whatsapp.net",
"name": "John Doe",
"last_message_time": "2025-10-26T10:30:00Z"
}
],
"error": null
}Examples:
# List 20 most recent chats
whatsapp-cli chats list
# List all chats (with pagination)
whatsapp-cli chats list --limit 100
# Filter chats by name
whatsapp-cli chats list --query "Team"
# Get list of all group chats
whatsapp-cli chats list | jq '.data[] | select(.jid | endswith("@g.us"))'Sorting: Chats ordered by last_message_time (most recent first)
Chat Types:
- Individual chats: JID ends with
@s.whatsapp.net - Group chats: JID ends with
@g.us
Send a text message to an individual or group.
Syntax:
whatsapp-cli send --to RECIPIENT --message TEXTParameters:
| Flag | Type | Required | Default | Description |
|---|---|---|---|---|
--to |
string | Yes | - | Phone number or JID |
--message |
string | Yes | - | Message text content |
Recipient Formats:
| Format | Example | Use Case |
|---|---|---|
| Phone number | 1234567890 |
Individual chats (auto-converted to JID) |
| Individual JID | 1234567890@s.whatsapp.net |
Individual chats |
| Group JID | 123456789@g.us |
Group chats (must use JID) |
Returns:
{
"success": true,
"data": {
"sent": true,
"recipient": "1234567890",
"message": "Hello!"
},
"error": null
}Examples:
# Send to individual (phone number)
whatsapp-cli send --to 1234567890 --message "Hello from CLI!"
# Send to individual (full JID)
whatsapp-cli send --to 1234567890@s.whatsapp.net --message "Hi there!"
# Send to group (requires JID)
whatsapp-cli send --to 123456789@g.us --message "Hello everyone!"
# Send with special characters (use quotes)
whatsapp-cli send --to 1234567890 --message "It's working! 🎉"
# Multi-line messages
whatsapp-cli send --to 1234567890 --message "Line 1
Line 2
Line 3"
# Send result of command
whatsapp-cli send --to 1234567890 --message "Server status: $(uptime)"Behavior:
- Requires active connection (authenticates if needed)
- Message stored locally in database
- Returns immediately after sending (does not wait for delivery)
- Supports Unicode (emojis, international characters)
Limitations:
- The
sendcommand supports text messages only. To download attachments, usemedia download. - No delivery/read receipt information returned
- Maximum message length: WhatsApp's standard limit (~65,536 characters)
Download media attachments (images, videos, audio, documents) that sync stored in the local database.
Syntax:
whatsapp-cli media download --message-id ID [--chat JID] [--output PATH]Parameters:
| Flag | Type | Required | Description |
|---|---|---|---|
--message-id |
string | Yes | Message identifier from messages list/search |
--chat |
string | No | Chat JID to disambiguate duplicate message IDs |
--output |
string | No | Destination file or directory (defaults to auto-structured path) |
Default storage:
- The CLI stores media next to the SQLite databases, under
STORE/media/{chat}/{message}/{media_type}/filename - The CLI sanitizes paths automatically
- If
--outputpoints to a directory, the CLI uses the original filename (or a name based on the message ID)
Return value:
{
"success": true,
"data": {
"message_id": "ABCD1234",
"chat_jid": "1234567890@s.whatsapp.net",
"path": "/path/to/media/1234567890@s.whatsapp.net/ABCD1234/image/ABCD1234.jpg",
"bytes": 204800,
"media_type": "image",
"mime_type": "image/jpeg",
"downloaded_at": "2025-02-01T12:34:56.789Z"
},
"error": null
}Examples:
# Download image using auto-organised directory layout
whatsapp-cli media download --message-id ABCD1234
# Save into a specific directory (filename auto-generated)
whatsapp-cli media download --message-id ABCD1234 --output /tmp/media/
# Save using explicit path and disambiguate by chat JID
whatsapp-cli media download --message-id XYZ987 --chat 1234567890@s.whatsapp.net --output ~/Downloads/report.pdfNotes:
- Requires that
whatsapp-cli synccaptured the message metadata first - The sync loop downloads media in the background, without blocking new messages
- If you run the command again, it overwrites the existing file with a fresh download
- Errors include metadata issues (an expired link or a missing direct path) or file permission problems
All commands return JSON in this format:
{
"success": true,
"data": <result_data>,
"error": null
}{
"success": false,
"data": null,
"error": "Error message describing what went wrong"
}| Command | Data Type | Structure |
|---|---|---|
auth |
object | {"authenticated": bool, "message": string} |
messages list |
array | [Message, ...] |
messages search |
array | [Message, ...] |
contacts search |
array | [Contact, ...] |
chats list |
array | [Chat, ...] |
send |
object | {"sent": bool, "recipient": string, "message": string} |
#!/bin/bash
# File: /usr/local/bin/daily-report.sh
RECIPIENT="1234567890" # Your phone number
# Generate report
REPORT=$(cat <<EOF
📊 Daily Report - $(date +%Y-%m-%d)
Server Status: $(systemctl is-active nginx)
Disk Usage: $(df -h / | awk 'NR==2 {print $5}')
Memory: $(free -h | awk 'NR==2 {print $3 "/" $2}')
Uptime: $(uptime -p)
EOF
)
# Send via WhatsApp
whatsapp-cli send --to "$RECIPIENT" --message "$REPORT"Cron entry:
0 9 * * * /usr/local/bin/daily-report.sh#!/bin/bash
# Send message to all contacts matching "Team"
CONTACTS=$(whatsapp-cli contacts search --query "Team" | jq -r '.data[].jid')
for JID in $CONTACTS; do
whatsapp-cli send --to "$JID" --message "Team meeting at 3 PM today!"
sleep 2 # Rate limiting
done#!/bin/bash
# Export specific chat to JSON file
JID="1234567890@s.whatsapp.net"
OUTPUT_FILE="chat_export_$(date +%Y%m%d).json"
# Get all messages (paginated)
PAGE=0
ALL_MESSAGES=[]
while true; do
RESPONSE=$(whatsapp-cli messages list --chat "$JID" --limit 100 --page $PAGE)
MESSAGES=$(echo "$RESPONSE" | jq '.data')
COUNT=$(echo "$MESSAGES" | jq 'length')
if [ "$COUNT" -eq 0 ]; then
break
fi
ALL_MESSAGES=$(echo "$ALL_MESSAGES" | jq ". + $MESSAGES")
PAGE=$((PAGE + 1))
done
echo "$ALL_MESSAGES" | jq '.' > "$OUTPUT_FILE"
echo "Exported to $OUTPUT_FILE"#!/bin/bash
# Alert when specific keywords appear in messages
ALERT_RECIPIENT="your_phone@s.whatsapp.net"
LAST_CHECK_FILE="/tmp/whatsapp_last_check"
# Get timestamp of last check
if [ -f "$LAST_CHECK_FILE" ]; then
LAST_CHECK=$(cat "$LAST_CHECK_FILE")
else
LAST_CHECK=$(date -u -d "1 hour ago" +%Y-%m-%dT%H:%M:%SZ)
fi
# Search for urgent messages since last check
MESSAGES=$(whatsapp-cli messages search --query "URGENT" --limit 100 | \
jq --arg since "$LAST_CHECK" '.data[] | select(.timestamp > $since)')
if [ -n "$MESSAGES" ]; then
COUNT=$(echo "$MESSAGES" | jq -s 'length')
whatsapp-cli send --to "$ALERT_RECIPIENT" \
--message "⚠️ $COUNT urgent messages found!"
fi
# Update last check timestamp
date -u +%Y-%m-%dT%H:%M:%SZ > "$LAST_CHECK_FILE"# Extract specific fields
whatsapp-cli contacts search --query "John" | jq '.data[0].jid'
# Output: "1234567890@s.whatsapp.net"
# Count results
whatsapp-cli chats list | jq '.data | length'
# Output: 42
# Filter and transform
whatsapp-cli messages list | jq '[.data[] | {name: .chat_name, msg: .content}]'#!/usr/bin/env python3
import subprocess
import json
def whatsapp_cli(command):
"""Execute whatsapp-cli command and return parsed JSON."""
result = subprocess.run(
['whatsapp-cli'] + command.split(),
capture_output=True,
text=True
)
return json.loads(result.stdout)
# Search contacts
contacts = whatsapp_cli('contacts search --query "Team"')
if contacts['success']:
for contact in contacts['data']:
print(f"{contact['name']}: {contact['jid']}")
# Send message
result = whatsapp_cli('send --to 1234567890 --message "Hello from Python!"')
print(f"Message sent: {result['success']}")import { execSync } from 'child_process';
interface WhatsAppResponse<T> {
success: boolean;
data: T | null;
error: string | null;
}
function whatsappCli<T>(command: string): WhatsAppResponse<T> {
const output = execSync(`whatsapp-cli ${command}`).toString();
return JSON.parse(output);
}
// Usage
const contacts = whatsappCli<Contact[]>('contacts search --query "Alice"');
if (contacts.success && contacts.data) {
const jid = contacts.data[0].jid;
whatsappCli(`send --to ${jid} --message "Hi from Node!"`);
}// Example Claude Code MCP tool wrapper
import { execSync } from 'child_process';
export const whatsappTools = {
sendMessage: async (to: string, message: string) => {
const result = execSync(
`whatsapp-cli send --to "${to}" --message "${message}"`
).toString();
return JSON.parse(result);
},
searchContacts: async (query: string) => {
const result = execSync(
`whatsapp-cli contacts search --query "${query}"`
).toString();
return JSON.parse(result);
},
getRecentMessages: async (chatJid: string, limit = 20) => {
const result = execSync(
`whatsapp-cli messages list --chat "${chatJid}" --limit ${limit}`
).toString();
return JSON.parse(result);
}
};
// Use in Claude Code
const contacts = await whatsappTools.searchContacts("Alice");
if (contacts.success) {
await whatsappTools.sendMessage(
contacts.data[0].jid,
"Automated message from Claude Code!"
);
}interface Message {
id: string; // Unique message ID
chat_jid: string; // Chat identifier (JID)
chat_name: string; // Display name of chat
sender: string; // Phone number of sender
content: string; // Message text content
timestamp: string; // ISO 8601 timestamp
is_from_me: boolean; // true if sent by you
media_type?: string; // "image", "video", "audio", "document", or ""
}Example:
{
"id": "3EB0F2A8B9C4D1E5F6A7",
"chat_jid": "1234567890@s.whatsapp.net",
"chat_name": "John Doe",
"sender": "1234567890",
"content": "See you at the meeting!",
"timestamp": "2025-10-26T14:30:00Z",
"is_from_me": false,
"media_type": ""
}interface Contact {
phone_number: string; // Phone number (without country code prefix)
name: string; // Display name
jid: string; // WhatsApp JID
}Example:
{
"phone_number": "1234567890",
"name": "John Doe",
"jid": "1234567890@s.whatsapp.net"
}interface Chat {
jid: string; // Chat identifier
name: string; // Display name
last_message_time: string; // ISO 8601 timestamp of last message
}Example:
{
"jid": "123456789@g.us",
"name": "Project Team",
"last_message_time": "2025-10-26T16:45:00Z"
}WhatsApp uses JIDs to identify chats:
| Type | Format | Example |
|---|---|---|
| Individual | {phone}@s.whatsapp.net |
1234567890@s.whatsapp.net |
| Group | {group_id}@g.us |
120363012345678901@g.us |
Extracting Components:
# Get phone number from individual JID
echo "1234567890@s.whatsapp.net" | cut -d'@' -f1
# Output: 1234567890
# Check if JID is a group
echo "$JID" | grep -q "@g.us" && echo "Group" || echo "Individual"Default storage directory: ./store/
store/
├── whatsapp.db # Session data (managed by whatsmeow)
└── messages.db # Message history (managed by CLI)
Custom Location:
whatsapp-cli --store /var/lib/whatsapp chats list- Format: SQLite3
- Managed by: whatsmeow library
- Contents:
- Device ID and encryption keys
- Contact information
- Session tokens
- Persistence: ~20 days before re-authentication required
- Security: Contains sensitive data. Protect it with file permissions.
Recommended Permissions:
chmod 600 store/whatsapp.db
chown $USER:$USER store/whatsapp.db- Format: SQLite3
- Managed by: WhatsApp CLI
- Schema:
-- Chats table
CREATE TABLE chats (
jid TEXT PRIMARY KEY,
name TEXT,
last_message_time TIMESTAMP
);
-- Messages table
CREATE TABLE messages (
id TEXT,
chat_jid TEXT,
sender TEXT,
content TEXT,
timestamp TIMESTAMP,
is_from_me BOOLEAN,
media_type TEXT,
filename TEXT,
url TEXT,
media_key BLOB,
file_sha256 BLOB,
file_enc_sha256 BLOB,
file_length INTEGER,
PRIMARY KEY (id, chat_jid),
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
);# Open with sqlite3
sqlite3 store/messages.db
# Query examples
sqlite> SELECT COUNT(*) FROM messages;
sqlite> SELECT chat_name, COUNT(*) FROM messages
JOIN chats ON messages.chat_jid = chats.jid
GROUP BY chat_name;
sqlite> .quit# Backup
tar -czf whatsapp-backup-$(date +%Y%m%d).tar.gz store/
# Restore
tar -xzf whatsapp-backup-20251026.tar.gz
# Migrate to new machine
rsync -avz store/ newserver:/path/to/store/- First Run: QR code displayed
- User Action: Scan with WhatsApp mobile app
- Session Creation: Credentials saved to
store/whatsapp.db - Subsequent Runs: Auto-reconnect using saved session
- Expiration: After ~20 days, re-authentication required
The store/whatsapp.db file contains your WhatsApp session credentials. Treat it like a password:
# Set proper permissions
chmod 600 store/whatsapp.db
chmod 700 store/
# Never commit to version control
echo "store/" >> .gitignore
# Use environment variable for custom location
export WHATSAPP_STORE="/secure/path/to/store"
whatsapp-cli --store "$WHATSAPP_STORE" chats listWhatsApp allows up to 5 linked devices. If you reach this limit:
- Open WhatsApp on phone
- Go to Settings → Linked Devices
- Remove an old device
- Re-authenticate CLI
- All communication with WhatsApp uses end-to-end encryption
- CLI communicates via WhatsApp Web protocol (WSS)
- No data sent to third parties
- Local storage only
- Message Storage: All messages stored locally in SQLite
- No Cloud Sync: Data never leaves your machine
- Media: Metadata stored, actual files downloaded on-demand
- Deletion: Delete
store/directory to remove all data
┌─────────────────────────────────────────────────┐
│ whatsapp-cli │
│ (Go Binary) │
├─────────────────────────────────────────────────┤
│ ┌───────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Commands │ │ Client │ │ Storage │ │
│ │ Layer │→ │ Wrapper │→ │ (SQLite) │ │
│ └───────────┘ └──────────┘ └─────────────┘ │
│ ↓ ↓ │
│ ┌───────────┐ ┌──────────┐ │
│ │ Output │ │whatsmeow │ │
│ │ (JSON) │ │ Library │ │
│ └───────────┘ └──────────┘ │
│ ↓ │
└──────────────────────┼──────────────────────────┘
↓
┌────────────────┐
│ WhatsApp Web │
│ API (WSS) │
└────────────────┘
| Layer | Location | Responsibility |
|---|---|---|
| CLI | main.go |
Argument parsing, command routing |
| Commands | internal/commands/ |
Business logic for each command |
| Client | internal/client/ |
WhatsApp protocol wrapper |
| Storage | internal/store/ |
Database operations |
| Output | internal/output/ |
JSON formatting |
whatsmeow (github.com/tulir/whatsmeow)
├── WebSocket communication
├── Protocol buffer encoding/decoding
├── End-to-end encryption
└── Session management
go-sqlite3 (github.com/mattn/go-sqlite3)
└── SQLite database driver
qrterminal (github.com/mdp/qrterminal)
└── QR code rendering in terminal
# Build with all dependencies statically linked
go build -ldflags="-s -w" -o whatsapp-cli .
# Cross-compile
GOOS=linux GOARCH=amd64 go build -o whatsapp-cli-linux .
GOOS=darwin GOARCH=arm64 go build -o whatsapp-cli-mac .
GOOS=windows GOARCH=amd64 go build -o whatsapp-cli.exe .Binary Size Optimization:
# Standard build
go build -o whatsapp-cli . # ~21MB
# Optimized build
go build -ldflags="-s -w" . # ~15MB
# Ultra-compressed (requires UPX)
upx --best --lzma whatsapp-cli # ~5MBFor more about the architecture, see docs/architecture.md.
{
"success": false,
"data": null,
"error": "not authenticated"
}Solution: Run whatsapp-cli auth
{
"success": false,
"data": null,
"error": "failed to connect: connection refused"
}Solutions:
- Check internet connection
- Verify WhatsApp is active on phone
- Check firewall rules
{
"success": false,
"data": null,
"error": "invalid JID format"
}Solution: Use the correct JID format (phone@s.whatsapp.net or id@g.us)
{
"success": false,
"data": null,
"error": "database is locked"
}Solution: Close other instances of whatsapp-cli
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error (check JSON error field) |
Problem: QR code does not appear
# Check terminal supports UTF-8
echo $LANG
# Should output: en_US.UTF-8 or similar
# Try with different QR code size
QRSIZE=small whatsapp-cli authProblem: "Session expired" after a few days
- WhatsApp sessions expire after ~20 days
- Re-run
whatsapp-cli auth - Sessions also expire if you unlink the device from the phone
Problem: "Connection timeout"
# Test network connectivity
ping -c 3 web.whatsapp.com
# Check DNS resolution
nslookup web.whatsapp.com
# Try with custom store path (permission issues)
whatsapp-cli --store /tmp/wa-test authProblem: "WebSocket connection failed"
- Corporate firewalls can block WSS
- A VPN can interfere with the connection
- Try a different network
WhatsApp sometimes requires a newer desktop or web build. When that happens, the bundled whatsmeow dependency logs Client outdated (405). It then closes the websocket with code 1006. As a result, whatsapp-cli stops right after you scan the QR code.
To fix it:
- Bump the dependency:
go get go.mau.fi/whatsmeow@latest go mod tidy
- Rebuild the binary:
go build -o whatsapp-cli . - Verify: Run
./whatsapp-cli version, then./whatsapp-cli author./whatsapp-cli sync. You will see✓ Connected to WhatsAppwith noClient outdated (405)messages.
If the upstream library does not yet have a new version, watch the tulir/whatsmeow issues for Client outdated (405) reports. Once a fix lands, repeat the steps above to update your local binary.
Problem: "Database is locked"
# Check for other processes
ps aux | grep whatsapp-cli
# Check for stale lock files
rm -f store/*.db-shm store/*.db-wal
# If corrupted, restore from backup
mv store/messages.db store/messages.db.bak
# Re-run cli to recreateProblem: "Disk full" or "No space left"
# Check database size
du -h store/messages.db
# Vacuum database to reclaim space
sqlite3 store/messages.db "VACUUM;"
# Delete old messages
sqlite3 store/messages.db "DELETE FROM messages WHERE timestamp < datetime('now', '-30 days');"Problem: Messages not appearing in searches
- The CLI searches only messages stored locally
- Messages received before the CLI was running are not stored
- To re-sync, list messages from specific chats
Problem: Cannot send to a group
- Use the group JID (it ends in
@g.us), not a phone number - Get the group JID from the
chats listcommand
Problem: Slow searches on large databases
# Add indexes
sqlite3 store/messages.db <<EOF
CREATE INDEX IF NOT EXISTS idx_content ON messages(content);
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
CREATE INDEX IF NOT EXISTS idx_chat_jid ON messages(chat_jid);
EOF# Enable verbose logging (if implemented)
export WHATSAPP_DEBUG=1
whatsapp-cli chats list
# Check SQLite directly
sqlite3 store/messages.db "SELECT * FROM messages LIMIT 5;"# Clone repository
git clone https://github.com/vicentereig/whatsapp-cli.git
cd whatsapp-cli
# Install dependencies
go mod download
# Run tests
go test ./...
# Run with race detector
go test -race ./...
# Build development version
go build -o whatsapp-cli-dev .Some environments, for example sandboxes or CI systems, cannot write to the global Go module cache. If your environment has this limit, point GOMODCACHE to a writable directory in the repository before you build:
GOMODCACHE=$(pwd)/.gomodcache go build ./...This command compiles the full project and stores downloaded modules under .gomodcache. The workspace then stays self-contained.
# All tests
go test ./...
# Specific package
go test ./internal/store
# With coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Verbose output
go test -v ./...
# Run specific test
go test -run TestStoreMessage ./internal/storeinternal/
├── client/
│ └── client.go # WhatsApp client wrapper
├── commands/
│ └── commands.go # CLI command implementations
├── output/
│ ├── output.go # JSON formatting
│ └── output_test.go # Output tests
└── store/
├── store.go # Database operations
└── store_test.go # Storage tests
- Define command in
internal/commands/commands.go:
func (a *App) NewCommand(param string) string {
// Implementation
result := doSomething(param)
return output.Success(result)
}- Add routing in
main.go:
case "newcommand":
cmdFlags := flag.NewFlagSet("newcommand", flag.ExitOnError)
param := cmdFlags.String("param", "", "description")
cmdFlags.Parse(args[1:])
result = app.NewCommand(*param)- Write tests in
internal/commands/commands_test.go:
func TestNewCommand(t *testing.T) {
// Test implementation
}Follow TDD (Test-Driven Development):
- Write test first
- Run test (it fails)
- Implement feature
- Run test (it passes)
- Refactor
Example:
// store_test.go
func TestStoreMessage(t *testing.T) {
store := setupTestDB(t)
err := store.StoreMessage("id1", "chat@s.whatsapp.net", "sender",
"Hello", time.Now(), false, "", "", "", nil, nil, nil, 0)
assert.NoError(t, err)
}See CONTRIBUTING.md for guidelines.
Q: Is this an official WhatsApp tool? A: No. This is a third-party tool. It uses the unofficial WhatsApp Web protocol, through the whatsmeow library.
Q: Can I use this with WhatsApp Business? A: Yes. It works with personal and business accounts.
Q: Does this work on multiple devices simultaneously? A: Yes. WhatsApp supports up to 5 linked devices.
Q: Can I send images/videos? A: Not yet. The CLI currently sends text only. Media support is planned for a future release.
Q: Can I create/manage groups? A: Not yet. The CLI currently gives read-only access to groups.
Q: Can I see delivery/read receipts? A: Not in the current version. This feature is planned for a future release.
Q: Can I receive real-time messages?
A: Yes. Run whatsapp-cli sync to receive and store messages continuously. Run it in the background or in a tmux session.
Q: Why Go instead of Python/Node.js? A: Go gives single-binary distribution and strong performance. Also, whatsmeow is written in Go.
Q: Can I run this in Docker?
A: Yes, but QR code authentication needs terminal access. Use the -it flags for interactive mode.
Q: Does it support proxy/VPN?
A: The CLI respects system proxy settings. Set the HTTP_PROXY and HTTPS_PROXY environment variables.
Q: Can I access messages from before installing CLI?
A: Yes. When you run whatsapp-cli sync, it downloads message history from WhatsApp servers. The amount of history depends on how long WhatsApp retains it on its servers.
Q: Is my data safe?
A: The CLI stores all data locally, with no cloud sync. Protect your store/ directory.
Q: Can WhatsApp detect/ban this? A: The CLI uses the official WhatsApp Web protocol. The risk is low, but use it at your own discretion.
Q: Is end-to-end encryption maintained? A: Yes. WhatsApp's protocol encrypts all messages end-to-end.
- Vicente Reig - Initial development
- Built with assistance from Claude Code (Anthropic)
- whatsmeow by Tulir Asokan - WhatsApp Web client library
- go-sqlite3 - SQLite driver
- qrterminal - QR code generation
MIT License - see LICENSE file
- Issues: https://github.com/vicentereig/whatsapp-cli/issues
- Discussions: https://github.com/vicentereig/whatsapp-cli/discussions
- Pull Requests: Welcome! See CONTRIBUTING.md
- WhatsApp for the multidevice protocol
- Tulir Asokan for the excellent whatsmeow library
- Go community for excellent tooling
- Anthropic for Claude Code
Last Updated: 2025-12-13 Version: 1.3.2 Documentation: https://github.com/vicentereig/whatsapp-cli/blob/main/README.md