Skip to main content
The Bot provides a simplified, ergonomic API for building WhatsApp bots. It handles client setup, event routing, and background sync tasks automatically.

Overview

Use the Bot builder pattern to:
  • Configure storage backend, transport, HTTP client, and async runtime
  • Register event handlers
  • Configure device properties and versions
  • Enable pair code authentication
  • Skip history sync for bot use cases
The builder uses a typestate pattern with four type parameters <B, T, H, R> (Backend, Transport, HttpClient, Runtime). The build() method is only callable when all four are Provided, making missing-component errors compile-time instead of runtime.
The Bot is the recommended way to use whatsapp-rust. It provides sensible defaults and handles boilerplate setup.

Basic Usage


Builder Methods

builder

Creates a new bot builder.

with_backend

Sets the storage backend (required).
Arc<dyn Backend>
required
Backend implementation providing storage operations
Example:
For multi-account scenarios, use SqliteStore::new_for_device(path, device_id) to create isolated storage per account.

with_transport_factory

Sets the transport factory for creating WebSocket connections (required).
F: TransportFactory
required
Transport factory implementation
Example:

with_http_client

Sets the HTTP client for media operations and version fetching (required).
C: HttpClient
required
HTTP client implementation
Example:

with_runtime

Sets the async runtime for spawning tasks, sleeping, and blocking operations (required).
Rt: Runtime
required
Runtime implementation providing spawn, sleep, and spawn_blocking
Example:
TokioRuntime is only available when the tokio-runtime feature is enabled (it is by default). To use a different async runtime, implement the Runtime trait from wacore::runtime. See custom backends for details.

with_task_instrument

Instruments the client’s internal tasks with a TaskInstrument hook, called around every poll of a spawned task (and around blocking work). Runtime-agnostic — it wraps whichever Runtime the client uses (via with_runtime or the default), so every task spawned through the Runtime trait is covered. Bot::run also meters the main run loop itself (the read loop, including frame decryption), so that work is covered whether you launch via bot.run().await or Bot::spawn() — the two paths never double-wrap. Default: no hook, the runtime is used untouched and nothing is metered.
When the voip feature is enabled, 1:1 call media tasks spawn directly on Tokio instead of through the Runtime trait, so they are not covered by this hook. A CpuMeter attached here will undercount CPU for sessions with active calls.
Pass the built-in wacore::stats::CpuMeter for per-session CPU accounting (busy time + poll count), keeping a clone to read .snapshot() later. Or implement TaskInstrument yourself to scope allocator attribution, an ESP-IDF heap_caps sampler, or any other per-session platform hook — the library only calls on_poll_start/on_poll_end and never inspects what the hook does.
Arc<dyn wacore::stats::TaskInstrument>
required
Hook invoked around every poll of the client’s internal tasks
Example:
Opt-in and off by default — instrumenting every poll has measurable overhead, so treat this as a diagnostics tool rather than an always-on meter. See Client::stats() for always-on wire I/O counters (atomics incremented on every frame) and Client::memory_report() for the on-demand, zero-cost-when-unused memory breakdown.

with_alloc_meter

Installs a wacore::stats::AllocMeter as this client’s task instrument and keeps a typed handle so Client::resource_report() can fold in its allocation-churn snapshot. AllocMeter is a first-class TaskInstrument — the churn counterpart to CpuMeter’s busy-time tracking — that attributes heap bytes allocated and freed to this client. This is sugar over with_task_instrument: it occupies the same single instrument slot, so it’s mutually exclusive with CpuMeter or any other hook — last setter wins. Calling with_task_instrument after with_alloc_meter drops the typed alloc-meter handle, so resource_report()’s alloc field reverts to None even though the instrument itself is replaced. The library never touches an allocator directly. The host must install a #[global_allocator] that calls AllocMeter::on_alloc / AllocMeter::on_dealloc on every (de)allocation — examples/alloc_tracking.rs in the source repo is the ~20-line reference implementation.
Only allocations made inside an instrumented poll or blocking closure are counted — every task spawned through the Runtime trait, plus the main run loop (see the with_task_instrument voip caveat above, which applies here too). Deallocations are charged to whichever meter is active when the free happens, not the one that allocated the block, so freed_bytes (and net_bytes()) drift for buffers that outlive the poll that made them — allocated_bytes is the reliable cumulative signal.
Arc<wacore::stats::AllocMeter>
required
The allocation meter to install and drive via the poll hooks
Example:

