Skip to main content
This entry catches the documentation up with the remaining early-June API work that landed after the June 8 release.

New features

Events with RSVP (client.events()) A new Events feature creates WhatsApp event messages and collects encrypted RSVPs.
  • client.events().create(to, params) sends an event and returns (SendResult, message_secret). Like polls, the event carries a per-message secret; store it — responders’ RSVPs are encrypted against it.
  • client.events().respond(chat, event_id, creator, secret, response, extra_guests) sends an encrypted Going / NotGoing / Maybe reply.
  • wacore::event::{encrypt_event_response_with_secret, decrypt_event_response_with_secret} decrypt inbound RSVPs.
use whatsapp_rust::features::{EventCreationParams, EventResponseType};

// Create an event — keep the returned secret to read RSVPs later.
let (sent, secret) = client.events().create(&chat, EventCreationParams {
    name: "Team offsite".into(),
    start_time: Some(1_760_000_000),
    ..Default::default()
}).await?;

// RSVP to an event you received (event id, creator, and secret come from that event).
client.events()
    .respond(&chat, &event_id, &creator, &event_secret, EventResponseType::Going, None)
    .await?;
See the Events API reference for the full field list. Quiz polls (Polls::create_quiz) client.polls().create_quiz(to, name, options, correct_index) sends a single-select quiz poll with one correct answer (correct_index is the 0-based index into options). It returns (SendResult, message_secret) just like create, and votes decrypt the same way. See create_quiz. 1:1 disappearing-message timer (Client::set_chat_disappearing_timer) Turn disappearing messages on or off for a direct chat with client.set_chat_disappearing_timer(chat, duration_secs) (0 disables). It sends an EPHEMERAL_SETTING protocol message and is 1:1-only — use Groups::set_ephemeral for groups. See set_chat_disappearing_timer. Save or rename contacts (ChatActions::save_contact) client.chat_actions().save_contact(jid, full_name, first_name, save_on_primary_addressbook) writes a contact app-state mutation that syncs the name to your other linked devices. The contact id must be a bare phone-number JID; LIDs and device-specific JIDs are rejected. See save_contact. Clear a chat’s messages (ChatActions::clear_chat) client.chat_actions().clear_chat(jid, delete_starred, delete_media) clears a chat’s messages while keeping the chat, syncing across devices. Inbound clears from a linked device arrive as the new Event::ClearChatUpdate. See clear_chat. Mute a contact’s status updates (ChatActions::set_user_status_mute) client.chat_actions().set_user_status_mute(jid, muted) mutes/unmutes a contact, group, or channel’s status updates across devices. Inbound changes arrive as Event::UserStatusMuteUpdate. See set_user_status_mute. Mute newsletter notifications (Newsletter::set_follower_mute / set_admin_mute) Silence a channel’s follower- or admin-activity notifications via MEX:
client.newsletter().set_follower_mute(&channel, true).await?;  // mute
client.newsletter().set_admin_mute(&channel, false).await?;    // unmute
See set_follower_mute. Message-secret encrypted edits (Client::edit_message_encrypted) client.edit_message_encrypted(to, original_id, new_content) edits a message via the secret_encrypted_message (MESSAGE_EDIT) path instead of the plaintext protocolMessage edit. This is the form Community Announcement Groups require and what WhatsApp Web sends when message_edit_to_message_secret_sender_enabled is on. Newsletters are rejected — use Newsletter::edit_message. See edit_message_encrypted. High-level media message builders (whatsapp_rust::media) The new media module turns an UploadResponse into a ready-to-send wa::Message, so you no longer hand-assemble the CDN/crypto fields (url, direct_path, media_key, the two SHA-256 hashes, file_length, media_key_timestamp, streaming_sidecar):
use whatsapp_rust::media::{self, ImageOptions};

let upload = client.upload(bytes, MediaType::Image, Default::default()).await?;
let msg = media::image_message(upload, ImageOptions { caption: Some("hi".into()), ..Default::default() });
client.send_message(to, msg).await?;
image_message, video_message, document_message, and audio_message each take a typed options struct with sensible MIME defaults. See media message builders. Device list from get_user_info UserInfo now carries devices: Vec<u16> — the device IDs from the <devices version="2"> sublist the same usync query returns (device 0 is the primary). Empty when the server omits it. offline flag on the Receipt event Event::Receipt gains offline: bool, set when the receipt was drained from the server’s offline queue on reconnect rather than delivered live. See the receipt event structure. Read-receipt status parity Outgoing read receipts now match WhatsApp Web: newsletter reads send read-self, and status reads carry context="status" plus peer_participant_pn (the resolved LID→PN) for a LID author. No API change — mark_as_read handles it. View-once sends emit <meta view_once="true"/> View-once image/video/voice sends now carry the view_once meta attribute, matching WhatsApp Web so recipients render the one-time bubble. Members tagged for member labels also emit appdata / tag_reason meta attributes on group sends.

