Skip to main content
The Client struct is the core of whatsapp-rust, managing connections, encryption, state, and all protocol-level operations.

Overview

The Client handles:
  • WebSocket connection lifecycle and automatic reconnection
  • Noise Protocol handshake and encryption
  • Signal Protocol E2E encryption for messages
  • App state synchronization
  • Device state persistence
  • Event dispatching
Most users should use the Bot builder instead of creating a Client directly. The Bot provides a simplified API with sensible defaults.

Creating a Client

Arc<dyn Runtime>
required
Async runtime for spawning tasks, sleeping, and blocking operations
Arc<PersistenceManager>
required
State manager for device credentials, sessions, and app state
Arc<dyn TransportFactory>
required
Factory for creating WebSocket connections
Arc<dyn HttpClient>
required
HTTP client for media operations and version fetching
Option<(u32, u32, u32)>
Optional WhatsApp version override (primary, secondary, tertiary)
(Arc<Client>, Receiver<MajorSyncTask>)
Returns the client Arc and a receiver for history/app state sync tasks

Creating with custom cache configuration

See Bot - Cache Configuration Reference for available cache options.

Connection Management

run

Main event loop that manages connection lifecycle with automatic reconnection. Runs indefinitely until:
  • disconnect() or logout() is called
  • Auto-reconnect is disabled and connection fails
  • Client receives a fatal stream error (401 unauthorized, 409 conflict, or 516 device removed)
Example:

connect

Establishes WebSocket connection and performs Noise Protocol handshake. Both the transport connection and the version fetch run in parallel under a 20-second timeout (TRANSPORT_CONNECT_TIMEOUT), matching WhatsApp Web’s MQTT and DGW connect timeout defaults. Without this, a dead network would block on the OS TCP SYN timeout (~60-75s). The Noise handshake response also has a separate 20-second timeout (NOISE_HANDSHAKE_RESPONSE_TIMEOUT).
As of PR #1090, connect() returns ConnectError instead of anyhow::Error. ClientError::AlreadyConnected was removed — that case is now ConnectError::AlreadyConnected.
Errors (ConnectError):
  • AlreadyConnected - a connection is already up, or another connect() attempt is already in flight
  • NotActivated - construction never activated (only reachable with the client-lifecycle feature)
  • Timeout { stage, timeout } - the version fetch or the transport open ran out of time, independently, each under the same 20s budget (ConnectStage::VersionFetch/Transport). connect() itself never reports ConnectStage::Socket or Ready — those are only produced by wait_for_socket()/wait_for_connected() below.
  • Version(anyhow::Error) / Transport(anyhow::Error) - app version resolution or transport open failed outright
  • Handshake(HandshakeError) - the Noise handshake failed after the transport was up; check HandshakeError::is_transient() to decide whether a retry is worthwhile
See ConnectError for the full variant reference.

logout

Deregisters this companion device from WhatsApp and disconnects. This sends a device removal IQ to the server, disables auto-reconnect, disconnects the transport, and emits a LoggedOut event.
As of PR #1090, logout() is infallible ((), not Result<()>). The deregistration IQ is best-effort — it cannot be sent at all while offline — and the local teardown runs either way, so there was nothing for a caller to branch on. A failed IQ is logged at warn.
This does not wipe stored keys or credentials. To fully clear session data, delete the storage backend after calling logout().
Example:
Behavior:
  1. Disables auto-reconnect
  2. Sends a RemoveCompanionDeviceSpec IQ to deregister the companion device (if connected); a failure here is logged, not returned
  3. Disconnects the transport
  4. Emits Event::LoggedOut with reason: ConnectFailureReason::LoggedOut

disconnect

Disconnects gracefully and disables auto-reconnect. It signals shutdown (sets the expected-disconnect and stop flags, fires the shutdown notifiers), flushes pending outbound receipts and device state, closes the transport, and then runs cleanup_connection_state(). That cleanup resets all connection-scoped state — invalidating per-chat message queues so stale workers exit, flushing then clearing the signal cache (so pending sender-key/identity writes are persisted, not lost), draining pending IQ waiters, and resetting offline sync state. The same cleanup_connection_state() also runs from run() after the message loop exits; it is idempotent and race-tolerant, so whichever path wins, connection-scoped state is reset once in effect. See disconnect cleanup for the full list of resources cleaned up.

signal_shutdown_sync

Synchronous, flag-only variant of disconnect() for places where you can’t await. It flips expected_disconnect, clears is_running, fires the terminal shutdown_notifier, and notifies the per-connection shutdown so spawned tasks exit on their next poll. It does not flush, close the transport, or touch persistence — prefer disconnect() whenever you can await. Intended for Drop impls on FFI wrappers (e.g. the WASM client) that need to release the runtime without blocking.

reconnect

Drops the current connection and triggers auto-reconnect with a deliberate ~5s offline window (Fibonacci backoff step 4). The run loop stays active. Use this for:
  • Handling network changes (e.g., Wi-Fi to cellular)
  • Forcing a fresh server session
  • Testing offline message delivery
Example:

reconnect_immediately

Drops the current connection and reconnects immediately with no delay. Unlike reconnect(), this sets the expected disconnect flag so the run loop skips the backoff delay. Example:

wait_for_socket

Waits for the Noise socket to be ready (before login). Useful for pair code flows.
Duration
required
Maximum time to wait
Result<(), ConnectError>
Ok if socket ready, ConnectError::Timeout { stage: ConnectStage::Socket, timeout } on timeout

wait_for_connected

Waits for full connection and authentication to complete, including offline sync. Post-login tasks (presence, background queries) are gated behind offline sync completion, which resolves either when the server sends the end marker, all expected items arrive, or the 60-second timeout fires.
Duration
required
Maximum time to wait
Result<(), ConnectError>
Ok once fully ready, ConnectError::Timeout { stage: ConnectStage::Ready, timeout } on timeout
As of PR #1090, both methods return ConnectError instead of anyhow::Error.

pair_with_code