Event Handling

on_event

Registers an async event handler. The handler receives an Arc<Event> — use &*event to pattern-match on the inner event type.
F
required
Async function that receives Arc<Event> and Arc<Client>
Example:
See Events Reference for all event types.

on_event_for

Like on_event, but registers the handler with a narrowed EventInterest so the event bus skips materializing kinds you don’t subscribe to. Useful when you only handle a couple of event types and want to avoid paying the Arc allocation for high-frequency events like presence or receipts.

with_event_delivery

Chooses how on_event/on_event_for (and the other closure-based registrars) receive events off the bus.
EventDelivery
required
Delivery strategy. Defaults to EventDelivery::Concurrent — existing consumers are unaffected unless they opt in.
Example:
capacity is clamped to at least 1. Only affects the closure-based callbacks (on_event, on_event_for, on_message, and the other typed registrars) — a raw handler registered via with_event_handler always runs inline on the dispatch path and is unaffected by this setting.
Under Ordered, a callback that panics is caught and logged; it does not kill the single drainer or drop later events. If you need at-least-once delivery instead of best-effort drops under load, pair Ordered with an inbound durability hook — the hook buffers and redelivers independently of the delivery mailbox. See Client::stats() for the events_dropped counter.

with_event_handler

Registers a struct-based EventHandler directly on the bus. Unlike the closure registrars, the handler holds its state as struct fields (no per-field clone dance); because handle_event takes &self, mutable state requires interior mutability (Mutex, RwLock, or atomics). handle_event runs inline on the dispatch path — spawn your own task for slow work. Not affected by with_event_delivery. Example:

Using ChannelEventHandler

For scenarios where you need to process events outside of a closure (e.g., testing, custom event loops, or runtime-agnostic code), use ChannelEventHandler with register_handler instead of on_event:
ChannelEventHandler uses async-channel (runtime-agnostic) with an unbounded buffer, so events fired before the receiver starts listening are not lost. You can combine it with on_event — both handlers will receive all events.

with_enc_handler

Registers a custom handler for specific encrypted message types.
String
required
Encrypted message type (e.g., “frskmsg”, “skmsg”)
H: EncHandler
required
Handler implementation
On wasm32 targets, EncHandler drops the Send + Sync supertrait — your handler can capture !Send JS handles. On native, Send + Sync is retained via MaybeSendSync so Arc<dyn EncHandler> remains thread-safe. This mirrors the convention used by EventHandler and SendContextResolver.

Configuration

with_version

Overrides the WhatsApp version used by the client.
(u32, u32, u32)
required
Tuple of (primary, secondary, tertiary) version numbers
Example:
By default, the client automatically fetches the latest WhatsApp Web version from web.whatsapp.com/sw.js on each connect and caches it for 24 hours. Use with_version to pin a specific version when you need deterministic behavior — for example, in integration tests or CI environments where external HTTP requests are undesirable.

with_device_props

Overrides device properties sent to WhatsApp servers.
Option<String>
Operating system name (e.g., “macOS”, “Windows”, “Linux”)
Option<AppVersion>
App version struct
Option<PlatformType>
Platform type (determines device name shown on phone)
Example:
The platform_type determines what device name is shown on the phone’s “Linked Devices” list. Common values: Chrome, Firefox, Safari, Desktop.

with_push_name

Sets an initial push name on the device before connecting.
String
required
Display name to set on the device
Example:
The push name is included in the ClientPayload during registration. This is useful for testing scenarios where the server assigns phone numbers based on push name.

Authentication

with_pair_code

Configures pair code authentication to run automatically after connecting.
PairCodeOptions
required
Configuration for pair code authentication
Example:
Pair code runs concurrently with QR code pairing — whichever completes first wins.
The companion_platform_display shown on the phone is derived automatically from the resolved platform_id and a canonicalized OS derived from the device’s os string: web variants emit <Browser> (<OS>) (Android PlatformTypes map to Chrome, so they show as Chrome (Android) by default); explicit AndroidPhone/AndroidTablet/AndroidAmbiguous overrides emit Android (<OS>). The OS is coerced into a small server-safe set (Windows/Mac OS/Linux/Android/iOS) because the pair-code server rejects a non-OS display with bad-request — an arbitrary branding os string falls back to Linux. Set PairCodeOptions::display_os to send a real, non-canonical OS name verbatim instead. See Authentication — companion_platform_display for the full classification table.

