Overview
This guide covers message event handling, decryption, and receipt management in whatsapp-rust.Event System
Subscribing to Events
Use the typed registrars onBotBuilder — they extract the relevant payload before calling your handler, so you never pattern-match on Arc<Event> for the common cases:
on_message, on_qr_code, on_pair_code, on_connected, on_logged_out. All handlers accumulate — registering a second one no longer silently replaces the first.
For events without a typed registrar, use the catch-all on_event / on_event_for:
&self, register a struct implementing EventHandler directly:
Available Events
Message Structure
MessageInfo
Every message event includes metadata:ephemeral_expiration field contains the disappearing messages timer in seconds, extracted from the message’s contextInfo.expiration. This tells you how long the message will be visible before it auto-deletes. Use this value when sending replies to the same chat via SendOptions.ephemeral_expiration.
The unavailable_request_id field is set when a message was recovered via PDO rather than normal decryption. It contains the PDO request message ID, which you can use to correlate recovered messages with the original UndecryptableMessage event.
The comment_target field is set when the dispatched message is a decrypted CAG channel comment. It contains the MessageKey of the parent post. The inner Message proto has no slot for the threading link, so it surfaces here instead. See Channel comments below.
Message content extraction
Use theMessageExt trait to extract content:
Message Types
Text Messages
Media Messages
Reactions
Incoming reactions — including encrypted CAG reactions — are dispatched in the samereaction_message shape. Encrypted reactions from Community Announcement Groups are decrypted transparently on the receive path; the key field is filled from the envelope’s target_message_key before dispatch.
Channel Comments
Encrypted channel comments from Community Announcement Groups are decrypted transparently and dispatched asEvent::Message carrying the comment body. The parent post key surfaces on MessageInfo::comment_target (the inner Message proto has no slot for the threading link):
messageSecret (carried in the outer envelope) is persisted under the comment’s id and sender, so that future encrypted reactions targeting the comment can be decrypted. comment_target is None for all other message types.
Quoted Messages
Message Unwrapping
DeviceSentMessage handling
When you send a message from one device, other devices receive it as aDeviceSentMessage wrapper. The library automatically unwraps this and merges messageContextInfo from both the outer envelope and inner message:
Self-sent messages synced from your primary device are automatically unwrapped. The
messageContextInfo is merged following WhatsApp Web’s logic, ensuring metadata like thread IDs and bot metadata are preserved correctly.Decryption
Automatic decryption
Messages are automatically decrypted by the client:Undecryptable Messages
When decryption fails, you receive anUndecryptableMessage event:
The client automatically handles decryption retries using the retry receipt mechanism. Failed messages trigger
Event::UndecryptableMessage, and the client will request re-encryption from the sender.Two-pass decryption model
Group messages arrive with two types of<enc> nodes in a single stanza:
- Session messages (
pkmsg/msg) — carry the Sender Key Distribution Message (SKDM) via a pairwise Signal session - Group messages (
skmsg) — carry the actual message content, encrypted with the sender key
- Pass 1: Process session
<enc>nodes to extract the SKDM, which establishes the sender key for the group. - Pass 2: Process group
<enc>nodes using the sender key from Pass 1.
skmsg decryption entirely (since it would always fail with NoSenderKey) and dispatches an UndecryptableMessage event. The retry receipt for the session message causes the sender to resend the entire message including the SKDM.
Before looking up or storing sender keys, the client normalizes the sender JID to its bare form (stripping the device component via to_non_ad()). This is necessary because WhatsApp delivers pkmsg stanzas (carrying SKDM) with a device-qualified participant JID, while skmsg stanzas use a bare participant JID. Without normalization, the sender key stored during SKDM processing would not match the key looked up during skmsg decryption. See Sender key address normalization for details.
When a group skmsg decryption fails with NoSenderKeyState (the sender key is missing or was never received), the client dispatches an UndecryptableMessage event before spawning the retry receipt. This ensures your application is immediately notified that the message is pending decryption, matching the behavior of the session-based decrypt path.
Exceptions where skmsg is still processed even without successful session decryption:
- No session messages present — the sender key was already established from a prior message
- Duplicate session messages — the SKDM was already processed in a previous delivery
This matches WhatsApp Web’s
canDecryptNext pattern. It prevents unnecessary retry receipts for skmsg nodes that can never succeed without the SKDM.Decrypt-fail mode
Each incoming message has adecrypt_fail_mode attribute parsed from the <enc> nodes:
DecryptFailMode::Show— the recipient should show a “waiting for this message” placeholder in the chatDecryptFailMode::Hide— the message should be silently hidden on failure (used for infrastructure messages like reactions, poll votes, pin changes, secret encrypted event/poll edits, message history notices, and certain protocol messages)
<enc> node in the stanza has decrypt-fail="hide", the entire message uses Hide mode. See Decrypt-fail suppression for which outgoing message types set this attribute.
Decryption retry mechanism
The library automatically:- Detects decryption failures (no session, invalid keys, MAC errors)
- Sends retry receipts with fresh prekeys
- Tracks retry count (max 5 attempts)
- Sends a parallel PDO (Peer Data Operation) request on the first retry
- Falls back to immediate PDO as last resort when retries are exhausted
Unavailable message recovery via PDO
When the server delivers a message with an<unavailable> child node instead of <enc> nodes, the message content is not present in the stanza. This happens when:
- A view-once message has already been viewed on another device
- The server cannot deliver the encrypted payload for other reasons
- The client detects the
<unavailable>node and its type (e.g.,view_onceor unknown) - An
UndecryptableMessageevent is dispatched immediately withis_unavailable: true - A PDO request (
PlaceholderMessageResend) is sent to your own bare JID (server routes to all devices including device 0) - The phone responds with the full
WebMessageInfocontaining the decrypted message - The client validates the response came from device 0 (primary phone) and dispatches the recovered message as a normal
Event::Message
MessageInfo includes unavailable_request_id — the PDO request message ID — so you can correlate recovered messages with the original UndecryptableMessage event.
PDO requests are deduplicated — if a request is already pending for a given message, subsequent requests are skipped. Pending requests expire after 30 seconds. The deduplication cache uses phone-number JIDs as keys (not LID JIDs) to ensure the cache key matches the JID format in the phone’s response.
Sent message retry (outbound)
When a recipient’s device cannot decrypt your message, it sends a retry receipt. The client handles this automatically using DB-backed sent message storage:- Every
send_message()persists the serialized message payload to thesent_messagesdatabase table - On retry receipt, the client retrieves the original payload, re-encrypts it for the requesting device, and resends
- The payload is consumed (deleted) on retrieval to prevent double-retry
- Expired entries are periodically cleaned up based on
sent_message_ttl_secs(default: 5 minutes)
getMessageTable pattern of reading from persistent storage on retry receipt.
An optional in-memory L1 cache (
recent_messages in CacheConfig) can be enabled for faster retry lookups. When disabled (default, capacity 0), all retry lookups go directly to the database. See Bot - Cache Configuration Reference for details.Receipts
Automatic delivery receipts
The client automatically sends delivery receipts for successfully decrypted messages:Sending read receipts
Receipt Events
Handle receipt updates from other participants:Advanced Usage
Custom encryption handlers
For custom encryption types (e.g.,pkmsg, msg, skmsg):
Filtering Messages
Use the type-safe JID methods (is_group(), is_broadcast_list(), is_status_broadcast()) to classify messages by chat type. With on_message, the MessageContext is already available:
Session and key management
The library automatically manages Signal Protocol sessions:IdentityChange event after the client has completed all session cleanup. The client also re-issues TC tokens in the background to maintain privacy token continuity.
See Signal Protocol for more on session management.
Error Handling
Best Practices
Next Steps
- Sending Messages - Send text, reactions, channel comments, and replies
- Media Handling - Download and process media
- Group Management - Handle group events
- Community management - CAG reactions and channel comments