Skip to main content
This guide will help you create a simple WhatsApp bot that responds to messages. You’ll learn the core concepts and have a working bot by the end.

Basic example

Here’s a minimal bot that responds to “ping” messages:
src/main.rs
use std::sync::Arc;
use whatsapp_rust::bot::Bot;
use whatsapp_rust::TokioRuntime;
use whatsapp_rust::store::SqliteStore;
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
use whatsapp_rust_ureq_http_client::UreqHttpClient;
use wacore::types::events::Event;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize storage backend
    let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);

    // Build the bot
    let mut bot = Bot::builder()
        .with_backend(backend)
        .with_transport_factory(TokioWebSocketTransportFactory::new())
        .with_http_client(UreqHttpClient::new())
        .with_runtime(TokioRuntime)
        .on_event(|event, client| async move {
            match &*event {
                Event::PairingQrCode { code, .. } => {
                    println!("Scan this QR code with WhatsApp:\n{}", code);
                }
                Event::Message(msg, info) => {
                    println!("Message from {}: {:?}", info.source.sender, msg);
                }
                _ => {}
            }
        })
        .build()
        .await?;

    // Start the bot
    bot.run().await?.await?;
    Ok(())
}

Step-by-step breakdown

1

Set up the storage backend

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:
.on_event(|event, client| async move {
    match &*event {
        Event::PairingQrCode { code, .. } => {
            println!("QR Code:\n{}", code);
        }
        Event::Message(msg, info) => {
            // Handle incoming message
        }
        Event::Connected(_) => {
            println!("Connected successfully!");
        }
        _ => {}
    }
})
The event handler receives two parameters:
  • event: An Arc<Event> — use &*event or event.as_ref() to pattern-match on the inner event type
  • client: An Arc<Client> you can use to send messages or call API methods
4

Build and run the bot

Build the bot and start the event loop:
.build()
.await?;

bot.run().await?.await?;
The double .await? is intentional:
  • First .await? starts the bot and returns a BotHandle
  • Second .await? waits for the bot to finish running

Responding to messages

Let’s extend the bot to respond to “ping” with “pong”:
use wacore::proto_helpers::MessageExt;
use waproto::whatsapp as wa;

.on_event(|event, client| async move {
    match &*event {
        Event::PairingQrCode { code, .. } => {
            println!("QR Code:\n{}", code);
        }
        Event::Message(msg, info) => {
            // Check if message is a text message saying "ping"
            if let Some(text) = msg.text_content() {
                if text == "ping" {
                    // Create reply message
                    let reply = wa::Message {
                        conversation: Some("pong".to_string()),
                        ..Default::default()
                    };

                    // Send the reply
                    if let Err(e) = client.send_message(info.source.chat.clone(), reply).await {
                        eprintln!("Failed to send reply: {}", e);
                    }
                }
            }
        }
        _ => {}
    }
})

Key methods

  • msg.text_content() - Extract text from any message type (conversation, extended text, etc.)
  • client.send_message() - Send a message to a chat
  • info.source.chat - The JID (identifier) of the chat where the message came from
  • info.source.sender - The JID of the user who sent the message

Authentication methods

QR code pairing (default)

The bot automatically generates QR codes when not authenticated. Scan with your phone to link:
Event::PairingQrCode { code, .. } => {
    println!("Scan this QR code:\n{}", code);
}

Pair code (phone number)

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.

Running the bot

1

First run - Authentication

On the first run, the bot will generate a QR code:
cargo run
Scan the QR code with WhatsApp on your phone:
  1. Open WhatsApp on your phone
  2. Go to Settings → Linked Devices
  3. Tap “Link a Device”
  4. Scan the QR code displayed in your terminal
2

Subsequent runs - Auto-login

After pairing, the session is saved. The bot will automatically reconnect:
cargo run
You should see:
Connected successfully!
3

Test the bot

Send “ping” to your bot from any WhatsApp chat. It should reply with “pong”!

Demo binary CLI flags

The repository includes a demo bot binary (src/main.rs) that supports CLI arguments for authentication:
cargo run                                      # QR code pairing only
cargo run -- --phone 15551234567               # Pair code + QR code (concurrent)
cargo run -- -p 15551234567                    # Short form
cargo run -- -p 15551234567 --code MYCODE12    # Custom 8-char pair code
cargo 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.

Using MessageContext

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);
            }
        }
    }
}

Media forwarding with CDN reuse

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
}
See the media forwarding guide for more details.

Complete example with logging

Here’s a production-ready example with proper logging, reactions, message editing, and media CDN reuse:
src/main.rs
use chrono::{Local, Utc};
use log::{error, info};
use std::sync::Arc;
use wacore::proto_helpers::MessageExt;
use wacore::types::events::Event;
use waproto::whatsapp as wa;
use whatsapp_rust::bot::{Bot, MessageContext};
use whatsapp_rust::TokioRuntime;
use whatsapp_rust::store::SqliteStore;
use whatsapp_rust_tokio_transport::TokioWebSocketTransportFactory;
use whatsapp_rust_ureq_http_client::UreqHttpClient;