on_pair_code_refresh

Registers a handler for Event::PairingCodeRefresh, fired when the server asks the companion to refresh an in-progress pair code. The bool argument is force_manual.
F
required
Async function that receives force_manual: bool and Arc<Client>
Example:
Only fires while a pair-code flow is outstanding and the notification’s ref matches it — a refresh_code notification for a stale or unrelated flow is ignored.

Cache Configuration

with_cache_config

Configures cache TTL and capacity settings for internal caches.
CacheConfig
required
Custom cache configuration
Example:
See Cache Configuration for available cache types.

History Sync

History sync transfers chat history from the phone to the linked device. The processing pipeline is optimized for minimal RAM usage through zero-copy streaming and lazy parsing.

How it works

When your bot receives history sync data, the pipeline:
  1. Stream-decrypts external blobs in 8KB chunks (or moves inline payloads without copying)
  2. Decompresses zlib data on a blocking thread with pre-allocated buffers capped at 8 MiB
  3. Walks protobuf fields manually instead of decoding the entire message tree — only internal data (pushname, NCT salt, TC tokens) is extracted at this stage
  4. Wraps the compressed payload in a LazyHistorySync with cheap metadata (sync type, chunk order, progress) available without decoding
  5. Dispatches Event::HistorySync(Box<LazyHistorySync>) — full protobuf decoding is deferred until you call .get(). Use .stream() for incremental memory-bounded access, .decompress() for one-shot inflation, or .compressed_bytes() for the raw compressed payload
If no event handlers are registered, the blob is not retained in memory.

skip_history_sync

Skips processing of history sync notifications from the phone. When enabled:
  • Sends a receipt so the phone stops retrying uploads
  • Does not download or process historical data
  • Emits debug log for each skipped notification
  • Useful for bot use cases where message history is not needed
Example:
For bots that only need to respond to new messages, enabling this can significantly reduce startup time and bandwidth usage.

with_wanted_pre_key_count

Sets the number of one-time pre-keys generated and uploaded per batch. Mirrors WhatsApp Web’s UPLOAD_KEYS_COUNT. Default: 812. The value is clamped at upload time to 5..=65_535. Values outside that range log a warn! and are clamped to the nearest bound.
usize
required
Pre-keys per upload batch. Clamped to 5..=65_535.
Example:
Leave this at the default unless you have a specific reason to change it. Embedded or memory-constrained consumers may prefer a smaller batch to shrink the working set during each upload; smaller batches also mean more frequent uploads as peers consume keys.
The floor of 5 prevents an empty-but-flagged pool and a re-upload loop (the count guard never clears below the trigger threshold). The ceiling of 65,535 is the wire-format limit — the upload IQ encodes the pre-key list length as a u16, so a larger batch would generate and store keys locally and then fail to encode.

Building and Running

build

Builds the bot with the configured options. Errors:
Missing required components (backend, transport, HTTP client, runtime) are caught at compile time via the typestate pattern — build() is only available when all four type parameters are Provided. You won’t see runtime errors for missing components.
BotBuilder is #[must_use] — building it does nothing until you call .build(), so a builder chain left unbound (or dropped before .build()) now triggers a compiler warning.

client

Returns the underlying Client Arc. Example:

run

Starts the bot’s connection loop and background workers. Returns a BotHandle that implements Future. You can also call .abort() on it to cancel the bot. Example:
BotHandle is #[must_use]: dropping it aborts the bot task instead of leaving it running in the background. Bind it and either .await it or call .shutdown()/.abort() explicitly — do not let it fall out of scope while you expect the bot to keep running.
If a with_task_instrument hook is configured, run() meters the client’s main run loop (the read loop, including frame decryption) itself, in addition to the tasks the instrumented runtime already covers. This closes the gap where bot.run().await polls that future on the caller’s task rather than through Runtime::spawn.

MessageContext

A convenience helper for message handling. You can construct it from an InboundMessage — the item type carried by Event::MessagesMessageBatch:
Since v0.6 message is Arc<wa::Message> (was Box<wa::Message>). This matches the InboundMessage payload and lets from_inbound / from_arc reuse the bus-dispatched Arc with zero deep clones.

from_parts

Constructs a MessageContext from individual message components. Internally clones the wa::Message into a new Arc.

from_arc

Constructs a MessageContext from an existing Arc<wa::Message> without copying the body — pair this with the Arc you receive from an InboundMessage to keep dispatch zero-clone.

