> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Build your first WhatsApp bot in minutes

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

Add the dependencies — one crate is enough; `whatsapp-rust` re-exports the entire stack:

```toml Cargo.toml theme={null}
[dependencies]
whatsapp-rust = "0.6"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Here's a minimal bot that responds to "ping" messages:

```rust src/main.rs theme={null}
use whatsapp_rust::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let bot = Bot::builder()
        .with_backend(SqliteStore::new("whatsapp.db").await?)
        .on_qr_code(|code, _timeout| async move {
            println!("Scan this QR code with WhatsApp:\n{code}");
        })
        .on_message(|ctx| async move {
            if ctx.message.text_content() == Some("ping") {
                if let Err(e) = ctx.reply("pong").await {
                    eprintln!("Failed to reply: {e}");
                }
            }
        })
        .build()
        .await?;

    // Runs until logout or client.disconnect() from another task.
    bot.run().await;
    Ok(())
}
```

## Step-by-step breakdown

<Steps>
  <Step title="Set up the storage backend">
    The bot needs persistent storage for session data, keys, and state:

    ```rust theme={null}
    let backend = SqliteStore::new("whatsapp.db").await?;
    ```

    This creates a SQLite database file named `whatsapp.db` in your current directory. The session will persist across restarts.
  </Step>

  <Step title="Configure the bot builder">
    The `Bot::builder()` pattern lets you configure all components:

    ```rust theme={null}
    let bot = Bot::builder()
        .with_backend(backend)
    ```

    <Note>
      With the default cargo features, only the backend is required. The Tokio WebSocket transport, ureq HTTP client, and Tokio runtime are pre-wired. Use `with_transport_factory`, `with_http_client`, and `with_runtime` to override any of them (for example when targeting WASM or a custom transport).
    </Note>
  </Step>

  <Step title="Register event handlers">
    Typed registrars cover the common cases — no `match &*event` needed:

    ```rust theme={null}
    .on_qr_code(|code, _timeout| async move {
        println!("QR Code:\n{code}");
    })
    .on_message(|ctx| async move {
        // ctx.message, ctx.info, ctx.reply(...), ctx.react(...)
        println!("From {}: {:?}", ctx.info.source.sender, ctx.message.text_content());
    })
    .on_connected(|_client| async {
        println!("Connected successfully!");
    })
    ```

    Available typed registrars: `on_message`, `on_qr_code`, `on_pair_code`, `on_connected`, `on_logged_out`. Use `on_event` or `on_event_for` as catch-alls for events without a typed registrar. Multiple handlers of any kind accumulate — registering a second one no longer silently replaces the first.

    `on_message` delivers a ready [`MessageContext`](/api/bot#messagecontext) with `ctx.reply(text)`, `ctx.reply_quoting(text)`, `ctx.react(emoji)`, and `ctx.send_message(msg)` helpers.
  </Step>

  <Step title="Build and run the bot">
    Build the bot and start the event loop:

    ```rust theme={null}
    .build()
    .await?;

    bot.run().await;
    ```

    `bot.run().await` runs the bot on the current task until it disconnects or logs out. To run in the background instead, use `bot.spawn()`, which returns a [`BotHandle`](/api/bot) with `client()`, `shutdown()`, and `abort()`.
  </Step>
</Steps>

## Responding to messages

Let's extend the bot to respond to "ping" with "pong" using the `on_message` helper and `ctx.reply`:

```rust theme={null}
use whatsapp_rust::prelude::*;

.on_qr_code(|code, _timeout| async move {
    println!("QR Code:\n{code}");
})
.on_message(|ctx| async move {
    if ctx.message.text_content() == Some("ping") {
        if let Err(e) = ctx.reply("pong").await {
            eprintln!("Failed to send reply: {e}");
        }
    }
})
```

### Key methods

* `msg.text_content()` — Extract text from any message type (conversation, extended text, etc.)
* `ctx.reply(text)` — Send a plain-text reply in the same chat without quoting
* `ctx.reply_quoting(text)` — Send a plain-text reply that quotes the received message
* `ctx.react(emoji)` — React to the received message
* `ctx.send_message(msg)` — Send an arbitrary `wa::Message` to the source chat
* `client.send_text(jid, text)` — Send a plain-text message from a `Client` reference
* `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

For raw protobuf access (advanced cases), build a `wa::Message` directly:

```rust theme={null}
use whatsapp_rust::prelude::*;

.on_message(|ctx| async move {
    if ctx.message.text_content() == Some("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 reply: {e}");
        }
    }
})
```

Or with the `MessageBuilderExt` helpers:

```rust theme={null}
use whatsapp_rust::prelude::*; // re-exports MessageBuilderExt

let msg = wa::Message::text("pong");
let msg_with_quote = wa::Message::text_with_context("pong", ctx.build_quote_context());
```

## Authentication methods

### QR code pairing (default)

The bot automatically generates QR codes when not authenticated. Display them with `on_qr_code`:

```rust theme={null}
.on_qr_code(|code, timeout| async move {
    println!("Scan this QR code (valid {}s):\n{code}", timeout.as_secs());
})
```