Breaking changes

Client::custom_enc_handlers field type changed (#792) The pub custom_enc_handlers field type changed from Arc<async_lock::RwLock<HashMap<String, Arc<dyn EncHandler>>>> to std::sync::OnceLock<HashMap<String, Arc<dyn EncHandler>>>. Migration depends on how the field was used: code that read handlers via .read().await should switch to .get(); code that inserted handlers via .write().await.insert(...) must move those registrations to BotBuilder::with_enc_handler() before build() — runtime insertion is no longer possible. get_participating returns HashMap<Jid, GroupMetadata> Groups::get_participating is now keyed by Jid instead of String. Bind the key as a Jid; call .to_string() if you need the old string form. download_from_params takes a DownloadParams struct download_from_params and download_from_params_to_writer now take a single &DownloadParams instead of six positional arguments. Build one with DownloadParams::encrypted(direct_path, media_key, file_sha256, file_enc_sha256, file_length, media_type). DownloadParams implements Downloadable, so it also works directly with client.download(&params). download_to_file removed The dead, fully-buffering download_to_file is gone. Use download_to_writer (streaming, constant-memory) instead — it accepts any File/BufWriter and returns the writer seeked to 0. set_description takes prev: Option<&str> Groups::set_description now borrows the previous description id (Option<&str>) instead of taking an owned Option<String>. Pass Some("PREV_ID") or None. mark_as_read / mark_as_played take &[&str] mark_as_read and mark_as_played now take message_ids: &[&str] instead of Vec<String>, avoiding per-call allocations. Update call sites from vec!["ID".to_string()] to &["ID"].

Fixes & hardening

App-state anti-tampering parity: snapshot application now requires a valid snapshot MAC (no silent skip), rejects duplicate indices within a patch, and guards against version rollback — matching WhatsApp Web’s validation so a malicious or corrupted server snapshot can’t desync or tamper with state. Poll wire shape: poll creation now matches WhatsApp Web (pollContentType=TEXT, no vote metadata on the creation message), improving interop with other clients. Companion device-identity (ADV) validation: fetched pre-key bundles are validated against the companion device-identity, rejecting bundles whose identity doesn’t check out before a session is built. ADV account-key fallback from store (#790): Fixed a regression where contacts’ companion devices (WhatsApp Web / Desktop) stopped receiving messages after the ADV validation above was introduced. The server legitimately omits account_signature_key from a companion’s <device-identity> because the client already holds that key as the contact’s primary (device 0) identity in the Signal store — the prior validation rejected those bundles outright. The fix mirrors WA Web’s validateADVwithIdentityKey (e.accountSignatureKey || t): the in-blob key is preferred; when absent, the contact’s stored primary identity is loaded via Client::load_account_identity and used as the fallback. The validation result is now the three-state wacore::adv::AdvValidation (Valid / Invalid / NoAccountKey) instead of a boolean — an unverifiable-for-lack-of-key bundle is logged and kept rather than dropped, preserving the existing “device-identity absent” behaviour. IQ errors keep error_type + backoff: failed IQ responses now surface the server’s error_type and server-directed backoff (seconds) instead of dropping them, so callers can honour retry hints. wasm: the cache backend is now target-aware so the moka-cache default no longer breaks wasm32 builds. EncHandler wasm portability (#793): EncHandler now uses MaybeSendSync as its supertrait and gates async_trait with ?Send on wasm32, matching the convention of EventHandler and SendContextResolver. Custom enc handlers that capture !Send JS handles now compile on the wasm32 port. No change on native — the blanket MaybeSendSync impl keeps Arc<dyn EncHandler> Send + Sync. Networking traits wasm portability (#795): Transport, TransportFactory, and HttpClient now use MaybeSendSync as their supertrait, matching the convention established by EventHandler, SendContextResolver, and EncHandler. Custom transport and HTTP implementations that hold !Send JS handles (e.g. a browser WebSocket or fetch backing) now compile on the wasm32 port. No change on native — the blanket MaybeSendSync impl keeps Arc<dyn Transport> and Arc<dyn HttpClient> Send + Sync there.