from_inbound

Extracts a MessageContext from a single InboundMessage (one item of a MessageBatch). Unlike the removed from_event, this is infallible — there’s no “wrong event kind” case once you’re iterating Event::Messages’ batch. Reuses the existing Arc<wa::Message> rather than cloning the body. This is what Bot::on_message uses internally to fan a batch out to your per-message handler, invoked once per item in arrival order.

send_message

Sends a message to the same chat. Returns a SendResult containing the message_id and to JID.

build_quote_context

Builds a quote context for replying to this message. Handles:
  • Correct stanza_id/participant for groups and newsletters
  • Stripping nested mentions
  • Preserving bot quote chains
Example:

edit_message

Edits a message in the same chat.

revoke_message

Deletes a message in the same chat.

react

Sends an emoji reaction to the incoming message. The chat JID, target message ID, and group/status participant are taken from the context — you only supply the emoji. Pass an empty string ("") to remove a previously sent reaction. Internally this calls Client::send_reaction with self.message_key() as the target. Example:
Newsletter (channel) messages don’t flow through MessageContext::react. Use client.newsletter().send_reaction() for newsletter reactions.

Complete Example


Cache configuration reference

The CacheConfig struct controls TTL and capacity for all internal caches. All fields have sensible defaults matching WhatsApp Web behavior.

CacheEntryConfig

Available Caches

Timed caches

The lid_pn_cache and sender_key_devices_cache use time-to-idle (TTI) semantics — entries expire after being idle for the timeout period. All other caches use time-to-live (TTL) semantics.
The recent_messages cache is disabled by default (capacity 0), meaning sent messages are stored only in the database for retry handling — matching WhatsApp Web’s behavior. Set capacity greater than 0 to enable a fast in-memory L1 cache in front of the database. See DB-backed sent message retry for details.

Coordination caches (capacity-only, no TTL)

The lane’s live entry count plus cumulative capacity evictions and blocked evictions are exposed via Client::memory_report() (group_distribution_locks, group_distribution_lock_evictions, group_distribution_lock_eviction_blocks), so operators can derive eviction rates and tune this capacity without guessing.

Sent message DB cleanup

The sent_message_ttl_secs default was raised from 300s to 7200s. Retry receipts can arrive well after a message is sent (e.g. after the recipient comes back online); a 5-minute TTL could expire the stored payload before its retry, silently dropping the retry. Two hours covers realistic offline gaps.

messageSecret retention

The client stores messageSecret values so it can later decrypt add-ons that reference an original message — poll votes, poll/event edits, message edits, and Meta AI / fbid bot replies. Retention is bounded by policy and a per-class event-time horizon (expires_at = parent_message_ts + horizon, not insertion time), so secrets survive offline gaps without growing unbounded.
The OriginalMessageResolver trait lets you supply secrets from your own store (required when the policy is Disabled):
It is consulted only after the in-core MsgSecretStore and the LID/PN alternate lookups miss.
MsgSecretPolicy, MsgSecretRetention, and OriginalMessageResolver are re-exported from the crate root (whatsapp_rust::{MsgSecretPolicy, MsgSecretRetention, OriginalMessageResolver}). The default Managed policy is bounded and needs no tuning for most apps.

Custom cache store overrides

You can replace any of the pluggable caches with a custom CacheStore backend (e.g., Redis):
Fields left as None keep the default in-process PortableCache behaviour. See Custom backends — cache store for a full implementation guide.
Coordination caches (session_locks, chat_lanes, group_distribution_locks), the signal write-behind cache, and pdo_pending_requests always stay in-process — they hold live Rust objects that cannot be serialized to an external store.

Custom configuration example

DB-backed sent message retry

Sent messages are persisted to the database for retry handling, matching WhatsApp Web’s getMessageTable pattern. When a retry receipt arrives, the client looks up the original message payload from the database, re-encrypts it, and resends. How it works:
  1. Every send_message() call stores the serialized message payload in the sent_messages table
  2. On retry receipt, the client retrieves and consumes the payload (atomic take)
  3. Expired entries are periodically cleaned up based on sent_message_ttl_secs
Optional L1 cache: By default, the recent_messages cache capacity is 0 (DB-only mode). If you set capacity greater than 0, sent messages are also cached in memory for faster retrieval. In L1 mode, the DB write is backgrounded since the cache serves reads immediately. In DB-only mode, the write is awaited to guarantee persistence.

See Also