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
<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.
Basic Usage
Builder Methods
builder
with_backend
Backend implementation providing storage operations
with_transport_factory
Transport factory implementation
with_http_client
HTTP client implementation
with_runtime
Runtime implementation providing spawn, sleep, and spawn_blocking
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.Event Handling
on_event
Arc<Event> — use &*event to pattern-match on the inner event type.
Async function that receives
Arc<Event> and Arc<Client>on_event_for
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.
Using ChannelEventHandler
For scenarios where you need to process events outside of a closure (e.g., testing, custom event loops, or runtime-agnostic code), useChannelEventHandler 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
Encrypted message type (e.g., “frskmsg”, “skmsg”)
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
Tuple of (primary, secondary, tertiary) version numbers
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
Operating system name (e.g., “macOS”, “Windows”, “Linux”)
App version struct
Platform type (determines device name shown on phone)
with_push_name
Display name to set on the device
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
Configuration for pair code authentication
Pair code runs concurrently with QR code pairing — whichever completes first wins.
Cache Configuration
with_cache_config
Custom cache configuration
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:- Stream-decrypts external blobs in 8KB chunks (or moves inline payloads without copying)
- Decompresses zlib data on a blocking thread with pre-allocated buffers capped at 8 MiB
- Walks protobuf fields manually instead of decoding the entire message tree — only internal data (pushname, NCT salt, TC tokens) is extracted at this stage
- Wraps the decompressed blob in a
LazyHistorySyncwith cheap metadata (sync type, chunk order, progress) available without decoding - Dispatches
Event::HistorySync(Box<LazyHistorySync>)— full protobuf decoding is deferred until you call.get(). Use.raw_bytes()for custom partial decoding if you only need specific fields
skip_history_sync
- 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
with_wanted_pre_key_count
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.
Pre-keys per upload batch. Clamped to
5..=65_535.Building and Running
build
| Variant | Cause |
|---|---|
Other | Backend initialization or other runtime failure |
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.client
run
BotHandle that implements Future. You can also call .abort() on it to cancel the bot.
Example:
MessageContext
A convenience helper for message handling. You can construct it from theEvent::Message components:
Since v0.6
message is Arc<wa::Message> (was Box<wa::Message>). This matches the Event::Message payload and lets from_event / from_arc reuse the bus-dispatched Arc with zero deep clones.from_parts
MessageContext from individual message components. Internally clones the wa::Message into a new Arc.
from_arc
MessageContext from an existing Arc<wa::Message> without copying the body — pair this with the Arc you receive from Event::Message to keep dispatch zero-clone.
from_event
MessageContext from an Event. Returns None if the event is not an Event::Message. Reuses the existing Arc<wa::Message> rather than cloning the body.
send_message
SendResult containing the message_id and to JID.
build_quote_context
- Correct stanza_id/participant for groups and newsletters
- Stripping nested mentions
- Preserving bot quote chains
edit_message
revoke_message
react
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
TheCacheConfig struct controls TTL and capacity for all internal caches. All fields have sensible defaults matching WhatsApp Web behavior.
CacheEntryConfig
Available Caches
Timed caches
| Cache | Default TTL | Default Capacity | Description |
|---|---|---|---|
group_cache | 1 hour | 250 | Group metadata |
device_registry_cache | 1 hour | 5,000 | Device registry |
lid_pn_cache | 1 hour (TTI) | 10,000 | LID-to-phone mapping |
retried_group_messages | 5 minutes | 2,000 | Retry tracking |
recent_messages | 5 minutes | 0 (disabled) | Optional L1 in-memory cache for sent messages (retry support) |
message_retry_counts | 5 minutes | 1,000 | Retry count tracking |
pdo_pending_requests | 30 seconds | 500 | PDO pending requests |
sender_key_devices_cache | 1 hour (TTI) | 500 | Per-group SKDM distribution state |
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)
| Setting | Default | Description |
|---|---|---|
session_locks_capacity | 10,000 | Per-device Signal session lock capacity |
chat_lanes_capacity | 5,000 | Per-chat lane capacity (combined enqueue lock + message queue) |
Sent message DB cleanup
| Setting | Default | Description |
|---|---|---|
sent_message_ttl_secs | 7200 (2 hours) | TTL in seconds for sent messages in DB before periodic cleanup. Set to 0 to disable automatic 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 storesmessageSecret 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.
| Field | Type | Default | Description |
|---|---|---|---|
msg_secret_policy | MsgSecretPolicy | Managed | Which retention tier to use (see below) |
msg_secret_retention | MsgSecretRetention | 30d / 90d / 30d | Per-class horizons (text / poll_event / bot) |
seed_msg_secrets_from_history | bool | true | Seed secrets from pairing history-sync blobs |
original_message_resolver | Option<Arc<dyn OriginalMessageResolver>> | None | App-supplied fallback when the store misses |
msg_secret_resolver_timeout | Duration | 5s | Timeout for a single resolver call |
OriginalMessageResolver trait lets you supply secrets from your own store (required when the policy is Disabled):
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 customCacheStore backend (e.g., Redis):
| Field | Cache | Description |
|---|---|---|
cache_stores.group_cache | Group metadata | Group info lookups |
cache_stores.device_registry_cache | Device registry | Device registry entries |
cache_stores.lid_pn_cache | LID-PN mapping | LID-to-phone bidirectional lookups |
None keep the default in-process moka behavior. See Custom backends — cache store for a full implementation guide.
Coordination caches (
session_locks, chat_lanes), 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’sgetMessageTable 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:
- Every
send_message()call stores the serialized message payload in thesent_messagestable - On retry receipt, the client retrieves and consumes the payload (atomic take)
- Expired entries are periodically cleaned up based on
sent_message_ttl_secs
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
- Client - Lower-level client API
- Events - All event types
- Sending Messages - Sending messages
- Storage - Storage and multi-account patterns