### Pair code (phone number)

Alternatively, link using a phone number and 8-digit code:

```rust theme={null}
use whatsapp_rust::prelude::*;
use whatsapp_rust::pair_code::PairCodeOptions;

let bot = Bot::builder()
    .with_backend(SqliteStore::new("whatsapp.db").await?)
    .with_pair_code(PairCodeOptions {
        phone_number: "15551234567".to_string(),
        ..Default::default()
    })
    .on_pair_code(|code, _timeout| async move {
        println!("Enter this code on your phone: {code}");
    })
    .build()
    .await?;
```

`PairCodeOptions` derives `companion_platform_id` and `companion_platform_display` from the device's `PlatformType` by default (Chrome with `Chrome (Linux)` for the stock web profile). You can override the wire id when needed:

```rust theme={null}
use whatsapp_rust::pair_code::PairCodeOptions;
use wacore::companion_reg::CompanionWebClientType;

PairCodeOptions {
    phone_number: "15551234567".to_string(),
    show_push_notification: true,
    custom_code: Some("ABCD1234".to_string()), // or None for random
    // `None` auto-derives from `Device.device_props.platform_type`.
    platform_id: Some(CompanionWebClientType::Chrome),
}
```

<Note>
  `platform_id` accepts the [`CompanionWebClientType`](/concepts/authentication#companionwebclienttype) wire enum (single-byte ASCII ids). The display string is always derived — there is no separate `platform_display` field.
</Note>

<Note>
  Pair code and QR code authentication run concurrently. Whichever method completes first will be used.
</Note>

## Running the bot

<Steps>
  <Step title="First run - Authentication">
    On the first run, the bot will generate a QR code:

    ```bash theme={null}
    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
  </Step>

  <Step title="Subsequent runs - Auto-login">
    After pairing, the session is saved. The bot will automatically reconnect:

    ```bash theme={null}
    cargo run
    ```

    You should see:

    ```
    Connected successfully!
    ```
  </Step>

  <Step title="Test the bot">
    Send "ping" to your bot from any WhatsApp chat. It should reply with "pong"!
  </Step>
</Steps>

### Demo binary CLI flags

The repository includes a demo bot example (`examples/demo.rs`) that supports CLI arguments for authentication:

```bash theme={null}
cargo run --example demo                                      # QR code pairing only
cargo run --example demo -- --phone 15551234567               # Pair code + QR code (concurrent)
cargo run --example demo -- -p 15551234567                    # Short form
cargo run --example demo -- -p 15551234567 --code MYCODE12    # Custom 8-char pair code
cargo run --example demo -- -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

`on_message` delivers a ready `MessageContext`, so you no longer need to extract it from `Arc<Event>` manually. Use typed handler functions for cleaner separation:

```rust theme={null}
use whatsapp_rust::prelude::*;

.on_message(|ctx| async move {
    handle_message(ctx).await;
})
```

Then define focused handler functions:

```rust theme={null}
async fn handle_message(ctx: MessageContext) {
    if ctx.message.text_content() == Some("ping") {
        if let Err(e) = ctx.reply("pong").await {
            eprintln!("Failed to send: {e}");
        }
    }
}
```

`MessageContext` provides convenience methods including `send_message` (auto-targets the source chat), `reply`, `reply_quoting`, `react`, `build_quote_context`, `edit_message`, and `revoke_message`.

### Media forwarding with CDN reuse

You can also forward media instantly by reusing the original CDN fields — no download or re-upload needed:

<Note>
  Constructing a message with a sub-message field (like `image_message` below) requires `MessageField`. `whatsapp-rust` re-exports it directly from `prelude`, so `use whatsapp_rust::prelude::*` is enough — no extra `Cargo.toml` entry needed. See [Installation](/installation#add-to-your-project).
</Note>

```rust theme={null}
/// 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.as_option() {
        return Some(wa::Message {
            image_message: MessageField::some(wa::message::ImageMessage {
                caption: Some("Received your image!".to_string()),
                ..img.clone()
            }),
            ..Default::default()
        });
    }
    None
}
```

See the [media forwarding guide](/guides/media-handling#forwarding-media-via-cdn-reuse) for more details.

## Background operation and graceful shutdown

Use `spawn()` to run the bot in the background while your code continues, and `shutdown()` for a clean stop:

```rust theme={null}
use whatsapp_rust::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let bot = Bot::builder()
        .with_backend(SqliteStore::new("whatsapp.db").await?)
        .on_message(|ctx| async move {
            let _ = ctx.reply("hello").await;
        })
        .build()
        .await?;

    let handle = bot.spawn(); // bot runs in the background; client is available via handle.client()

    whatsapp_rust::shutdown_signal().await;
    handle.shutdown().await; // graceful: flushes pending state, then stops
    Ok(())
}
```

`BotHandle` also exposes `abort()` as an escape hatch (skips the flush). Awaiting the handle resolves to `()` once the run loop exits.

<Note>
  Prefer [`shutdown_signal()`](/installation#graceful-shutdown) over a bare `tokio::signal::ctrl_c()`: it resolves on **SIGTERM as well as SIGINT**. Supervisors like `docker stop`, Kubernetes, and systemd stop a process with SIGTERM first — a Ctrl+C-only wait never sees it, so cleanup is skipped and the process is hard-killed once the stop grace period elapses. Requires the `signal` feature, on by default.
</Note>

## Complete example with logging

Here's a production-ready example with proper logging, reactions, message editing, and media CDN reuse:

```rust src/main.rs theme={null}
use log::{error, info};
use whatsapp_rust::prelude::*;
use whatsapp_rust::pair_code::PairCodeOptions;

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

#[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}] [{}] - {}",
                wacore::time::now_utc().format("%H:%M:%S"),
                record.level(),
                record.target(),
                record.args()
            )
        })
        .init();

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

    let bot = Bot::builder()
        .with_backend(store)
        .on_qr_code(|code, _timeout| async move {
            println!("\n{code}\n");
        })
        .on_connected(|_client| async {
            info!("Bot connected!");
        })
        .on_logged_out(|_info| async {
            error!("Bot was logged out!");
        })
        .on_message(|ctx| async move {
            handle_message(ctx).await;
        })
        .build()
        .await?;

    info!("Starting bot...");
    bot.run().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;
    }

    if ctx.message.text_content() == Some(PING_TRIGGER) {
        if let Err(e) = ctx.react(REACTION_EMOJI).await {
            error!("Failed to send reaction: {e}");
        }
        if let Err(e) = ctx.reply_quoting(PONG_TEXT).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.as_option()
        && img.caption.as_deref() == Some(PING_TRIGGER)
    {
        return Some(wa::Message {
            image_message: MessageField::some(wa::message::ImageMessage {
                caption: Some(PONG_TEXT.to_string()),
                ..img.clone()
            }),
            ..Default::default()
        });
    }
    if let Some(vid) = base.video_message.as_option()
        && vid.caption.as_deref() == Some(PING_TRIGGER)
    {
        return Some(wa::Message {
            video_message: MessageField::some(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

| Target                  | Description                                                                                               |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `Client/Keepalive`      | Keepalive ping/pong and dead socket detection                                                             |
| `Client/Recv`           | Incoming frame processing (unmarshal, decompress)                                                         |
| `Client/Send`           | Outgoing message encryption and dispatch                                                                  |
| `Client/OfflineSync`    | Offline message sync progress and timing                                                                  |
| `Client/AppState`       | App state sync (contacts, settings, etc.)                                                                 |
| `Client/AccountSync`    | Account-level sync operations                                                                             |
| `Client/PairCode`       | Pair code authentication flow                                                                             |
| `Client/Pair`           | QR code pairing flow                                                                                      |
| `Client/Receipt`        | Receipt processing (read, delivered, played)                                                              |
| `Client/TcToken`        | Trusted contact token operations                                                                          |
| `Client/Group`          | Group metadata and participant operations                                                                 |
| `Client/Contacts`       | Contact sync and lookup                                                                                   |
| `Client/Business`       | Business profile updates                                                                                  |
| `Client/PDO`            | Peer Data Operations — message recovery from primary phone via bare-JID peer messaging and retry fallback |
| `Client/IQ`             | IQ stanza send/receive                                                                                    |
| `Client/Ack`            | Stanza acknowledgment                                                                                     |
| `Client/Status`         | Status/story operations                                                                                   |
| `Client/Picture`        | Profile picture updates                                                                                   |
| `Client/UnifiedSession` | Session establishment                                                                                     |
| `Blocking`              | Block/unblock operations                                                                                  |
| `Chatstate`             | Typing indicator events                                                                                   |
| `PresenceHandler`       | Presence update processing                                                                                |
| `AppState`              | App state patch encoding/decoding                                                                         |
| `Bot/PairCode`          | Bot-level pair code handling                                                                              |

### Filtering examples

```bash theme={null}
# 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
```

<Note>
  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.
</Note>

## Running with Docker

You can also run the bot using Docker instead of compiling locally:

```bash theme={null}
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](/installation#docker-deployment) 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:

```bash theme={null}
cargo run --example benchmark --features danger-skip-tls-verify
```

<Note>
  The benchmark example requires the `danger-skip-tls-verify` feature flag because it's designed for use with local test servers.
</Note>

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:

```bash theme={null}
# 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](https://github.com/iai-callgrind/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:

```bash theme={null}
# Run all wacore benchmarks (requires valgrind and iai-callgrind-runner)
cargo bench --workspace
```

See the [wacore benchmarks documentation](/api/wacore#benchmarks) for details on each suite, allocation optimizations, and CI integration.

## Next steps

<CardGroup cols={2}>
  <Card title="Sending messages" icon="message" href="/guides/sending-messages">
    Learn about different message types and how to send them
  </Card>

  <Card title="Media handling" icon="image" href="/guides/media-handling">
    Upload and download images, videos, and documents
  </Card>

  <Card title="Group management" icon="users" href="/guides/group-management">
    Create and manage WhatsApp groups
  </Card>

  <Card title="Client API reference" icon="code" href="/api/client">
    Explore all available client methods
  </Card>
</CardGroup>
