send_message
Send a message to a user, group, or newsletter. Newsletter messages are sent as plaintext (no E2E encryption) automatically when the recipient is a newsletter JID.impl Into<Jid>
required
Recipient JID. Can be:
- Direct message:
15551234567@s.whatsapp.net - Group:
120363040237990503@g.us - Newsletter:
120363999999999999@newsletter
type and mediatype stanza attributes inferred automatically.wa::Message
required
Protobuf message to send. Set one of the message fields:
conversation- Plain text messageextended_text_message- Text with formatting/linksimage_message- Image with captionvideo_message- Video with captiondocument_message- Document/fileaudio_message- Audio/voice notesticker_message- Stickersticker_pack_message- Sticker pack (grouped sticker collection)location_message- GPS locationcontact_message- Contact cardalbum_message- Album (grouped media) parent message
SendResult
Contains the
message_id (unique ID for tracking receipts, edits, revokes) and to (resolved recipient JID). Use send_result.message_key() to get a wa::MessageKey for album child linking, pinning, or other operations that reference this message.The outbound Signal ratchet advance is persisted through a batched counter lease, for DMs and for group/status sends alike. For DMs,
SessionRecord reserves its sender-chain counter 64 at a time; for group sends and status posts sent via client.status(), SenderKeyRecord reserves its chain iteration 64 at a time the same way. Most sends are already covered by a durable lease and only schedule a coalesced write-behind — though the pre-wire flush check is global, so a pending flush on an unrelated session or sender key can still force this send to flush synchronously. The send that exhausts the current lease, roughly 1 in 64, persists to the backend synchronously, before the stanza is transmitted. Status reactions (send_reaction targeting status@broadcast) are the exception: they route through the same DM branch as an ordinary 1:1 message, addressed to the status author’s device, so they follow the DM counter-lease behavior instead of the group/status one. Reusing an outbound counter or iteration would reuse its message key and IV, so the advance is always durable before it can be reused. If a required persistence write fails, send_message returns Err instead of transmitting an advance that couldn’t be saved. See Signal Protocol — flush scheduling for the full durability model.SendResult
Result of a successfully sent message. Provides the message ID and a convenience method to construct aMessageKey for follow-up operations like album child linking.
SendResult is #[non_exhaustive]: struct-literal construction and exhaustive struct destructuring from outside the crate are both disallowed. Field reads are unaffected; add .. to any exhaustive destructuring patterns.
ChatMessageId
Identifies a specific message within a chat. Useful for operations that need both the chat and message ID together.Example: text message
Example: Newsletter message
Newsletter reactions use a different stanza format and are still sent through
client.newsletter().send_reaction(). See the Newsletter API.Example: image with caption
Example: Album (grouped media)
Send multiple images and/or videos as a single grouped album. First send the parentAlbumMessage with expected counts, then send each child media wrapped with wrap_as_album_child:
forward_message
Forward an existing message to a chat. Builds a forward-ready copy ofmessage and sends it via send_message.
impl Into<Jid>
required
Recipient JID (DM, group, or newsletter).
&wa::Message
required
Source message to forward. May be a received body or a wrapper (ephemeral / view-once); the inner content is unwrapped automatically before sending.
SendResult
Same shape as
send_message: contains the new message_id and the resolved recipient JID.- Sets
context_info.is_forwarded = trueso the recipient sees the Forwarded label. - Bumps
forwarding_score. At 5 it jumps to the127sentinel that clients render as Forwarded many times. - Strips the reply/quote chain and mentions from the source message.
- Drops the source
message_context_infoso the send path mints a freshmessage_secret. - Promotes a bare
conversationtoextended_text_messageso the forward marker can attach. - Relays existing media from the same CDN blob (
media_key,url, and friends are carried over) — no re-download or re-upload.
Example: forward a received message
MessageExt::prepare_for_forward, which returns the prepared wa::Message without sending it.
send_message_with_options
Send a message with additional customization options.impl Into<Jid>
required
Recipient JID
wa::Message
required
Protobuf message to send
SendOptions
required
Additional send options (see below)
SendResult
Contains the message ID and recipient JID
SendOptions
Options for customizing message sending behavior.SendOptions is #[non_exhaustive], so it can no longer be constructed as a struct literal (even with ..Default::default()) from outside the crate. Build it by chaining the with_* setters off SendOptions::default() instead: SendOptions::default().with_message_id(id). Each field above has a matching with_* method (with_message_id, with_extra_stanza_nodes, with_ephemeral_expiration, with_stanza_type_override).Option<String>
default:"None"
Override the auto-generated message ID. When set, the provided ID is used instead of generating a new one. Useful for resending a failed message with the same ID or ensuring idempotency.
Vec<Node>
default:"[]"
Additional XML nodes to include in the message stanza. Used for advanced protocol features like quoted replies, mentions, or custom metadata.
Option<u32>
default:"None"
Sets the ephemeral (disappearing) message duration in seconds by injecting
contextInfo.expiration on the protobuf message. When the recipient’s chat has disappearing messages enabled, set this to match the chat’s ephemeral timer. Common values: 86400 (24 hours), 604800 (7 days), 7776000 (90 days). Pass 0 or None to send a non-ephemeral message.Option<StanzaType>
default:"None"
Forces the
<message type="..."> attribute on the outgoing stanza instead of letting the content classifier pick one. Leave as None for normal sends — the library infers the correct type from the protobuf payload. Set this only when you’re sending a message variant the classifier can’t recognize and the server requires a specific wire type. See Stanza types for the available values.The override is applied when the stanza is first sent. The retry path reclassifies from content, so an override doesn’t follow a message through resends.
Example: Send with a custom message ID
Example: Send an ephemeral (disappearing) message
The
ephemeral_expiration value should match the chat’s disappearing messages timer. You can get this from GroupMetadata.ephemeral (via metadata.ephemeral.as_ref().and_then(|e| e.expiration)) for groups, or from MessageInfo.ephemeral_expiration on received messages. See the sending messages guide for a complete walkthrough.Example: Send with extra stanza nodes
edit_message
Edit a previously sent message.impl Into<Jid>
required
Chat JID where the original message was sent
String
required
ID of the message to edit (from
send_message return value)wa::Message
required
New message content to replace the original
String
Message ID of the edit message
Example: Edit a message
The edit is sent as a top-level
protocolMessage with type = MESSAGE_EDIT, matching the WhatsApp Web wire shape. In group chats, the correct participant JID (LID or PN) is resolved for you, and a fresh stanza ID is used so the server does not deduplicate the edit against the original message.edit_message_with_options
Edit-path counterpart tosend_message_with_options. Builds the same protocolMessage edit as edit_message, but accepts an EditOptions struct for cases where the default fresh-stanza-id behavior isn’t what you want.
impl Into<Jid>
required
Chat JID where the original message was sent
String
required
ID of the message to edit (from
send_message return value)wa::Message
required
New message content to replace the original
EditOptions
required
Edit options (see below)
String
Message ID of the edit message
EditOptions
EditOptions is #[non_exhaustive], so it can no longer be constructed as a struct literal from outside the crate. Build it with EditOptions::default().with_stanza_id(id) instead.Option<String>
default:"None"
Overrides the outer stanza id normally auto-generated by
edit_message. The edit-path counterpart of SendOptions::message_id — pin the outer id to an existing message’s id so clients re-render that slot instead of appending a new one.Pinning stanza_id to a borrowed id (one that already belongs to another message) is a best-effort, side-effect-aware operation:- The edit does not persist a retry-cache entry or an outbound message secret under the borrowed id, so the original message’s retry content and secret are left intact.
- The edit does not register a phash ack-waiter under the borrowed id either, since the waiter map is keyed by outer stanza id and registering one would risk resolving the wrong send.
- Whether the server and recipient clients actually honor the collision (dedupe/re-render onto the borrowed id) is server- and client-dependent — treat the visible outcome as non-guaranteed, not a reliable primitive.
SendError::InvalidRequest, same as an empty SendOptions::message_id.Example: edit with a pinned stanza id
Leave
stanza_id as None (or use plain edit_message) for ordinary edits — a fresh id is what prevents the server from deduplicating the edit against the original message. Only set stanza_id when you specifically need to collide the outer stanza id with an existing message.edit_message_encrypted
Edit a message via the message-secret encrypted path (asecret_encrypted_message with secret_enc_type = MESSAGE_EDIT) instead of the plaintext protocolMessage edit produced by edit_message. This is the form Community Announcement Groups require, and the shape WhatsApp Web sends when its message_edit_to_message_secret_sender_enabled flag is on. The new content is encrypted under the original message’s secret.
impl Into<Jid>
required
Chat JID where the original message was sent. Newsletter/channel JIDs are rejected — use
Newsletter::edit_message for channels.String
required
ID of the message to edit. You can only edit your own messages, so the original sender and the editor are both you.
&[u8]
required
The 32-byte secret of the original message. Must be exactly 32 bytes. Persist it from
MessageContextInfo.message_secret on the sent message and retrieve it by message ID from your store.wa::Message
required
Replacement message content.
String
Message ID of the edit message.
Example: encrypted edit
Use
edit_message for ordinary DM and group edits. Reach for edit_message_encrypted only when the chat requires the message-secret edit form (e.g. Community Announcement Groups). Inbound encrypted edits are decrypted automatically on receive — see decrypting secret-encrypted envelopes.revoke_message
Delete a message for everyone in the chat (revoke). This sends a revoke protocol message that removes the message for all participants. The message will show as “This message was deleted” for recipients.impl Into<Jid>
required
Chat JID (direct message or group)
String
required
ID of the message to delete (from
send_message return value)RevokeType
required
Who is revoking the message:
RevokeType::Sender- Delete your own messageRevokeType::Admin { original_sender }- Admin deleting another user’s message in a group
RevokeType
Specifies who is revoking (deleting) the message.RevokeType is #[non_exhaustive], so match statements should include a wildcard arm to handle future variants.
variant
Default variant. Use when deleting your own message. Works in both DMs and groups.
variant
Use when a group admin is deleting another user’s message. Only valid in groups. Requires
original_sender JID.Example: revoke own message
Example: admin revoke in group
Admin revoke is only valid for group chats. Attempting to use it in a direct message will return an error.
Since v0.6 admin revoke fan-outs are propagated correctly to every recipient device on retry: the original stanza carries
edit="8" (EditAttribute::AdminRevoke), and prepare_dm_retry_stanza now accepts that attribute and re-emits it on each retry stanza. Previously the retry path stripped the edit attribute, so a single dropped device fan-out left the message un-deleted on that device. The library infers the right attribute from the protocol-message type via EditAttribute::infer_from_message(...), so applications calling revoke_message need no code changes.pin_message
Pin a message in a chat for all participants.impl Into<Jid>
required
Chat JID where the message to pin is located
wa::MessageKey
required
The message key identifying which message to pin. Construct this from the message’s chat JID, message ID, sender info, and participant (for groups).
PinDuration
required
How long the message should remain pinned (see below)
PinDuration
Specifies how long a message stays pinned. Defaults to 7 days (matches WhatsApp Web behavior).PinDuration is #[non_exhaustive], so match statements should include a wildcard arm to handle future variants.
variant
Pin for 24 hours
variant
Pin for 7 days (default)
variant
Pin for 30 days
Example: Pin a message for 7 days
Example: Pin a group message for 30 days
unpin_message
Unpin a previously pinned message.impl Into<Jid>
required
Chat JID where the pinned message is located
wa::MessageKey
required
The message key identifying which message to unpin
Example: Unpin a message
keep_message
Keep (or un-keep) a message in a disappearing chat for everyone. This is thekeepInChatMessage add-on used by WhatsApp Web’s “Keep in chat” action: when a chat has disappearing messages enabled, keeping a message prevents it from being deleted when the ephemeral timer expires.
impl Into<Jid>
required
Chat JID where the target message lives.
wa::MessageKey
required
The message key identifying the message to keep or un-keep. Construct this from the target message’s chat JID, message ID, sender info, and participant (for groups).
bool
required
true requests KEEP_FOR_ALL (keep the message past the disappearing timer). false requests UNDO_KEEP_FOR_ALL (reverse a previous keep).timestamp_ms records the send time (not the kept message’s timestamp). The send path classifies this as a text add-on and maps the undo case to a sender-revoke edit attribute automatically, so no extra wiring is required.
Example: Keep a message for everyone
Example: Undo a previous keep
set_chat_disappearing_timer
Turn disappearing messages on or off for a 1:1 chat. Sends anEPHEMERAL_SETTING protocol message, mirroring WhatsApp Web’s chat-action.
Jid
required
The 1:1 chat (PN or LID). Group, status, and newsletter JIDs are rejected — for groups use
Groups::set_ephemeral; for the account default use Client::set_default_disappearing_mode.u32
required
Timer in seconds. Common values:
86400 (24h), 604800 (7 days), 7776000 (90 days). Pass 0 to turn disappearing messages off.SendResult
Result of the setting message send. See SendResult.
Example: enable and disable
This sets the chat-wide timer. To keep an individual message past the timer, use
keep_message.send_reaction
React to a DM, group, orstatus@broadcast message with an emoji. The helper builds the ReactionMessage payload (including sender_timestamp_ms) and routes it through the standard send path, so the same retry, fan-out, and phash logic that applies to other messages applies here.
impl Into<Jid>
required
Chat JID where the reaction is delivered. For
status@broadcast, this is the broadcast JID; the reaction fans out to the status author’s devices using target_key.participant.wa::MessageKey
required
Identifies the message being reacted to.
remote_jid— chat JID of the target messagefrom_me—trueif you sent the original message, otherwisefalseid— message ID of the target messageparticipant— original sender JID. Required for groups andstatus@broadcast; leave asNonefor DMs.
&str
required
Emoji to send (e.g.
"👍", "❤️"). Pass an empty string ("") to remove a previous reaction — this matches WhatsApp Web’s empty-text-as-revoke behavior.SendResult
Contains the reaction’s
message_id and resolved recipient to JID. See SendResult.Example: react to a DM
Example: react to a group message
Example: remove a reaction
Inside an event handler, prefer
MessageContext::react — it derives chat, target_key, and participant from the incoming message for you.Newsletter (channel) reactions use a different plaintext stanza format and are not handled here. Use
client.newsletter().send_reaction() for newsletters.Phash validation (stale device list detection)
When sending group, status, or DM messages, the library automatically validates the participant hash (phash) from the server’s acknowledgment against the locally computed value. If the hashes differ, the appropriate caches are invalidated so the next send re-fetches current participant devices from the server:
- Group messages: sender key device cache and group info cache are invalidated
- Status messages: sender key device cache is invalidated
- DM messages: the device registry cache is invalidated for both the recipient and your own phone number (PN), matching WA Web’s
syncDeviceListJob([recipient, me])behavior
PreparedDmStanza.phash field and compared against the server’s ACK phash.
This runs asynchronously in the background and does not block the send path. See Signal Protocol — Phash validation for implementation details.
Automatic stanza metadata
When you callsend_message or send_message_with_options, the library automatically infers and injects stanza-level metadata that WhatsApp servers expect for certain message types. This applies to all recipient types — direct messages, groups, and newsletters. You never need to set these manually — the library handles it for you.
This means you can construct a raw
wa::Message with any of these fields set and pass it directly to send_message_with_options — the correct protocol metadata is derived automatically. Any nodes you provide via extra_stanza_nodes in SendOptions are merged with the auto-inferred nodes.
The
pin_message() and unpin_message() convenience methods already handle this internally. Auto-detection is most useful when you build a wa::Message manually and send it through send_message or send_message_with_options.Automatic business node detection
When you send anInteractiveMessage with a NativeFlowMessage (used for business features like payments, CTAs, and catalogs), the library automatically injects a <biz> stanza child node. You don’t need to construct this manually.
The detection works by:
- Inspecting the outgoing message for an
InteractiveMessagewith aNativeFlowMessage - Extracting the first button’s
namefield - Mapping the button name to a WhatsApp flow name
- Building the
<biz>XML node with the correct structure
Supported button-to-flow mappings
Unrecognized button names pass through as-is.
Payment vs nested-form vs fallback shapes
The<biz> node is emitted in one of three shapes depending on the button content, matching WA Web’s reproducer for native-flow stanzas:
- Payment buttons (
review_and_pay,payment_info,payment_status, …) — emitted as a flat<native_flow name="…">with aprivacy_mode_tsattribute.privacy_mode_tsis the current Unix timestamp from the newwacore::time::now_secs_u64()helper, which safely handles clocks set before 1970 by returning0instead of panicking. - Nested-form buttons (
cta_*,quick_reply,galaxy_message, …) — emitted with the<interactive type="native_flow" v="1">wrapper shown above. - Mixed / unrecognized — falls back to the wrapper form for forward compatibility.
bot_invoke_message continues to emit a <bot> stanza child instead of <biz> and is unaffected by the above shapes.
The
<biz> node is merged with any other auto-inferred metadata (like <meta> nodes for polls or events) and any extra_stanza_nodes you provide in SendOptions. The library also checks inside document_with_caption_message wrappers for interactive messages.Decrypt-fail suppression
Certain infrastructure messages setdecrypt-fail="hide" on their <enc> nodes so recipients don’t see “waiting for this message” placeholders when decryption fails. The library applies this automatically based on message content — you don’t need to handle it manually.
The following message types are marked with decrypt-fail="hide":
Additionally,
decrypt-fail="hide" is applied for:
- Messages with an
editattribute (exceptEmpty,AdminRevoke, andSenderRevoke— WA Web never hides revokes and the server rejects revoke stanzas carrying this attribute) - Sender Key Distribution Message (SKDM) stanzas — always hidden since they are infrastructure-only
Wrapper messages (
ephemeral_message, view_once_message, etc.) are unwrapped before checking. The decrypt-fail attribute is set on the inner <enc> node, not the outer <message> stanza.Privacy token attachment
When you send a 1:1 message (not to groups, newsletters, or yourself), the library automatically attaches a privacy token to the outgoing stanza. This follows WhatsApp Web’sMsgCreateFanoutStanza.js fallback chain:
After sending, if the TC token bucket boundary has been crossed (7-day buckets), the library automatically issues a new TC token to the recipient in the background.
Privacy token selection is fully automatic. The library resolves the recipient’s LID (using the LID-PN cache), looks up stored TC tokens, and falls back to cstoken computation when needed. See the TC Token API for details on the token lifecycle and NCT salt provisioning.
Stanza types
When you send a message, the library automatically determines two protocol-level type attributes based on the protobuf message content. You don’t need to set these manually, but understanding them can help with debugging.Message stanza type
Thetype attribute on the outer <message> XML node is determined by stanza_type_from_message:
The payment-family messages (
request_payment_message, send_payment_message, payment_invite_message, decline_payment_request_message, cancel_payment_request_message) classify as "text". These message types only exist on the Android client and are silently dropped by the server when sent as "media" (no mediatype) or as "pay"; "text" is the wire type that actually delivers.Overriding the stanza type
When the classifier can’t recognize a message variant, setSendOptions.stanza_type_override to force the <message type="..."> attribute. Use the StanzaType enum:
StanzaType variants map to the wire values listed above plus "pay":
Leave
stanza_type_override as None for normal sends — the classifier already handles every supported message type.
Encrypted media type
Themediatype attribute on the inner <enc> XML node provides a more specific media classification. This is set by media_type_from_message and is omitted for text-only messages:
Both stanza type and encrypted media type are resolved automatically by the library before encryption. Wrapper messages are unwrapped first to determine the underlying content type. Since v0.6
unwrap_message peels a much wider set of FutureProofMessage wrappers — group_status_mention_message / groupStatusV2, spoiler, question, newsletter, lottie, and ~15 others — so classification follows the inner content rather than defaulting the wrapper to "text" (aligning with WA Web).Message types
Thewa::Message protobuf supports various message types. Set exactly one of these fields:
Text Messages
String
Simple text message without formatting
ExtendedTextMessage
Text with formatting, links, quoted replies, or mentionsKey fields:
text- Message textcontextInfo- Quoted message, mentionspreviewType- Link preview behavior
Media Messages
ImageMessage
Image with optional caption. Upload the image first using
client.upload(), then populate:url,direct_path,media_key,file_enc_sha256,file_sha256,file_length,media_key_timestampcaption- Image captionmimetype- e.g.,"image/jpeg"
VideoMessage
Video with optional caption. Same upload pattern as images.
AudioMessage
Audio file or voice note:
ptt- Set totruefor voice notes (Push-To-Talk)mimetype- e.g.,"audio/ogg; codecs=opus"
DocumentMessage
Document/file with metadata:
file_name- Original filenamemimetype- File MIME typecaption- Optional description
StickerMessage
Sticker image (WebP format)
StickerPackMessage
Sticker pack containing multiple stickers as a ZIP. Build using
create_sticker_pack_zip and build_sticker_pack_message from wacore::sticker_pack. Requires two uploads: the sticker pack ZIP (MediaType::StickerPack) and a JPEG thumbnail (MediaType::StickerPackThumbnail) sharing the same media_key. See sticker packs.Other Messages
LocationMessage
GPS location with latitude, longitude, and optional name/address
ContactMessage
Contact card with vCard data
ContactsArrayMessage
Multiple contact cards
LiveLocationMessage
Real-time location sharing
ReactionMessage
Emoji reaction to another message
PollCreationMessage
Poll with multiple options. Use
client.polls().create() for a higher-level API that handles message secret generation automatically. See Polls API.EventMessage
Calendar event message. The required
<meta event_type="creation"/> stanza node is injected automatically when sent through send_message or send_message_with_options.AlbumMessage
Album parent message declaring the expected number of grouped media items. Set
expected_image_count and/or expected_video_count. After sending the parent, use wrap_as_album_child to wrap each child media message and link it to the parent via SendResult::message_key(). See album messages.Example: Extended text with quote
Usebuild_quote_context_with_info to create a reply with the correct participant and remote_jid fields. It takes both the quoted message’s chat (quoted_chat_jid) and the chat you’re sending into (target_chat_jid):
remote_jid is emitted only when quoted_chat_jid and target_chat_jid refer to different chats (a cross-chat quote, such as quoting a status into a DM). Same-chat replies omit it, matching WhatsApp Web. For newsletter chats, the participant field is automatically set to the newsletter JID instead of the sender.
Error types
SendError
All send-path methods return Result<T, SendError>:
NotLoggedIn— client is not authenticated; check connection state before sendingIq— IQ request required by the send path failedInvalidRequest— the send request was malformed (e.g., invalid JID, bad message shape)Client— underlying transport/connection errorInternal— catch-all for errors not yet assigned a typed variant. Includes a failure to durably persist the outbound Signal ratchet advance before the stanza was sent — see the durability note undersend_message.