Initiates pair code authentication as an alternative to QR code pairing. The returned 8-character code should be displayed to the user, who enters it on their phone under WhatsApp > Linked Devices > Link a Device > Link with phone number instead. This can run concurrently with QR code pairing — whichever completes first wins.
PairCodeOptions
required
Configuration for pair code authentication:
  • phone_number — Phone number in international format (e.g., "15551234567")
  • show_push_notification — Whether to show a push notification on the phone (default: true)
  • custom_code — Optional custom 8-character code using Crockford Base32 alphabet
  • platform_idOption<CompanionWebClientType> override for <companion_platform_id>. None derives the wire id from Device.device_props.platform_type (typically Chrome; Android PlatformTypes also map to Chrome because the server requires attestation for the Android letter codes). The matching <companion_platform_display> is always derived; web variants emit <Browser> (<OS>), and explicit AndroidPhone/AndroidTablet/AndroidAmbiguous overrides emit Android (<OS>).
String
The 8-character pairing code to display to the user
Errors (PairError): PairError::PairCode(PairCodeError) covers validation and crypto failures; PairError::RequestFailed(IqError) covers the IQ transport. Example:

set_passkey_authenticator

Registers a PasskeyAuthenticator for passkey (SHORTCAKE_PASSKEY) linking. Once set, the client auto-drives the flow end-to-end: it calls get_assertion when the server requests one, sends the response, and auto-confirms a re-link whose skip_handoff_ux is true. Leave it unset to drive every step manually from the Event::PairPasskey* events.
Arc<dyn PasskeyAuthenticator>
required
Produces a WebAuthn assertion for the server’s challenge — typically backed by Android Credential Manager, hybrid/caBLE, or a software vault. Use whatsapp_rust::passkey::CallbackAuthenticator::new(f) to wrap an async closure.

send_passkey_response

Sends the WebAuthn assertion as <passkey_prologue> and opens the ephemeral-identity handshake. Call after an Event::PairPasskeyRequest. Returns PasskeyError::Flow if a passkey open is already in progress.

send_passkey_confirmation

Finishes the link: encrypts the rotated ADV secret under the derived key, sends <encrypted_pairing_request>, and commits the secret rotation. For a fresh link, call this only after the user confirms the code from an Event::PairPasskeyConfirmation — a proven re-link (skip_handoff_ux: true) can call it immediately, and the automatic driver does so itself. Returns PasskeyError::Flow if called before the confirmation stage or without an active session. Errors (PasskeyError):

Connection State

is_connected

Returns true if the Noise socket is established. This method uses an internal AtomicBool flag (with Acquire ordering) instead of probing the noise socket mutex, making it lock-free and immune to false negatives under mutex contention.
Prior to this design, connection checks used try_lock() on the noise socket mutex. Under contention (e.g., during frame encryption), try_lock() would fail and incorrectly report the client as disconnected — silently dropping receipt acks. The AtomicBool approach eliminates this race condition entirely.

is_logged_in

Returns true if authenticated with WhatsApp servers.

Auto-Reconnection

The client includes automatic reconnection handling with Fibonacci backoff.

How it works

  1. On disconnect: The client detects unexpected disconnections and automatically attempts to reconnect
  2. Fibonacci backoff: Each failed attempt increases the delay following the Fibonacci sequence (1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s…) with a maximum of 900 seconds (15 minutes) and +/-10% jitter
  3. Expected disconnects: Protocol-expected disconnects (e.g., 515 stream error after pairing) trigger immediate reconnection without backoff
  4. Keepalive monitoring: A keepalive loop sends periodic pings (every 15-30s) and forces reconnection if the socket appears dead (no data received for 20s after a send)

Controlling auto-reconnect

The consecutive-failure counter used for Fibonacci backoff is internal; read it via client.stats().reconnect_errors instead of a public field.

Stream error handling

The client handles specific <stream:error> codes from the WhatsApp server:
When you receive a LoggedOut or StreamReplaced event, auto-reconnect is permanently disabled for that session. You must create a new client and re-pair to continue.

Rate limiting (429)

When the server returns a 429 stream error, the client bumps the internal backoff counter by 5 Fibonacci steps before reconnecting. This means the reconnection delay jumps significantly (e.g., from ~1s to ~13s on the first rate limit) to respect the server’s throttling.

General reconnection behavior


Messaging

send_message

Sends an encrypted message to a chat.
Jid
required
Recipient JID (user@s.whatsapp.net or group@g.us)
wa::Message
required
Protobuf message content
Result<SendResult, SendError>
A SendResult containing the message_id and destination to JID
Example:

send_message_with_options

Sends a message with advanced options like a custom message ID, extra stanza nodes, or ephemeral expiration.
SendOptions
required
Configuration for message sending behavior. Supports message_id (override the auto-generated ID), extra_stanza_nodes (custom XML nodes on the stanza), and ephemeral_expiration (disappearing message duration in seconds).
See Send API reference for available options.

edit_message

Edits a previously sent message.
String
required
ID of the message to edit
wa::Message
required
New message content

edit_message_with_options

Edit-path counterpart of send_message_with_options. Accepts an EditOptions (built via EditOptions::default().with_stanza_id(id)) to pin the outer stanza id to an existing message’s id (best-effort — server/client dependent) instead of the fresh id edit_message generates. See Send API reference for the full EditOptions type and its side-effect notes.

revoke_message

Deletes a message. Use Sender to revoke your own message, or Admin to revoke another user’s message as group admin.
RevokeType
required
RevokeType::Sender (delete your own message) or RevokeType::Admin { original_sender: Jid } (admin revoke in groups)

Feature APIs

The Client provides namespaced access to feature-specific operations:

blocking

Access blocking operations. Methods:
  • block(jid: &Jid) - Block a contact
  • unblock(jid: &Jid) - Unblock a contact
  • get_blocklist() - Get all blocked contacts
  • is_blocked(jid: &Jid) - Check if contact is blocked
Example:

groups

