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.Recipient JID. Can be:
- Direct message:
15551234567@s.whatsapp.net - Group:
120363040237990503@g.us - Newsletter:
120363999999999999@newsletter
type and mediatype stanza attributes inferred automatically.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
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.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.
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:
send_message_with_options
Send a message with additional customization options.Recipient JID
Protobuf message to send
Additional send options (see below)
Contains the message ID and recipient JID
SendOptions
Options for customizing message sending behavior.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.
Additional XML nodes to include in the message stanza. Used for advanced protocol features like quoted replies, mentions, or custom metadata.
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.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_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.Chat JID where the original message was sent
ID of the message to edit (from
send_message return value)New message content to replace the original
Message ID of the edit message
Example: Edit a message
The edit wraps your new content in an
edited_message (FutureProofMessage) envelope automatically. In group chats, the correct participant JID (LID or PN) is resolved for you.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.Chat JID (direct message or group)
ID of the message to delete (from
send_message return value)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.
Default variant. Use when deleting your own message. Works in both DMs and groups.
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.
pin_message
Pin a message in a chat for all participants.Chat JID where the message to pin is located
The message key identifying which message to pin. Construct this from the message’s chat JID, message ID, sender info, and participant (for groups).
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.
Pin for 24 hours
Pin for 7 days (default)
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.Chat JID where the pinned message is located
The message key identifying which message to unpin
Example: Unpin a message
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.
| Message type | Auto-injected metadata |
|---|---|
pin_in_chat_message | Sets the edit="2" attribute on the message stanza |
poll_creation_message (v1, v2, v3) | Adds a <meta polltype="creation"/> child node |
poll_update_message (with vote) | Adds a <meta polltype="vote"/> child node |
event_message | Adds a <meta event_type="creation"/> child node |
enc_event_response_message | Adds a <meta event_type="response"/> child node |
secret_encrypted_message with SecretEncType::EventEdit | Adds a <meta event_type="edit"/> child node |
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
| Button name | Flow name |
|---|---|
review_and_pay | order_details |
payment_info | payment_info |
review_order, order_status | order_status |
payment_status | payment_status |
payment_method | payment_method |
payment_reminder | payment_reminder |
open_webview | message_with_link |
message_with_link_status | message_with_link_status |
cta_url | cta_url |
cta_call | cta_call |
cta_copy | cta_copy |
cta_catalog | cta_catalog |
catalog_message | catalog_message |
quick_reply | quick_reply |
galaxy_message | galaxy_message |
booking_confirmation | booking_confirmation |
call_permission_request | call_permission_request |
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":
| Message type | Condition |
|---|---|
reaction_message | Always |
enc_reaction_message | Always |
pin_in_chat_message | Always |
edited_message | Always |
keep_in_chat_message | Always |
enc_event_response_message | Always |
poll_update_message | Only when .vote is present |
message_history_notice | Always |
secret_encrypted_message | Only when SecretEncType is EventEdit or PollEdit |
bot_invoke_message | Only when the inner protocol_message has type RequestWelcomeMessage |
protocol_message | When type is EphemeralSyncResponse, RequestWelcomeMessage, or GroupMemberLabelChange, or when edited_message is present |
decrypt-fail="hide" is applied for:
- Messages with an
editattribute (exceptEmptyandAdminRevoke— the server rejects admin revokes with 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:
| Priority | Token type | Condition | Stanza node |
|---|---|---|---|
| 1 | tctoken | Stored TC token exists and hasn’t expired (within 28-day rolling window) | <tctoken> |
| 2 | cstoken | No valid TC token, but NCT salt and recipient LID are available | <cstoken> |
| 3 | None | Neither token nor salt available | No token node |
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:
| Stanza type | Message types |
|---|---|
"text" | conversation, protocol_message, keep_in_chat_message, edited_message, pin_in_chat_message, extended_text_message (without matched_text), secret_encrypted_message with SecretEncType::MessageEdit, poll_result_snapshot_message, poll_result_snapshot_message_v3 |
"media" | image_message, video_message, audio_message, document_message, sticker_message, sticker_pack_message, location_message, contact_message, extended_text_message (with matched_text), and all other media types |
"reaction" | reaction_message, enc_reaction_message |
"poll" | poll_creation_message, poll_creation_message_v2, poll_creation_message_v3, poll_creation_message_v5, poll_update_message, secret_encrypted_message with SecretEncType::PollEdit |
"event" | event_message, enc_event_response_message, secret_encrypted_message with SecretEncType::EventEdit |
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:
| Media type | Condition |
|---|---|
"image" | image_message present |
"video" | video_message with gif_playback not set or false |
"gif" | video_message with gif_playback = true |
"ptv" | ptv_message present |
"ptt" | audio_message with ptt = true |
"audio" | audio_message with ptt not set or false |
"document" | document_message present |
"sticker" | sticker_message present |
"sticker_pack" | sticker_pack_message present |
"location" | location_message with is_live not set or false |
"livelocation" | location_message with is_live = true, or live_location_message |
"vcard" | contact_message present |
"contact_array" | contacts_array_message present |
"url" | extended_text_message with non-empty matched_text, or group_invite_message |
Both stanza type and encrypted media type are resolved automatically by the library before encryption. Wrapper messages (
ephemeral_message, view_once_message, etc.) are unwrapped first to determine the underlying content type.Message types
Thewa::Message protobuf supports various message types. Set exactly one of these fields:
Text Messages
Simple text message without formatting
Text with formatting, links, quoted replies, or mentionsKey fields:
text- Message textcontextInfo- Quoted message, mentionspreviewType- Link preview behavior
Media Messages
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"
Video with optional caption. Same upload pattern as images.
Audio file or voice note:
ptt- Set totruefor voice notes (Push-To-Talk)mimetype- e.g.,"audio/ogg; codecs=opus"
Document/file with metadata:
file_name- Original filenamemimetype- File MIME typecaption- Optional description
Sticker image (WebP format)
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
GPS location with latitude, longitude, and optional name/address
Contact card with vCard data
Multiple contact cards
Real-time location sharing
Emoji reaction to another message
Poll with multiple options. Use
client.polls().create() for a higher-level API that handles message secret generation automatically. See Polls API.Calendar event message. The required
<meta event_type="creation"/> stanza node is injected automatically when sent through send_message or send_message_with_options.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 remote_jid field set for cross-platform compatibility:
remote_jid is set to the chat JID (group, DM, or newsletter) and is required by iOS clients to display quoted messages correctly. For newsletter chats, the participant field is automatically set to the newsletter JID instead of the sender.