The bot needs persistent storage for session data, keys, and state:
let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);
This creates a SQLite database file named whatsapp.db in your current directory. The session will persist across restarts.
2
Configure the bot builder
The Bot::builder() pattern lets you configure all required components:
let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) .with_runtime(TokioRuntime)
All four components (backend, transport, HTTP client, runtime) are required. The builder uses a typestate pattern — your code won’t compile if any are missing.
3
Handle events
Use .on_event() to handle incoming events from WhatsApp:
Alternatively, link using a phone number and 8-digit code:
use whatsapp_rust::pair_code::PairCodeOptions;let mut bot = Bot::builder() .with_backend(backend) .with_transport_factory(TokioWebSocketTransportFactory::new()) .with_http_client(UreqHttpClient::new()) .with_runtime(TokioRuntime) .with_pair_code(PairCodeOptions { phone_number: "15551234567".to_string(), ..Default::default() }) .on_event(|event, client| async move { match &*event { Event::PairingCode { code, .. } => { println!("Enter this code on your phone: {}", code); } _ => {} } }) .build() .await?;
PairCodeOptions defaults to PlatformId::Chrome with "Chrome (Linux)" as the display name. You can customize these if needed:
use whatsapp_rust::pair_code::{PairCodeOptions, PlatformId};PairCodeOptions { phone_number: "15551234567".to_string(), show_push_notification: true, custom_code: Some("ABCD1234".to_string()), // or None for random platform_id: PlatformId::Chrome, platform_display: "Chrome (Linux)".to_string(),}
Pair code and QR code authentication run concurrently. Whichever method completes first will be used.
The repository includes a demo bot binary (src/main.rs) that supports CLI arguments for authentication:
cargo run # QR code pairing onlycargo run -- --phone 15551234567 # Pair code + QR code (concurrent)cargo run -- -p 15551234567 # Short formcargo run -- -p 15551234567 --code MYCODE12 # Custom 8-char pair codecargo run -- -p 15551234567 -c MYCODE12 # Short form
The demo bot responds to 🦀ping with a quoted 🏓 Pong! reply, edits the reply to append the send latency, and supports media ping/pong via CDN reuse.
For cleaner message handling, use MessageContext to wrap the message, metadata, and client together. This provides convenience methods like send_message (auto-targets the source chat), build_quote_context, edit_message, and revoke_message:
use whatsapp_rust::bot::{Bot, MessageContext};.on_event(|event, client| async move { if let Some(ctx) = MessageContext::from_event(&event, client) { handle_message(&ctx).await; }})
Then define focused handler functions:
async fn handle_message(ctx: &MessageContext) { if let Some(text) = ctx.message.text_content() { if text == "ping" { let reply = wa::Message { conversation: Some("pong".to_string()), ..Default::default() }; if let Err(e) = ctx.send_message(reply).await { eprintln!("Failed to send: {}", e); } } }}
You can also forward media instantly by reusing the original CDN fields — no download or re-upload needed:
/// Reuses the original CDN blob, only swaps the caption./// Instant regardless of file size.fn build_media_reply(message: &wa::Message) -> Option<wa::Message> { let base = message.get_base_message(); if let Some(img) = &base.image_message { return Some(wa::Message { image_message: Some(Box::new(wa::message::ImageMessage { caption: Some("Received your image!".to_string()), ..*img.clone() })), ..Default::default() }); } None}
whatsapp-rust uses the log crate with module-specific targets for fine-grained filtering. You can use RUST_LOG to control which components emit log output.
# Show only connection and messaging logsRUST_LOG="Client/Keepalive=debug,Client/Send=debug,Client/Recv=trace" cargo run# Debug offline sync issuesRUST_LOG="Client/OfflineSync=debug" cargo run# Quiet mode: only errors and warningsRUST_LOG="warn" cargo run# Verbose: all client internals at debug levelRUST_LOG="debug" cargo run
During shutdown or disconnect, the client automatically downgrades sync errors from error to debug level to reduce noise. This means you won’t see spurious error logs when the client is intentionally disconnecting.
You can also run the bot using Docker instead of compiling locally:
docker build -t whatsapp-rust .docker run -v ./data:/data whatsapp-rust
Session data is stored in the /data directory inside the container. Mount a volume to persist it across restarts. The container shuts down gracefully on docker stop — the bot disconnects cleanly from WhatsApp before exiting. See the installation guide for more details.
The repository includes a benchmark example at examples/benchmark.rs that you can use for quick integration-level performance testing. It uses an in-memory backend and supports a custom WebSocket URL via the WHATSAPP_WS_URL environment variable:
cargo run --example benchmark --features danger-skip-tls-verify
The benchmark example requires the danger-skip-tls-verify feature flag because it’s designed for use with local test servers.
For more comprehensive integration benchmarks with allocation tracking, the bench-integration test suite measures real-world scenarios (connect, send, receive, reconnect) and reports wall-clock time plus heap allocation counts per operation:
# Requires a mock server (e.g., Bartender)MOCK_SERVER_URL="wss://127.0.0.1:8080/ws/chat" \ cargo run -p bench-integration --release
For low-level protocol benchmarks, the wacore crate includes an iai-callgrind benchmark suite that measures instruction counts for the full send/receive pipeline (DM and group messaging with various participant counts), binary protocol encoding, Signal Protocol operations, and reporting token generation:
# Run all wacore benchmarks (requires valgrind and iai-callgrind-runner)cargo bench --workspace