Access group management operations. Methods:
  • query_info(jid: &Jid) - Get cached group info
  • get_metadata(jid: &Jid) - Fetch group metadata from server
  • get_participating() - List all groups you’re in
  • create_group(options: GroupCreateOptions) - Create a new group
  • set_subject(jid: &Jid, subject: GroupSubject) - Change group name
  • set_description(jid: &Jid, desc: Option<GroupDescription>, prev: PreviousDescription<'_>) - Change description; prev is an optimistic-concurrency token (PreviousDescription::Resolve reads the current one for you)
  • leave(jid: &Jid) - Leave a group
  • add_participants(jid: &Jid, participants: &[Jid]) - Add members
  • remove_participants(jid: &Jid, participants: &[Jid]) - Remove members
  • promote_participants(jid: &Jid, participants: &[Jid]) - Make members admins
  • demote_participants(jid: &Jid, participants: &[Jid]) - Remove admin status
  • get_invite_link(jid: &Jid, reset: bool) - Get/reset invite link
  • join_with_invite_code(code: &str) - Join a group via invite code or URL
  • join_with_invite_v4(group_jid, code, expiration, admin_jid) - Accept a V4 invite message
  • get_invite_info(code: &str) - Preview group metadata from invite code
  • set_locked(jid: &Jid, locked: bool) - Lock/unlock group info editing
  • set_announce(jid: &Jid, announce: bool) - Enable/disable announcement mode
  • set_ephemeral(jid: &Jid, expiration: u32) - Set disappearing messages timer
  • set_membership_approval(jid: &Jid, mode: MembershipApprovalMode) - Require admin approval
  • get_membership_requests(jid: &Jid) - Get pending membership requests
  • approve_membership_requests(jid: &Jid, participants: &[Jid]) - Approve pending requests
  • reject_membership_requests(jid: &Jid, participants: &[Jid]) - Reject pending requests
  • set_member_add_mode(jid: &Jid, mode: MemberAddMode) - Set who can add members
  • set_no_frequently_forwarded(jid: &Jid, restrict: bool) - Restrict forwarding of frequently forwarded messages
  • set_allow_admin_reports(jid: &Jid, allow: bool) - Allow or disallow admin reports
  • set_group_history(jid: &Jid, enabled: bool) - Enable or disable group history for new members
  • set_member_link_mode(jid: &Jid, mode: MemberLinkMode) - Set member link mode
  • set_member_share_history_mode(jid: &Jid, mode: MemberShareHistoryMode) - Set history sharing mode for new members
  • set_limit_sharing(jid: &Jid, enabled: bool) - Limit sharing within the group
  • cancel_membership_requests(jid: &Jid, participants: &[Jid]) - Cancel pending membership requests
  • revoke_request_code(jid: &Jid, participants: &[Jid]) - Revoke request codes for participants
  • acknowledge(jid: &Jid) - Acknowledge a group
  • batch_get_info(jids: Vec<Jid>) - Batch fetch group metadata for multiple groups
  • get_profile_pictures(group_jids: Vec<Jid>, picture_type: PictureType) - Batch fetch group profile pictures
Example:

presence

Access presence operations. Methods:
  • set(status: PresenceStatus) - Set presence status
  • set_available() - Set status to available/online
  • set_unavailable() - Set status to unavailable/offline
  • subscribe(jid: &Jid) - Subscribe to contact’s presence updates
  • unsubscribe(jid: &Jid) - Unsubscribe from contact’s presence updates
Subscriptions are automatically tracked and re-subscribed on reconnect. Example:

chatstate

Access chat state (typing indicator) operations. Methods:
  • send(to: &Jid, state: ChatStateType) - Send a chat state update
  • send_composing(to: &Jid) - Send typing indicator
  • send_recording(to: &Jid) - Send recording indicator
  • send_paused(to: &Jid) - Send paused/stopped typing indicator
Example:

contacts

Access contact operations. Methods:
  • is_on_whatsapp(jids: &[Jid]) - Check if JIDs are registered on WhatsApp (supports PN and LID JIDs)
  • get_user_info(jids: &[Jid]) - Get profile info for users by JID
  • get_profile_picture(jid: &Jid, preview: bool) - Get profile picture URL (preview or full size)

tc_token

Access trust/privacy token operations. Methods:
  • issue_tokens(jids: &[Jid]) - Request tokens for contacts
  • prune_expired() - Remove expired tokens
  • get(jid: &str) - Get a stored token by JID
  • get_all_jids() - List all JIDs with stored tokens

chat_actions

Access chat management actions. Operations sync across all linked devices via app state sync. Methods:
  • archive_chat(jid: &Jid, message_range: Option<SyncActionMessageRange>) - Archive a chat
  • unarchive_chat(jid: &Jid, message_range: Option<SyncActionMessageRange>) - Unarchive a chat
  • pin_chat(jid: &Jid) - Pin a chat
  • unpin_chat(jid: &Jid) - Unpin a chat
  • mute_chat(jid: &Jid) - Mute a chat indefinitely
  • mute_chat_until(jid: &Jid, mute_end_timestamp_ms: i64) - Mute until a specific time
  • unmute_chat(jid: &Jid) - Unmute a chat
  • star_message(chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool) - Star a message
  • unstar_message(chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool) - Unstar a message
  • mark_chat_as_read(jid: &Jid, read: bool, message_range: Option<SyncActionMessageRange>) - Mark a chat as read or unread across devices
  • delete_chat(jid: &Jid, delete_media: bool, message_range: Option<SyncActionMessageRange>) - Delete a chat from all linked devices
  • delete_message_for_me(chat_jid: &Jid, participant_jid: Option<&Jid>, message_id: &str, from_me: bool, delete_media: bool, message_timestamp: Option<i64>) - Delete a message locally (not for the other party)

status

Access status/story operations. Methods:
  • send_text(text, background_argb, font, recipients, options) - Post a text status
  • send_image(upload, thumbnail, caption, recipients, options) - Post an image status
  • send_video(upload, thumbnail, duration_seconds, caption, recipients, options) - Post a video status
  • send_raw(message, recipients, options) - Post any message type as a status
  • revoke(message_id, recipients, options) - Delete a posted status
  • send_reaction(status_owner, server_id, reaction) - React to a status update
See Status API for full documentation.

mex

Access Meta Exchange (GraphQL) operations. Methods:
  • query(request: MexRequest) - Execute a GraphQL query
  • mutate(request: MexRequest) - Execute a GraphQL mutation
See MEX API for full documentation.

profile

Access profile operations. Methods:
  • set_push_name(name: &str) - Set display name (syncs across devices)
  • set_status_text(text: &str) - Set profile “About” text
  • set_profile_picture(image_data: Vec<u8>) - Set profile picture (JPEG, 640x640 recommended)
  • remove_profile_picture() - Remove profile picture

newsletter

Access newsletter (channel) operations. Methods:
  • list_subscribed() - List all subscribed newsletters
  • get_metadata(jid: &Jid) - Get newsletter metadata
  • get_metadata_by_invite(invite: &str) - Get metadata via invite link
  • create(name, description) - Create a new newsletter
  • join(jid: &Jid) - Join a newsletter
  • leave(jid: &Jid) - Leave a newsletter
  • update(jid, options) - Update newsletter settings
  • send_reaction(jid, msg_server_id, reaction) - React to a newsletter message
  • get_messages(jid, count, before) - Fetch newsletter messages
  • subscribe_live_updates(jid: &Jid) - Subscribe to real-time updates
Newsletter message sending is handled by the unified client.send_message() method — pass a newsletter JID and the message is sent as plaintext automatically. See the Send API for details.
Example:
See Newsletter API for full documentation.

community

Access community operations. Methods:
  • create(options: CreateCommunityOptions) - Create a community
  • deactivate(jid: &Jid) - Deactivate a community
  • link_subgroups(jid: &Jid, subgroups: &[Jid]) - Link groups to a community
  • unlink_subgroups(jid: &Jid, subgroups: &[Jid], remove_orphan_members: bool) - Unlink groups from a community
  • get_subgroups(jid: &Jid) - List community subgroups
  • get_subgroup_participant_counts(jid: &Jid) - Get participant counts per subgroup
  • query_linked_group(community_jid: &Jid, subgroup_jid: &Jid) - Query a linked group’s community metadata
  • join_subgroup(community_jid: &Jid, subgroup_jid: &Jid) - Join a community subgroup
  • get_linked_groups_participants(jid: &Jid) - Get participants across linked groups
Example:
See Community API for full documentation.

polls

Access poll operations. Methods:
  • create(to: &Jid, name: &str, options: &[String], selectable_count: u32) - Create a poll (returns message ID and secret)
  • vote(chat_jid, poll_msg_id, poll_creator_jid, message_secret, option_names) - Cast a vote on a poll
  • decrypt_vote(enc_payload, enc_iv, message_secret, poll_msg_id, poll_creator_jid, voter_jid) - Decrypt a vote (static method)
  • aggregate_votes(poll_options, votes, message_secret, poll_msg_id, poll_creator_jid) - Tally all votes (static method)
Example:
See Polls API for full documentation.

media_reupload

Access media reupload operations. Use this when a media download fails because the URL has expired. Methods:
  • request(req: &MediaReuploadRequest) - Request the server to re-upload expired media
Example:
The request sends a server-error receipt and waits up to 30 seconds for a mediaretry notification with an updated download path.

signal

Access low-level Signal protocol operations for direct encryption, decryption, and session management. Methods:
  • encrypt_message(jid: &Jid, plaintext: &[u8]) - Encrypt plaintext for a single recipient
  • decrypt_message(jid: &Jid, enc_type: EncType, ciphertext: &[u8]) - Decrypt a Signal protocol message
  • encrypt_group_message(group_jid: &Jid, plaintext: &[u8]) - Encrypt plaintext for a group using sender keys
  • decrypt_group_message(group_jid: &Jid, sender_jid: &Jid, ciphertext: &[u8]) - Decrypt a group message
  • validate_session(jid: &Jid) - Check whether a Signal session exists
  • delete_sessions(jids: &[Jid]) - Delete Signal sessions and identity keys
  • create_participant_nodes(recipient_jids: &[Jid], message: &Message) - Create encrypted participant nodes
  • assert_sessions(jids: &[Jid]) - Ensure E2E sessions exist
  • get_user_devices(jids: &[Jid]) - Get all device JIDs for users
Example:
These are low-level APIs that bypass the high-level message sending pipeline. Most users should use send_message() which handles encryption automatically.
See Signal API for full documentation.

query_usync

Executes a typed USync (“user sync”) query directly — the same protocol engine that powers contacts() and signal().get_user_devices() under the hood. Use it for protocol combinations not covered by a specialized helper (bot profile lookup, username resolution, disappearing_mode/text_status, feature flags). This is a neutral operation: it only returns decoded wire data, with no cache or persistence side effects. See USync API for the full UsyncQuery/UsyncResponse model and examples.

Public fields

http_client

The HTTP client used for media operations, version fetching, and other HTTP requests. This field is public and can be used directly for custom HTTP operations that share the same client configuration.

enable_auto_reconnect

Controls whether the client automatically reconnects after an unexpected disconnection. Defaults to true. Set to false to disable auto-reconnect.

custom_enc_handlers

Custom handlers for encrypted message types. Set once at Bot::build and immutable afterward; read lock-free via .get(). Register handlers exclusively through BotBuilder::with_enc_handler() — direct mutation after build is not possible.

RECONNECT_BACKOFF_STEP

The number of Fibonacci steps added to the backoff counter when reconnect() is called, creating an approximately 5-second offline window before the next connection attempt. This prevents tight reconnect loops after intentional disconnects.

Client Profile

The noise-handshake ClientPayload.UserAgent identity that this client presents to WhatsApp servers. The default is ClientProfile::web(), which matches the legacy desktop-web payload (platform Web, device Desktop, OS version 0.1.0, and an attached web_info field). This is independent of DevicePropsdevice_props controls what is reported during companion registration (e.g., the entry shown under Linked Devices on the phone), while ClientProfile controls the user agent fields used during the Noise handshake on every connect.

set_client_profile

Sets the noise-handshake ClientPayload profile. The profile is held in-memory only (#[serde(skip)] on Device.client_profile), so you must call this before each connect() on a fresh process.
ClientProfile
required
The profile to apply. Use the constructors on ClientProfileweb(), android(os_version), smb_android(os_version), ios(os_version), macos(os_version), windows(os_version).
Example:
Native profiles (android, smb_android, ios, macos, windows) automatically omit web_info from the ClientPayload. Only web() includes it.

Device State

push_name

Returns the current push name (display name).
Renamed from get_push_name — the get_ prefix was dropped to match the neighboring accessors.

pn

Returns the phone number JID, or None before pairing completes.
Renamed from get_pn.

lid

Returns the LID (Linked Identity), or None before pairing completes.
Renamed from get_lid.

is_lid_migrated

Whether the account is 1:1-LID-migrated on WhatsApp’s servers. This gates outbound DM wire addressing (the stanza to/<participants> namespace) — an unmigrated account keeps DMs on PN even when a LID mapping is cached, since the server rejects LID-addressed DMs from unmigrated accounts with ack error="400" (#941). Signal session addressing is unaffected either way. Returns true if the persisted Device.lid_migrated flag is set, or (as a fallback for accounts paired before the flag existed) if the lid_one_on_one_migration_enabled ab prop is currently enabled. See Signal Protocol — DM wire namespace vs. Signal session addressing and Authentication — one-to-one LID migration state.
This is normally handled automatically by the send path — you don’t need to call it yourself before sending. It’s exposed for diagnostics/telemetry.

get_lid_pn_entry

Unified LID-PN lookup that auto-routes based on the JID type. Pass a phone number JID (@s.whatsapp.net) to look up its LID, or a LID JID (@lid) to look up its phone number. Returns None for non-user JIDs (groups, newsletters, etc.) or if no mapping is cached.
&Jid
required
The JID to look up — either a PN JID or a LID JID
Option<LidPnEntry>
Contains lid (Arc<str>), phone_number (Arc<str>), created_at (i64 Unix timestamp), and learning_source (LearningSource). Use &*entry.lid for &str comparisons or pass directly to anything that accepts AsRef<str>.
Example:
This replaces the previous get_phone_number_from_lid method. The new API accepts a full Jid instead of a raw string and supports bidirectional lookup — pass either a PN or LID JID to resolve the mapping in either direction.

LearningSource

The LearningSource enum indicates how a LID-PN mapping was discovered. The source is not mere provenance — it also selects the write policy applied when the pair reaches the cache, mirroring WhatsApp Web’s createLidPnMappings (WAWebDBCreateLidPnMappings) switch (learningSource):
  • Directed sources (Usync, PeerPnMessage, PeerLidMessage, RecipientLatestLid, MigrationSyncLatest, MigrationSyncOld, BlocklistActive, BlocklistInactive) overwrite the cache on any change from what’s already stored.
  • Observational bulk sources (Other, Pairing, DeviceNotification) only seed a LID that isn’t cached yet. If the pair conflicts with an already-known LID for that phone, the observational pair is not applied — the client instead fires one background live LID query (LidQuerySpec) and learns the authoritative result under Usync, which can never itself trigger another reconcile.
  • Known-stale sources (MigrationSyncOld, BlocklistInactive) are additionally stamped with created_at = 0, so a fresher mapping for the same phone always outranks them in the cache’s most-recent-wins (PN→LID) resolution. This only guards the forward direction — the LID→PN reverse map always takes the latest write.
A pair that already matches the cache’s current mapping always re-affirms durability regardless of source — it is never treated as a conflict. This includes an exact match, and also a reverse-only match: the LID-PN cache is capacity-bounded (see lid_pn_cache), so the PN→LID entry can be evicted while the LID→PN entry survives, and a re-learn of that surviving pair still counts as self-consistent.

persistence_manager

Access to the persistence manager for multi-account scenarios.

History Sync

History sync transfers chat history from the phone to the linked device. The client processes history sync notifications through a RAM-optimized pipeline that minimizes peak memory usage.

Processing pipeline

When a history sync notification arrives, the client:
  1. Sends a HistorySync receipt immediately (so the phone knows delivery succeeded)
  2. Retrieves the data — either from an inline payload (moved via .take(), not cloned) or by stream-decrypting an external blob in 8KB chunks
  3. Extracts a compressed_size_hint from the notification’s file_length field, which the decompressor uses with a 4x multiplier for better buffer pre-allocation (avoids repeated Vec reallocation)
  4. Runs decompression and protobuf parsing on a blocking thread (tokio::task::spawn_blocking) to avoid stalling the async runtime
  5. Wraps the decompressed blob in a LazyHistorySync with cheap metadata (sync type, chunk order, progress) and dispatches it as Event::HistorySync(Box<LazyHistorySync>). Full protobuf decoding is deferred until the event handler calls .get()
If no event handlers are registered, the blob is not retained — only internal data (pushname, NCT salt, TC tokens) is extracted.

process_sync_task

Processes a MajorSyncTask received from the sync channel returned by Client::new. This is the public entry point for handling history sync and app state sync tasks. The method dispatches to the appropriate internal handler based on the task variant:
  • MajorSyncTask::HistorySync — downloads and processes history sync data
  • MajorSyncTask::AppStateSync — synchronizes app state (contacts, mutes, pins, etc.)
Example:
If you use the Bot builder, sync task processing is handled automatically. You only need this method when building a custom client setup.

set_skip_history_sync

Enable or disable skipping of history sync notifications at runtime. When skipping is enabled, the client sends a receipt (so the phone stops retrying uploads) but does not download or process any data.

skip_history_sync_enabled

Returns true if history sync is currently being skipped.

set_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. Intended for consumers that construct Client directly (rather than via Bot::builder().with_wanted_pre_key_count(...)). Set this before calling connect(). The value is clamped at upload time to 5..=65_535; out-of-range values log a warn!.
usize
required
Pre-keys per upload batch. Clamped to 5..=65_535.
Example:
The floor of 5 prevents an empty-but-flagged pool and a re-upload loop. The ceiling of 65,535 matches the upload IQ’s u16 list-length encoding — larger batches would generate keys locally and then fail to encode.

wanted_pre_key_count

Returns the currently configured pre-key upload batch size.

set_force_active_delivery_receipts

Force the client to send active delivery receipts (matching the recipient pattern WA Web uses for foreground chats) regardless of the local delivery_receipt_active setting. v0.6 added this knob so consumers can opt every incoming message into active receipts during a known foreground session. When active is true, the client emits <receipt> stanzas without the silent flag for every successful decrypt. When false (default), behavior follows the existing per-chat heuristic. The setting is mirrored across offline-resume so the post-resume ack pattern matches the live one.

send_history_sync_server_error_receipt

Ask the phone to re-upload a history-sync blob whose download failed (corrupt body, mismatched HMAC, missing CDN file, etc.). The client emits a <receipt type="server-error" category="peer"> to the companion device carrying an encrypted retry payload, mirroring WA Web’s WAWebSendHistSyncServerErrorReceiptJob. Parameters:
  • message_id — the MessageInfo::id of the failed history-sync notification
  • media_key — the 32-byte key carried by the original <historysync mediaKey="…"> element
When to call: Typically inside your Event::HistorySync (or upstream download) error path, once you’ve determined the blob can’t be recovered locally. The phone will then retry the upload, producing a fresh HistorySync notification.

Offline sync

The client automatically manages offline message sync when reconnecting. During sync, message processing is restricted to sequential mode (1 concurrent task) to preserve ordering.

Semaphore transition safety

When offline sync completes, the concurrency semaphore is swapped from 1 permit to 64 permits. Tasks that were already waiting on the old semaphore use a generation-checked re-acquire loop to safely transition — they detect the swap via an atomic generation counter, drop the stale permit, and re-acquire from the new semaphore. This prevents pkmsg messages (which carry SKDM for group decryption) from being silently dropped during the transition. See Concurrency gating for details.

Timeout fallback

If the server advertises offline messages but never completes delivery, a 60-second timeout ensures startup is not blocked indefinitely. On timeout:
  1. A warning is logged with the number of processed vs. expected items
  2. Offline sync is marked complete
  3. OfflineSyncCompleted event is emitted
  4. Message processing switches from sequential to parallel (64 concurrent tasks)

State reset on reconnect

All offline sync state (counters, timing, concurrency semaphore) is fully reset on reconnect so stale state does not carry over to the next connection. Related events: OfflineSyncPreview, OfflineSyncCompleted

App State

fetch_props

Fetches A/B experiment properties from WhatsApp servers and updates the in-memory AbPropsCache. When a stored props hash exists and the cache has been seeded (at least one full fetch has occurred), the request includes the hash for a delta update — the server only returns changed props. Otherwise, a full fetch is performed and all cached props are replaced. After the response is applied to the cache, the new hash (if present) is persisted for future delta requests. Features like group privacy token attachment query the AbPropsCache to check whether specific experiment flags are enabled. See AB props cache for details.

AB props cache

The client maintains an in-memory AbPropsCache that stores server-side A/B experiment properties. The cache is populated each time fetch_props() runs (automatically on connect) and is not persisted — props are re-fetched on every connection. Features query the cache by passing a typed AbProp constant from the vendored wacore::iq::abprops registry. A bool prop is considered enabled when its value is "1", "true", or "enabled" (case-insensitive), falling back to the registry default when the server didn’t send it.
The cache exposes:

Watching additional flags

Only flags in the cache’s interest set are retained when props come in — every other server prop is discarded to avoid allocating for the ~2,000+ flags WhatsApp ships. The interest set is pre-seeded with the flags the library itself reads (see wacore::iq::props::WATCHED). If you need to gate your own code on a flag the library doesn’t already watch, register it before the first fetch_props():
Flags retain their code, value_type, and default straight from the WA Web bundle, so behavior tracks WhatsApp Web without hand-maintained config tables.
The AB props cache is internal to the client. You don’t need to interact with it directly — the library automatically checks relevant flags when performing group operations like create_group and add_participants.

fetch_privacy_settings

Fetches privacy settings (last seen, profile photo, about, etc.). See Privacy API for types and details.

set_privacy_setting

Sets a privacy setting for a specific category using type-safe enums.
PrivacyCategory
required
Privacy category enum: Last, Online, Profile, Status, GroupAdd, ReadReceipts, CallAdd, Messages, or DefenseMode
PrivacyValue
required
Privacy value enum: All, Contacts, None, ContactBlacklist, MatchLastSeen, Known, Off, or OnStandard
Example:
See Privacy API for all categories, values, and valid combinations.

set_privacy_disallowed_list

Updates a privacy category’s disallowed list (contacts-except-specific-users mode). Only available for Last, Profile, Status, and GroupAdd. See Privacy API for details and examples.

set_default_disappearing_mode

Sets the default disappearing messages duration for new chats.
u32
required
Timer duration in seconds. Common values: 86400 (24 hours), 604800 (7 days), 7776000 (90 days). Pass 0 to disable.
Example:

get_business_profile

Fetches the business profile for a WhatsApp Business account. Returns None if the account is not a business account or has no business profile.
&Jid
required
JID of the business account to query
Example:
See Business API for full type details.

clean_dirty_bits

Cleans app state dirty bits. The DirtyBit struct contains a dirty_type (e.g., AccountSync, Groups, SyncdAppState, NewsletterMetadata) and an optional timestamp.

Protocol Operations

send_node

Sends a raw protocol node (advanced usage).
Node
required
Binary protocol node to send
Errors:
  • ClientError::NotConnected - Not connected
  • ClientError::EncryptSend - Encryption/send failure

send_raw_bytes

Send pre-marshaled plaintext bytes through the noise socket. The bytes must be a valid WABinary-marshaled stanza (as produced by wacore_binary::marshal::marshal_to). Sending malformed data will cause the server to close the connection.
Vec<u8>
required
WABinary-marshaled stanza bytes
Errors:
  • ClientError::NotConnected - Not connected
  • ClientError::EncryptSend - Encryption/send failure
This bypasses node logging and wait_for_sent_node waiter resolution. Use send_node for normal stanza sending. This method is intended for performance-critical paths where you already have marshaled bytes.

flush_pending_signal_state

As of PR #1090, this returns SignalMaintenanceError instead of anyhow::Error (a Storage failure keeps the typed backend cause reachable via source()).
Forces any pending write-behind Signal cache state to the backend, returning once the flush completes (or fails). Ordinarily — without calling this method — the backend trails the in-memory cache. Most sends — DM, group, and status alike — are already covered by a durable lease (SessionRecord’s sender-chain counter lease for DMs, SenderKeyRecord’s chain iteration lease for group/status), so they only schedule the coalesced write-behind; only the roughly-1-in-64 send that exhausts the current lease flushes synchronously — and because the pre-wire flush check is global, a pending flush on an unrelated session or sender key can force a synchronous flush too. Status reactions are the DM-branch exception and follow the DM lease behavior instead of the group/status one. The live receive path always schedules a coalesced flush, on a ~25ms window (see flush scheduling). A successful call to flush_pending_signal_state() closes that gap deterministically: everything dirty as of the call is persisted by the time it returns Ok. The call has no hard wall-clock bound — it can wait on locks, or on slow or failing storage, and a backend outage extends it until the retry loop succeeds. Check the returned Result: a failure means the flush did not complete, and state is still pending, not persisted.
Never call this from inside an InboundDurabilityHook — during an offline-sync drain it runs while the processing permit is held, and settling routes through that same permit, so re-entering it would deadlock. The same risk applies to a custom EventHandler::handle_event implementation that itself blocks synchronously inline (dispatch is synchronous). It does not apply to ordinary Bot closure handlers (.on_message(), etc.) — both the default concurrent and ordered delivery modes run your callback in a detached task that never holds the permit, so calling flush_pending_signal_state() from inside one of those is safe.
Example:
See Signal Protocol — flush scheduling for the full durability model.

generate_message_id

Generates a unique WhatsApp-protocol-conformant message ID. Combines timestamp, user JID, and random components for uniqueness.
This is intended for advanced users who need to build custom protocol interactions or manage message IDs manually. Most users should use send_message which handles ID generation automatically.

send_iq

Sends a custom IQ (Info/Query) stanza to the WhatsApp server.
InfoQuery
required
IQ query containing stanza type, namespace, content, and optional timeout
Example:
This bypasses higher-level abstractions and safety checks. You should be familiar with the WhatsApp protocol and IQ stanza format before using this.

execute

Executes a typed IQ specification. This is the preferred way to send IQ stanzas — each spec type handles building the request and parsing the response.
S: IqSpec
required
A typed IQ specification that defines the request structure and response parsing
Example:

wait_for_node

Waits for a specific incoming protocol node matching the given filter. Returns a receiver that resolves when a matching node arrives.
NodeFilter
required
Filter specifying which node to wait for (by tag and attributes)
Example:
Register the waiter before performing the action that triggers the expected node. When no waiters are active, this has zero cost (single atomic load per incoming node).

NodeFilter

Builder for matching incoming protocol nodes:

wait_for_sent_node

Waits for a specific outgoing protocol node matching the given filter. Returns a receiver that resolves when a matching node is sent by the client. This is the outbound counterpart to wait_for_node.
NodeFilter
required
Filter specifying which outgoing node to intercept (by tag and attributes)
Example:
Register the waiter before performing the action that produces the outgoing node. When no sent-node waiters are active, this has zero cost (single atomic load per outgoing node). Useful for testing whether <tctoken> or <cstoken> was attached to a sent stanza.

register_handler

Registers an event handler for protocol events.
Arc<dyn EventHandler>
required
Handler implementing the EventHandler trait
Example:
Using ChannelEventHandler: For channel-based event processing (useful for testing and custom event loops), use the built-in ChannelEventHandler:
See ChannelEventHandler for details.

register_chatstate_handler

Registers a handler for chat state events (typing indicators). The handler is wrapped in Arc for thread-safe sharing across the event dispatching system.

set_raw_node_forwarding

Enable or disable raw node forwarding. When enabled, Event::RawNode is emitted for every decoded stanza before the stanza router dispatches it. Disabled by default to avoid overhead.
bool
required
Whether to emit Event::RawNode for every incoming stanza
Example:
Only enable this when you need raw protocol access. Every decoded stanza triggers the event, which adds overhead to the message processing pipeline.

Call management

reject_call

Reject an incoming call. This is fire-and-forget — no server response is expected. Sends a <call><reject> stanza to the WhatsApp server.
&str
required
The ID of the incoming call to reject. Must not be empty.
&Jid
required
The JID of the caller.
Example:

Spam Reporting

send_spam_report

Send a spam report to WhatsApp for messages or groups.
SpamReportRequest
required
The spam report request containing:
  • message_id - ID of the message being reported
  • message_timestamp - Timestamp of the message
  • spam_flow - Context where report was initiated (MessageMenu, GroupInfoReport, etc.)
  • from_jid - Optional sender JID
  • group_jid - Optional group JID for group spam
  • group_subject - Optional group name/subject for group reports
  • participant_jid - Optional participant JID in group context
  • raw_message - Optional raw message bytes
  • media_type - Optional media type if reporting media
  • local_message_type - Optional local message type
Returns: SpamReportResult indicating success or failure Example:
SpamFlow variants:
  • MessageMenu - Reported from message context menu
  • GroupInfoReport - Reported from group info screen
  • GroupSpamBannerReport - Reported from group spam banner
  • ContactInfo - Reported from contact info screen
  • StatusReport - Reported from status view

Passive Mode

set_passive

Sets passive mode. When false (active), the server starts sending offline messages.

Prekeys

refresh_pre_keys

Force-refreshes the server’s one-time pre-key pool with a fresh batch. This is intended for device migration scenarios where you restore a device from an external source (e.g., migrating a Baileys session into an InMemoryBackend) and the server may still hold pre-key IDs whose private key material you cannot reconstruct. Any pkmsg referencing those old IDs will fail permanently with InvalidPreKeyId. Calling refresh_pre_keys() uploads a fresh batch that the caller does have locally, and old unmatched IDs drain as peers consume them. Behavior:
Only call this after restoring a device from an external session store. Under normal operation, the client manages pre-key uploads automatically.
Example:

send_digest_key_bundle

Validates that the server’s copy of the Signal Protocol key bundle matches local keys by querying a digest endpoint and comparing SHA-1 hashes. This matches WhatsApp Web’s WAWebDigestKeyJob.digestKey() flow. Behavior:
  • Queries the server for the current key bundle digest (identity key, signed pre-key, pre-key IDs, and a SHA-1 hash)
  • If the server returns 404 (no record), triggers a full pre-key re-upload
  • On success, loads local keys, computes the same SHA-1 digest, and compares
  • Hash mismatches or missing keys are logged but do not trigger re-upload — only 404 does
See Signal Protocol - Digest key validation for the wire format and detailed validation process.

Signed pre-key rotation

Rotation itself runs automatically: once per connection, after the post-login pre-key upload, the client checks whether the signed pre-key is due for rotation (weekly by default) and, if so, generates a fresh one, uploads it via an encrypt/<rotate> IQ, and retains the previous key so in-flight prekey messages still decrypt. Automatic rotation failures are logged and retried on a later connect — they never fail login.
rotate_signed_pre_key() is a public method — callers can force an out-of-cadence rotation directly instead of waiting for the weekly check. It shares a lock with the automatic path (so a manual call can’t race a background rotation) and, unlike the automatic path, propagates failures to the caller as SignalMaintenanceError instead of only logging them. As of PR #1090 this replaces bare anyhow::Error. See Signal Protocol - Signed pre-key rotation (RotateKeyJob) for the full sequence, wire format, and error handling.

Diagnostics

Three on-Client surfaces answer “what does this session cost?” without any feature flag: always-on wire I/O counters via stats(), an on-demand client-only memory breakdown via memory_report(), and an on-demand unified estimate — client plus storage, transport, and HTTP — via resource_report(). All three are dependency-free and safe to call once per client even when running many clients in one process. For CPU/custom attribution (e.g. per-session allocator tracking), see BotBuilder::with_task_instrument and BotBuilder::with_alloc_meter.

stats

Cumulative wire I/O and activity counters for this client session, always recorded — no feature gate. Byte counts are post-noise wire bytes (frame headers and AEAD tags included; handshake and TLS/WebSocket overhead excluded), so sessions from different clients in the same process can be compared directly. Cheap to call: it’s a copy of a handful of atomics. StatsSnapshot fields (#[non_exhaustive]): Most counters are monotonic over the client’s lifetime and survive reconnects. last_data_received_ms is the exception: it resets on connection teardown. reconnect_errors also resets, to 0 on every successful reconnect (it counts consecutive failures, not a lifetime total).
Breaking: last_data_sent_ms was removed — nothing internal ever read it, and stamping it cost a clock read on every frame written (the client’s hottest path, and a call out of the module on wasm32/embedded targets). frames_sent answers “is it still sending?”; there is no drop-in replacement for “when did I last write?” — an embedder that needs that timestamp should stamp it at its own send call site rather than have the wire path pay for it.
Example:

memory_report

Entry counts plus estimated retained heap bytes for the client’s internal collections. On-demand only — it walks the in-process caches under their locks when called, and costs nothing otherwise (unused report code is eliminated by fat LTO). Counts are approximate (caches may have pending evictions). Byte figures are honest estimates rather than byte-exact accounting — Signal records use their protobuf encoded size, other collections sum key/payload capacities — suitable for per-session attribution and leak detection. Store-backed caches (e.g. Redis-backed custom stores) report bytes: 0, since their entries don’t live in this process’s memory. MemoryReport fields (#[non_exhaustive]): CollectionStats carries both entries: u64 and bytes: u64. MemoryReport::total_estimated_bytes(&self) -> u64 sums .bytes across every byte-carrying field. MemoryReport implements Display for a pretty-printed, human-readable breakdown. This output includes an --- In-flight history sync --- section with the two peak fields above. Example:
Client::stats(), MemoryReport, CollectionStats, and StatsSnapshot were introduced to replace the old debug-diagnostics-gated memory_diagnostics() / MemoryDiagnostics, which have been removed. CollectionStats, MemoryReport, and StatsSnapshot are re-exported from the whatsapp_rust crate root.

resource_report

Unified per-session resource estimate: memory_report()’s client-only collections plus the components that live outside the Client and dominate real per-session RAM — the storage backend’s page cache, the transport’s buffers and TLS/noise state, and the HTTP client’s connection pool. When an AllocMeter is installed via BotBuilder::with_alloc_meter, the report also folds in an allocation-churn snapshot. On-demand only, no hot-path cost. Each out-of-client figure is best-effort — a component reports only what it can introspect, so absent (None) means “not reported”, not “zero”. resource_report()’s future is Send, so multi-session consumers can await it off a worker task (e.g. from an axum handler). ResourceReport fields (#[non_exhaustive]): StorageResourceReport fields: memory_bytes: Option<u64> (retained bytes; Some(0) for remote/store-backed backends whose data isn’t process memory), pages: Option<u64> (backing page/entry count), io_read_bytes / io_write_bytes: Option<u64> (cumulative I/O, when counted). TransportResourceReport fields: read_buffer_bytes, write_buffer_bytes, tls_state_bytes — all Option<u64>. HttpResourceReport fields: pool_connections: Option<u64>, pool_buffer_bytes: Option<u64>, inflight_bytes: Option<u64>. ResourceReport::total_estimated_bytes(&self) -> u64 sums the retained components (client + storage + transport + HTTP). Treat it as a best-effort retained estimate, not a strict lower bound: unreported fields (None) are treated as 0, so components that cannot fully introspect their footprint are silently undercounted — but the storage figure is itself a min(cache cap, db size) upper bound on the SQLite page cache, and can overstate actual heap residency when mmap_size is enabled (see the caveat there), so the total can run either high or low depending on configuration. alloc (churn) is deliberately excluded. ResourceReport implements Display for a pretty-printed breakdown alongside memory_report()’s. Example:
Storage, transport, and HTTP reports are supplied by the trait implementations behind Client — see DeviceStore::resource_report, Transport::resource_report, and HttpClient::resource_report. AllocSnapshot, StorageResourceReport, TransportResourceReport, and HttpResourceReport are re-exported from wacore::stats; all four are also re-exported from the whatsapp_rust crate root.

Error Types

As of PR #1090, connect()/wait_for_socket()/wait_for_connected() return ConnectError (ClientError::AlreadyConnected was removed in favor of ConnectError::AlreadyConnected), and rotate_signed_pre_key()/flush_pending_signal_state() return SignalMaintenanceError. See Error Types for the full reference across the crate.

See Also