const PING_TRIGGER: &str = "🦀ping";
const PONG_TEXT: &str = "🏓 Pong!";

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
        .format(|buf, record| {
            use std::io::Write;
            writeln!(
                buf,
                "{} [{:<5}] [{}] - {}",
                Local::now().format("%H:%M:%S"),
                record.level(),
                record.target(),
                record.args()
            )
        })
        .init();

    let backend = Arc::new(SqliteStore::new("whatsapp.db").await?);
    info!("SQLite backend initialized");

    let mut bot = Bot::builder()
        .with_backend(backend)
        .with_transport_factory(TokioWebSocketTransportFactory::new())
        .with_http_client(UreqHttpClient::new())
        .with_runtime(TokioRuntime)
        .on_event(|event, client| async move {
            match &*event {
                Event::PairingQrCode { code, .. } => {
                    println!("\n{}", code);
                }
                Event::Message(msg, info) => {
                    let ctx = MessageContext::from_parts(msg, info, client);
                    handle_message(&ctx).await;
                }
                Event::Connected(_) => info!("Bot connected!"),
                Event::LoggedOut(_) => error!("Bot was logged out!"),
                _ => {}
            }
        })
        .build()
        .await?;

    info!("Starting bot...");
    bot.run().await?.await?;
    Ok(())
}

async fn handle_message(ctx: &MessageContext) {
    // Try CDN-reuse media reply first (instant, no download needed)
    if let Some(reply) = build_media_pong(&ctx.message) {
        if let Err(e) = ctx.send_message(reply).await {
            error!("Failed to send media pong: {}", e);
        }
        return;
    }

    // Handle text ping
    if ctx.message.text_content() == Some(PING_TRIGGER) {
        let context_info = ctx.build_quote_context();
        let reply = wa::Message {
            extended_text_message: Some(Box::new(wa::message::ExtendedTextMessage {
                text: Some(PONG_TEXT.to_string()),
                context_info: Some(Box::new(context_info)),
                ..Default::default()
            })),
            ..Default::default()
        };

        if let Err(e) = ctx.send_message(reply).await {
            error!("Failed to send pong: {}", e);
        }
    }
}

/// Reuses the original CDN blob, only swaps the caption.
/// Instant regardless of file size — no download or re-upload.
fn build_media_pong(message: &wa::Message) -> Option<wa::Message> {
    let base = message.get_base_message();
    if let Some(img) = &base.image_message
        && img.caption.as_deref() == Some(PING_TRIGGER)
    {
        return Some(wa::Message {
            image_message: Some(Box::new(wa::message::ImageMessage {
                caption: Some(PONG_TEXT.to_string()),
                ..*img.clone()
            })),
            ..Default::default()
        });
    }
    if let Some(vid) = &base.video_message
        && vid.caption.as_deref() == Some(PING_TRIGGER)
    {
        return Some(wa::Message {
            video_message: Some(Box::new(wa::message::VideoMessage {
                caption: Some(PONG_TEXT.to_string()),
                ..*vid.clone()
            })),
            ..Default::default()
        });
    }
    None
}

Configuring log targets

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.

Available log targets

TargetDescription
Client/KeepaliveKeepalive ping/pong and dead socket detection
Client/RecvIncoming frame processing (unmarshal, decompress)
Client/SendOutgoing message encryption and dispatch
Client/OfflineSyncOffline message sync progress and timing
Client/AppStateApp state sync (contacts, settings, etc.)
Client/AccountSyncAccount-level sync operations
Client/PairCodePair code authentication flow
Client/PairQR code pairing flow
Client/ReceiptReceipt processing (read, delivered, played)
Client/TcTokenTrusted contact token operations
Client/GroupGroup metadata and participant operations
Client/ContactsContact sync and lookup
Client/BusinessBusiness profile updates
Client/PDOPeer Data Operations — message recovery from primary phone via bare-JID peer messaging and retry fallback
Client/IQIQ stanza send/receive
Client/AckStanza acknowledgment
Client/StatusStatus/story operations
Client/PictureProfile picture updates
Client/UnifiedSessionSession establishment
BlockingBlock/unblock operations
ChatstateTyping indicator events
PresenceHandlerPresence update processing
AppStateApp state patch encoding/decoding
Bot/PairCodeBot-level pair code handling

Filtering examples

# Show only connection and messaging logs
RUST_LOG="Client/Keepalive=debug,Client/Send=debug,Client/Recv=trace" cargo run

# Debug offline sync issues
RUST_LOG="Client/OfflineSync=debug" cargo run

# Quiet mode: only errors and warnings
RUST_LOG="warn" cargo run

# Verbose: all client internals at debug level
RUST_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.

Running with Docker

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.

Benchmarking

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
See the wacore benchmarks documentation for details on each suite, allocation optimizations, and CI integration.

Next steps

Sending messages

Learn about different message types and how to send them

Media handling

Upload and download images, videos, and documents

Group management

Create and manage WhatsApp groups

Client API reference

Explore all available client methods