> ## Documentation Index
> Fetch the complete documentation index at: https://personal-9eca1d6c-claude-nifty-bohr-567oke.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Sending Messages

> Learn how to send text messages, reactions, channel comments, quoted replies, album messages, sticker packs, and edit messages in whatsapp-rust

## Overview

This guide covers sending messages, including text, reactions, channel comments, quotes, album messages (grouped media), sticker packs, and message editing operations using the whatsapp-rust library.

## Text message shortcuts

For the common case of sending or replying with plain text, prefer the high-level helpers:

### `MessageContext` reply helpers (inside event handlers)

When you're already inside an `on_message` handler:

```rust theme={null}
.on_message(|ctx| async move {
    // Reply without quoting
    if let Err(e) = ctx.reply("pong").await {
        eprintln!("Failed to reply: {e}");
    }

    // Reply quoting the received message
    if let Err(e) = ctx.reply_quoting("pong").await {
        eprintln!("Failed to reply: {e}");
    }
})
```

### `Client::send_text` (from any `Client` reference)

```rust theme={null}
client.send_text(&chat_jid, "Hello from whatsapp-rust!").await?;
```

### `wa::Message::text` / `wa::Message::text_with_context`

Build a text message without hand-assembling the protobuf struct:

```rust theme={null}
use whatsapp_rust::prelude::*; // re-exports MessageBuilderExt

// Plain text — WA Web sends this as `conversation`
let msg = wa::Message::text("Hello!");

// Text with a quote/context — switches to `extendedTextMessage` internally
let msg_with_quote = wa::Message::text_with_context("Replying!", ctx.build_quote_context());
```

These are equivalent to building `wa::Message { conversation: Some(...) }` and `wa::Message { extended_text_message: buffa::MessageField::some(...) }` by hand, but less verbose.

***

## Sending text messages

### Simple text message

Use the `conversation` field for plain text messages, or `wa::Message::text("...")` for shorter syntax:

```rust theme={null}
use waproto::whatsapp as wa;
use wacore_binary::jid::Jid;

let to: Jid = "1234567890@s.whatsapp.net".parse()?;

let message = wa::Message {
    conversation: Some("Hello, WhatsApp!".to_string()),
    ..Default::default()
};

let result = client.send_message(&to, message).await?;
println!("Message sent with ID: {}", result.message_id);
```

`send_message` returns a `SendResult` containing the `message_id` and recipient `to` JID. Use `result.message_key()` to get a `wa::MessageKey` for follow-up operations like album child linking.

### Extended text message

For messages with formatting, links, or context (replies/quotes):

```rust theme={null}
use waproto::whatsapp::message::ExtendedTextMessage;

let message = wa::Message {
    extended_text_message: buffa::MessageField::some(ExtendedTextMessage {
        text: Some("Check out this link: https://example.com".to_string()),
        matched_text: Some("https://example.com".to_string()),
        title: Some("Example Site".to_string()),
        description: Some("A sample website".to_string()),
        ..Default::default()
    }),
    ..Default::default()
};

let result = client.send_message(to, message).await?;
```

## Quoted Replies

### Replying to a message

Use `build_quote_context` to create a basic reply:

```rust theme={null}
use wacore::proto_helpers::{build_quote_context, MessageExt};

// Assume you received the original message
let original_message: wa::Message = /* ... */;
let original_message_id = "3EB0ABC123";
let original_sender = "1234567890@s.whatsapp.net";

// Build quote context
let context = build_quote_context(
    original_message_id,
    original_sender,
    &original_message,
);

// Create reply message
let reply = wa::Message {
    extended_text_message: buffa::MessageField::some(ExtendedTextMessage {
        text: Some("This is my reply".to_string()),
        context_info: buffa::MessageField::some(context),
        ..Default::default()
    }),
    ..Default::default()
};

let result = client.send_message(to, reply).await?;
```

### Cross-platform quoted replies with remoteJid

For quoted replies that display correctly on all platforms (including iOS), use `build_quote_context_with_info`. It matches WhatsApp Web's behavior: it sets `participant` correctly (channel JID for newsletters, sender for everything else) and emits `remote_jid` only for cross-chat quotes — when the quoted message lives in a different chat than the one you're sending to.

The function takes both the quoted message's chat (`quoted_chat_jid`) and the chat you're sending into (`target_chat_jid`). For an in-place reply these are the same JID, and `remote_jid` is omitted; for a cross-chat quote (for example, quoting a status update into a DM) they differ, and `remote_jid` is set to the quoted chat:

```rust theme={null}
use wacore::proto_helpers::{build_quote_context_with_info, MessageExt};
use wacore_binary::jid::Jid;

let original_message: wa::Message = /* ... */;
let original_message_id = "3EB0ABC123";
let original_sender: Jid = "1234567890@s.whatsapp.net".parse()?;
let chat_jid: Jid = "120363040237990503@g.us".parse()?; // group or DM JID

// Same-chat reply: quoted_chat and target_chat are identical, so remote_jid is omitted.
let context = build_quote_context_with_info(
    original_message_id,
    &original_sender,
    &chat_jid, // quoted_chat_jid
    &chat_jid, // target_chat_jid
    &original_message,
);

let reply = wa::Message {
    extended_text_message: buffa::MessageField::some(ExtendedTextMessage {
        text: Some("This is my reply".to_string()),
        context_info: buffa::MessageField::some(context),
        ..Default::default()
    }),
    ..Default::default()
};

let result = client.send_message(chat_jid, reply).await?;
```

The `build_quote_context_with_info` function handles two important details:

* **`remote_jid`** is set only when `quoted_chat_jid` and `target_chat_jid` refer to different chats (cross-chat quote). For same-chat replies it is omitted, matching WhatsApp Web.
* **`participant`** is set to the sender JID for normal chats, or the newsletter JID for newsletter quotes.

<Note>
  Pass the same JID for both `quoted_chat_jid` and `target_chat_jid` when replying in-place. Use different JIDs only when forwarding a quote across chats (for example, quoting a status into a DM).
</Note>

### Setting context on media messages

You can add quote context to any message type using `set_context_info`:

```rust theme={null}
use waproto::whatsapp::message::ImageMessage;

let mut reply = wa::Message {
    image_message: buffa::MessageField::some(ImageMessage {
        url: Some("https://mmg.whatsapp.net/...".to_string()),
        mimetype: Some("image/jpeg".to_string()),
        caption: Some("Here's an image reply".to_string()),
        // ... other image fields
        ..Default::default()
    }),
    ..Default::default()
};

// Build and set context (use build_quote_context_with_info for cross-platform support)
let context = build_quote_context_with_info(
    original_message_id,
    &original_sender,
    &chat_jid, // quoted_chat_jid
    &chat_jid, // target_chat_jid
    &original_message,
);

reply.set_context_info(context);

let result = client.send_message(chat_jid, reply).await?;
```

## Reactions

### Sending a reaction

Use `client.send_reaction()` to react to a DM, group, or `status@broadcast` message. The helper builds the `ReactionMessage` payload, stamps `sender_timestamp_ms`, and routes the stanza through the standard send path.

```rust theme={null}
use waproto::whatsapp as wa;

let target_key = wa::MessageKey {
    remote_jid: Some(chat_jid.to_string()),
    from_me: Some(false),
    id: Some(message_id_to_react_to.to_string()),
    // Required for groups and status@broadcast; leave None for DMs.
    participant: Some(sender_jid.to_string()),
};

let result = client.send_reaction(&chat_jid, target_key, "👍").await?;
```

For groups and `status@broadcast`, `target_key.participant` must point to the original sender so the receipt can be attributed. In DMs, leave `participant` as `None`.

If you're already inside an event handler, [`MessageContext::react`](/api/bot#react) fills in `chat`, `target_key`, and `participant` from the incoming message automatically:

```rust theme={null}
use whatsapp_rust::bot::MessageContext;

for inbound in event.messages() {
    let ctx = MessageContext::from_inbound(inbound, client.clone());
    ctx.react("❤️").await?;
}
```

### Reactions in Community Announcement Groups

Community Announcement Groups (CAGs) — the default announcement subgroup of a community — require encrypted reactions. `send_reaction` handles this transparently: it detects CAG chats automatically and sends the reaction as an encrypted `enc_reaction_message` envelope instead of a plaintext stanza.

No change to your call is needed. The only requirement is that the target post's `messageSecret` was captured when the post was received. If it was not captured (for example, `msg_secret_policy` is disabled without a resolver, or the post arrived before the current session), the call returns an error rather than emitting a plaintext reaction the channel would reject.

```rust theme={null}
// Works for DMs, regular groups, and CAGs — no API difference.
client.send_reaction(&cag_jid, target_key, "🔥").await?;
```

See [Community management — CAG reactions](/guides/communities#community-announcement-group-cag-reactions) for details.

### Removing a reaction

Pass an empty `emoji` to revoke a previous reaction (matches WhatsApp Web's empty-text-as-revoke semantics):

```rust theme={null}
client.send_reaction(&chat_jid, target_key, "").await?;
```

<Note>
  Newsletter (channel) reactions use a different plaintext stanza format. Use [`client.newsletter().send_reaction()`](/api/newsletter#send_reaction) for newsletters instead.
</Note>

## Channel Comments

Channel comments are encrypted threaded replies under a Community Announcement Group (CAG) post. Use `client.comments()` to send them:

```rust theme={null}
use whatsapp_rust::features::Comments;

let parent_key = wa::MessageKey {
    remote_jid: Some(cag_jid.to_string()),
    from_me: Some(false),
    id: Some("3EB0POSTID".to_string()),
    // participant must be the post author.
    participant: Some(post_author_jid.to_string()),
};

// Text comment:
let result = client.comments()
    .send_text(&cag_jid, parent_key, "Great post!")
    .await?;
println!("Comment sent: {}", result.message_id);
```

For arbitrary message bodies:

```rust theme={null}
let body = wa::Message {
    extended_text_message: buffa::MessageField::some(wa::message::ExtendedTextMessage {
        text: Some("Great post!".to_string()),
        ..Default::default()
    }),
    ..Default::default()
};

let result = client.comments()
    .send_message(&cag_jid, parent_key, body)
    .await?;
```

The `parent_key.participant` field must identify the post author so receivers can derive the HKDF decryption key from the envelope. When `from_me` is `true` and `participant` is absent the library resolves the author to your own identity.

Incoming encrypted comments are decrypted transparently. The comment body is dispatched as part of an `Event::Messages` batch and the parent post key is available on `MessageInfo::comment_target`:

```rust theme={null}
Event::Messages(batch) => {
    for InboundMessage { message: msg, info } in batch.iter() {
        if let Some(parent_key) = &info.comment_target {
            println!("Comment on post: {:?}", parent_key.id);
            if let Some(text) = msg.text_content() {
                println!("Text: {}", text);
            }
        }
    }
}
```

See [Community management — Channel comments](/guides/communities#channel-comments) for full details.

## Editing messages

Use [`client.edit_message`](/api/send#edit_message) to replace the content of a message you previously sent. Pass the chat JID, the original message ID, and the new content as a plain `wa::Message` — the client builds the correct wire envelope, resolves the participant JID (LID or PN) for groups, and sends the edit with a fresh stanza ID so the server does not deduplicate it against the original.

```rust theme={null}
let new_content = wa::Message {
    conversation: Some("This is the edited text".to_string()),
    ..Default::default()
};

let edit_id = client.edit_message(
    chat_jid,
    &result.message_id,
    new_content,
).await?;
```

<Note>
  Message editing only works for text messages (`conversation` or `extended_text_message`) sent by you within the last 15 minutes.
</Note>

<Warning>
  Do not hand-roll the edit envelope by wrapping a `ProtocolMessage` inside `Message.edited_message` (a `FutureProofMessage`) and passing it to `send_message`. That shape is the history/storage form; on the wire WhatsApp expects a top-level `protocolMessage` with `type = MESSAGE_EDIT`. `client.edit_message` produces the correct shape — manual envelopes will be silently dropped by the server.
</Warning>

## Deleting messages (revoke)

### Delete your own message

```rust theme={null}
use whatsapp_rust::send::RevokeType;

client.revoke_message(
    chat_jid,
    message_id,
    RevokeType::Sender,
).await?;
```

See [Send API reference](/api/send#revoke_message) for full details.

### Admin delete (group only)

Group admins can delete messages from other participants:

```rust theme={null}
let original_sender: Jid = "1234567890@s.whatsapp.net".parse()?;

client.revoke_message(
    group_jid,
    message_id,
    RevokeType::Admin { original_sender },
).await?;
```

<Warning>
  Admin revoke only works in group chats. The `original_sender` must match the JID format (LID or phone number) of the message being deleted.
</Warning>

## Sending to newsletters

Newsletter messages are sent through the same `client.send_message()` method. The library automatically detects newsletter JIDs and sends messages as plaintext (no Signal encryption):

```rust theme={null}
use waproto::whatsapp as wa;
use wacore_binary::jid::Jid;

let newsletter_jid: Jid = "120363999999999999@newsletter".parse()?;

let message = wa::Message {
    conversation: Some("Hello subscribers!".to_string()),
    ..Default::default()
};

let result = client.send_message(newsletter_jid, message).await?;
```

The correct stanza `type` (text, media, reaction, poll), `mediatype` attributes, and `<meta>` nodes (for polls, events, etc.) are inferred automatically from the message content. See the [Newsletters guide](/guides/newsletters) for more details.

<Note>
  Newsletter reactions use a different protocol format. Use `client.newsletter().send_reaction()` for reactions instead of `send_message()`.
</Note>

## Album messages

Album messages let you send multiple images and/or videos as a grouped media album — the collapsed album bubble that WhatsApp displays when someone sends several photos at once.

An album consists of:

1. A **parent `AlbumMessage`** declaring the expected image and video counts
2. Multiple **child messages** (individual media messages) wrapped with `wrap_as_album_child` and linked to the parent

### Sending an album

```rust theme={null}
use waproto::whatsapp as wa;
use whatsapp_rust::proto_helpers::wrap_as_album_child;

let chat_jid: Jid = "1234567890@s.whatsapp.net".parse()?;

// Step 1: Send the parent album message
let album_parent = wa::Message {
    album_message: buffa::MessageField::some(wa::message::AlbumMessage {
        expected_image_count: Some(3),
        expected_video_count: Some(0),
        ..Default::default()
    }),
    ..Default::default()
};

let parent_result = client.send_message(&chat_jid, album_parent).await?;
let parent_key = parent_result.message_key();

// Step 2: Upload and send each child image
for image_data in [image1_bytes, image2_bytes, image3_bytes] {
    let upload = client.upload(image_data, MediaType::Image, Default::default()).await?;

    let image_msg = wa::Message {
        image_message: buffa::MessageField::some(wa::message::ImageMessage {
            url: Some(upload.url),
            direct_path: Some(upload.direct_path),
            media_key: Some(upload.media_key_vec()),
            file_sha256: Some(upload.file_sha256_vec()),
            file_enc_sha256: Some(upload.file_enc_sha256_vec()),
            file_length: Some(upload.file_length),
            media_key_timestamp: Some(upload.media_key_timestamp),
            mimetype: Some("image/jpeg".to_string()),
            ..Default::default()
        }),
        ..Default::default()
    };

    // Wrap as album child and send
    let wrapped = wrap_as_album_child(image_msg, parent_key.clone());
    client.send_message(&chat_jid, wrapped).await?;
}
```

### How it works

The `wrap_as_album_child` function (from `whatsapp_rust::proto_helpers`) takes a media `wa::Message` and a parent `wa::MessageKey`, then:

1. Wraps the inner message in an `associated_child_message` (`FutureProofMessage` envelope)
2. Attaches a `MessageAssociation` with type `MediaAlbum` pointing to the parent
3. Lifts any existing `message_context_info` from the inner message to the outer wrapper

<Note>
  The parent `AlbumMessage` declares the total expected counts so WhatsApp clients know how many media items to group together. Each child is sent as a separate message linked back to the parent via `MessageAssociation`.
</Note>

### Mixed albums (images and videos)

You can mix images and videos in the same album:

```rust theme={null}
let album_parent = wa::Message {
    album_message: buffa::MessageField::some(wa::message::AlbumMessage {
        expected_image_count: Some(2),
        expected_video_count: Some(1),
        ..Default::default()
    }),
    ..Default::default()
};

let parent_result = client.send_message(&chat_jid, album_parent).await?;
let parent_key = parent_result.message_key();

// Send image children
let image_wrapped = wrap_as_album_child(image_msg, parent_key.clone());
client.send_message(&chat_jid, image_wrapped).await?;

// Send video children
let video_wrapped = wrap_as_album_child(video_msg, parent_key.clone());
client.send_message(&chat_jid, video_wrapped).await?;
```

## Sticker packs

Sticker packs let you send a collection of stickers as a single message — the inline sticker pack bubble that WhatsApp displays with a tray icon, pack name, and publisher info.

A sticker pack requires:

1. **Sticker images** — 512x512 WebP files
2. **Cover image** — WebP file used as the tray icon
3. **Thumbnail** — JPEG uploaded separately with the same `media_key` as the ZIP
4. **Metadata** — pack ID, name, and publisher

### Sending a sticker pack

```rust theme={null}
use wacore::sticker_pack::{
    StickerInput, StickerPackMetadata,
    create_sticker_pack_zip, build_sticker_pack_message,
};
use wacore::download::MediaType;
use whatsapp_rust::upload::UploadOptions;

let chat_jid: Jid = "1234567890@s.whatsapp.net".parse()?;

// Step 1: Create sticker inputs with optional emojis
let stickers = vec![
    StickerInput::new(&webp_bytes_1).with_emojis(vec!["😀".into()]),
    StickerInput::new(&webp_bytes_2).with_emojis(vec!["🎉".into()]),
    StickerInput::new(&webp_bytes_3),
];

// Step 2: Bundle stickers into a ZIP with the cover image
let zip_result = create_sticker_pack_zip("my-pack-id", &stickers, &cover_webp)?;

// Step 3: Upload the ZIP
let zip_upload = client.upload(
    zip_result.zip_bytes.clone(),
    MediaType::StickerPack,
    UploadOptions::default(),
).await?;

// Step 4: Upload the thumbnail JPEG with the same media_key
let thumb_upload = client.upload(
    thumbnail_jpeg,
    MediaType::StickerPackThumbnail,
    UploadOptions::new().with_media_key(zip_upload.media_key),
).await?;

// Step 5: Build and send the message
let metadata = StickerPackMetadata::new(
    "my-pack-id".into(),
    "My Sticker Pack".into(),
    "My Name".into(),
);

let msg = build_sticker_pack_message(
    &zip_result,
    &zip_upload.into(),
    &thumb_upload.into(),
    metadata,
);

client.send_message(chat_jid, msg).await?;
```

### How it works

The sticker pack flow uses two helper functions from `wacore::sticker_pack`:

* **`create_sticker_pack_zip`** — bundles stickers and a cover image into a ZIP file. Filenames use `base64url(sha256).webp`, and identical stickers are deduplicated. Returns a `StickerPackZipResult` containing the ZIP bytes and proto metadata.
* **`build_sticker_pack_message`** — constructs a `wa::Message` with a `StickerPackMessage` from the ZIP result and upload responses.

The thumbnail must be uploaded with `MediaType::StickerPackThumbnail` and the same `media_key` as the ZIP upload. The `UploadResponse` implements `Into<MediaUploadInfo>` for convenience.

### Sticker format requirements

| Field          | Requirement                                                   |
| -------------- | ------------------------------------------------------------- |
| Sticker images | 512x512 WebP                                                  |
| Cover image    | WebP, stored in ZIP as `{pack_id}.webp`                       |
| Thumbnail      | JPEG, uploaded separately with same `media_key`               |
| Pack ID        | Non-empty, max 128 bytes, no path separators or control chars |
| Sticker count  | 1 to 60 per pack                                              |

### Sticker metadata

Each sticker supports optional metadata:

```rust theme={null}
StickerInput::new(&webp_bytes)
    .with_emojis(vec!["😀".into(), "🎉".into()])
    .with_accessibility_label("happy face".into())
```

Pack-level metadata supports optional description and caption:

```rust theme={null}
let metadata = StickerPackMetadata::new(
    "pack-id".into(),
    "Pack Name".into(),
    "Publisher".into(),
)
.with_description("A fun sticker pack".into())
.with_caption("Check out my stickers!".into());
```

<Note>
  Animated stickers are automatically detected from the WebP data. The `is_animated` field on each sticker proto entry is set based on whether the WebP file contains animation frames. You can also use [`whatsapp_rust::webp::is_animated()`](/api/wacore#is_animated) directly to check WebP files before processing.
</Note>

## Forwarding messages

Use `forward_message` to forward any received message to a chat. It produces the same on-wire result as tapping **Forward** in the official clients:

```rust theme={null}
// `received` is a `wa::Message` from an incoming event.
client.forward_message(destination_jid, &received).await?;
```

The helper takes care of the WhatsApp forwarding rules so you don't have to rebuild the message by hand:

* Sets `context_info.is_forwarded = true` so recipients see the **Forwarded** label.
* Bumps the forwarding score. Once the score reaches 5, it jumps to the `127` sentinel that clients render as **Forwarded many times**.
* Strips the reply/quote chain and mentions from the source.
* Drops the source `message_secret` so the send path mints a fresh one.
* Unwraps ephemeral and view-once wrappers before sending the inner content.
* Promotes a bare `conversation` to `extended_text_message` so the forward marker can attach.

Media is relayed from the same CDN blob, so forwarding an image, video, document, audio, or sticker is instant regardless of file size — nothing is downloaded or re-uploaded.

### Manually forwarding media

If you need to customize fields (for example, change a caption) before forwarding, you can still build the `wa::Message` yourself and pass it to `send_message`. Reusing the original CDN fields keeps the send instant:

```rust theme={null}
use waproto::whatsapp as wa;

// Clone the image message, only change the caption
if let Some(img) = received_message.image_message.as_option() {
    let forwarded = wa::Message {
        image_message: buffa::MessageField::some(wa::message::ImageMessage {
            caption: Some("Forwarded!".to_string()),
            ..img.clone()
        }),
        ..Default::default()
    };

    client.send_message(destination_jid, forwarded).await?;
}
```

Note that this path does not apply the **Forwarded** marker or the forwarding-score bump. Use `forward_message` whenever you want the recipient to see the standard forward indicator. See [Media handling — CDN reuse](/guides/media-handling#forwarding-media-via-cdn-reuse) for details on supported media types and when to fall back to download + re-upload.

## Ephemeral (disappearing) messages

WhatsApp supports disappearing messages that automatically delete after a set duration. When a chat has disappearing messages enabled, you should set the ephemeral expiration on outgoing messages so recipients see the correct countdown timer.

### Sending a disappearing message

Use `send_message_with_options` with `ephemeral_expiration` set to the chat's timer value:

```rust theme={null}
use whatsapp_rust::send::SendOptions;
use waproto::whatsapp as wa;

let message = wa::Message {
    conversation: Some("This message will disappear!".to_string()),
    ..Default::default()
};

let options = SendOptions {
    ephemeral_expiration: Some(604800), // 7 days, matching the chat's timer
    ..Default::default()
};

let result = client.send_message_with_options(chat_jid, message, options).await?;
```

This sets `contextInfo.expiration` on the protobuf message, which tells WhatsApp clients to display the disappearing countdown.

### Common timer values

| Duration | Value (seconds) |
| -------- | --------------- |
| 24 hours | `86400`         |
| 7 days   | `604800`        |
| 90 days  | `7776000`       |
| Disabled | `0`             |

### Getting the chat's ephemeral timer

For groups, read the timer from group metadata:

```rust theme={null}
let metadata = client.groups().get_metadata(&group_jid).await?;
let expiration = metadata.ephemeral_expiration; // 0 if disabled
```

For incoming messages, read it from `MessageInfo`:

```rust theme={null}
Event::Messages(batch) => {
    for InboundMessage { info, .. } in batch.iter() {
        if let Some(expiration) = info.ephemeral_expiration {
            println!("Chat has {}s disappearing timer", expiration);
        }
    }
}
```

### Configuring disappearing messages

**Per-group:** Use `set_ephemeral` to enable or disable disappearing messages on a group:

```rust theme={null}
// Enable 7-day disappearing messages
client.groups().set_ephemeral(&group_jid, 604800).await?;

// Disable disappearing messages
client.groups().set_ephemeral(&group_jid, 0).await?;
```

**Account-level default:** Use `set_default_disappearing_mode` to set the default for all new 1-on-1 chats:

```rust theme={null}
// Enable 24-hour default for new chats
client.set_default_disappearing_mode(86400).await?;

// Disable default disappearing messages
client.set_default_disappearing_mode(0).await?;
```

<Note>
  The account-level default only applies to **new** chats. Existing chats keep their current setting. To change a specific group's timer, use [`set_ephemeral`](/api/groups#set_ephemeral).
</Note>

### Listening for timer changes

When a contact changes their default disappearing messages setting, you receive a `DisappearingModeChanged` event:

```rust theme={null}
Event::DisappearingModeChanged(change) => {
    println!("Contact {} set disappearing to {}s", change.from, change.duration);
}
```

When a group's ephemeral setting changes, you receive a `GroupUpdate` event with a `GroupNotificationAction::Ephemeral` action containing the new `expiration` value.

See [Events reference](/concepts/events#disappearingmodechanged) for details.

### Creating groups with disappearing messages

You can enable disappearing messages at group creation time:

```rust theme={null}
use whatsapp_rust::features::groups::{GroupCreateOptions, GroupParticipantOptions};

let options = GroupCreateOptions::builder()
    .subject("Ephemeral Group")
    .participants(vec![
        GroupParticipantOptions::new(participant_jid),
    ])
    .ephemeral_expiration(604800) // 7-day timer
    .build();

let result = client.groups().create_group(options).await?;
```

See [Send API reference](/api/send#sendoptions) for the full `SendOptions` type.

## Send Options

### Specifying a custom message ID

You can override the auto-generated message ID by setting `message_id` on `SendOptions`. This is useful for resending a failed message with the same ID or ensuring idempotency:

```rust theme={null}
use whatsapp_rust::send::SendOptions;

let options = SendOptions {
    message_id: Some("3EB0ABC123".to_string()),
    ..Default::default()
};

let result = client.send_message_with_options(
    to,
    message,
    options,
).await?;

// result.message_id will be "3EB0ABC123"
```

### Overriding the stanza type

The library infers the `<message type="...">` attribute from the protobuf content of every send. If you're sending a message variant the classifier can't recognize, set `stanza_type_override` to force a specific wire type:

```rust theme={null}
use whatsapp_rust::{StanzaType, send::SendOptions};

let options = SendOptions {
    stanza_type_override: Some(StanzaType::Text),
    ..Default::default()
};

let result = client.send_message_with_options(
    to,
    message,
    options,
).await?;
```

Leave this as `None` for every supported message type — the classifier already picks the right value, and the override is not preserved across the retry path. See [Stanza types](/api/send#stanza-types) for the available variants.

### Adding extra stanza nodes

For advanced use cases, you can include custom XML nodes:

```rust theme={null}
use whatsapp_rust::send::SendOptions;
use wacore_binary::builder::NodeBuilder;

let options = SendOptions {
    extra_stanza_nodes: vec![
        NodeBuilder::new("custom")
            .attr("key", "value")
            .build(),
    ],
    ..Default::default()
};

let result = client.send_message_with_options(
    to,
    message,
    options,
).await?;
```

See [Send API reference](/api/send#sendoptions) for full details.

## Message preparation helpers

### Preparing messages for quoting

The `prepare_for_quote` method strips nested context info:

```rust theme={null}
use wacore::proto_helpers::MessageExt;

let quoted_message = original_message.prepare_for_quote();

let context = wa::ContextInfo {
    stanza_id: Some(message_id.clone()),
    participant: Some(sender_jid.to_string()),
    quoted_message: buffa::MessageField::from_box(quoted_message),
    ..Default::default()
};
```

This ensures:

* Nested mentions are stripped
* Quote chains are broken (except for bot messages)
* Content fields (text, caption, media) are preserved

### Preparing messages for forwarding

`prepare_for_forward` is the lower-level helper that powers [`forward_message`](#forwarding-messages). It returns a forward-ready `wa::Message` (forward marker set, score bumped, quote chain stripped, source `message_secret` dropped) without sending anything. Reach for it when you need to attach extra fields — for example, a custom caption or stanza nodes — before calling `send_message_with_options`:

```rust theme={null}
use wacore::proto_helpers::MessageExt;

let mut forward = *received_message.prepare_for_forward();
// Tweak the prepared message here if needed, for example overriding a caption.

client.send_message(destination_jid, forward).await?;
```

For the common case, prefer `client.forward_message(to, &message)` — it unwraps wrapper bodies (ephemeral, view-once) and sends in one call.

See [WAProto API reference](/api/waproto) for message type details.

## Error Handling

All send-path methods return `Result<T, SendError>`. Callers using `?` into an `anyhow` context compile unchanged — `SendError` implements `Into<anyhow::Error>`.

```rust theme={null}
use anyhow::Result;
use whatsapp_rust::SendError;

async fn send_and_return_id(
    client: &Client,
    jid: Jid,
    message: wa::Message,
) -> Result<String> {
    let result = client.send_message(jid.clone(), message).await?;
    Ok(result.message_id)
}
```

For typed error matching:

```rust theme={null}
use whatsapp_rust::SendError;

match client.send_message(jid.clone(), message).await {
    Ok(result) => println!("Sent: {}", result.message_id),
    Err(SendError::NotLoggedIn) => {
        eprintln!("Not authenticated — reconnect first");
    }
    Err(SendError::Iq(e)) => {
        eprintln!("Server rejected the send: {}", e);
    }
    Err(SendError::InvalidRequest(msg)) => {
        eprintln!("Malformed request: {}", msg);
    }
    Err(e) => eprintln!("Send error: {}", e),
}
```

See the [Error Types reference](/api/errors) for the complete `SendError` definition and all domain error types.

## Best Practices

<Steps>
  ### Use the right message type

  * **Simple text**: Use `conversation`
  * **Links/formatting**: Use `extended_text_message`
  * **Replies**: Use `extended_text_message` with `context_info`
  * **Media**: Use specific media message types with optional captions
  * **Albums**: Use `album_message` parent + `wrap_as_album_child` for grouped media
  * **Sticker packs**: Use `create_sticker_pack_zip` + `build_sticker_pack_message` with two uploads (ZIP + thumbnail)
  * **Disappearing chats**: Use `send_message_with_options` with `ephemeral_expiration` matching the chat's timer
  * **CAG reactions**: Use `send_reaction` — encryption is applied automatically for CAG chats
  * **Channel comments**: Use `client.comments().send_text()` or `send_message()`

  ### Handle message IDs

  ```rust theme={null}
  // Store the result for later operations
  let result = client.send_message(&chat_jid, message).await?;

  // Use the message ID for reactions, edits, or deletes
  client.revoke_message(&chat_jid, &result.message_id, RevokeType::Sender).await?;

  // Use message_key() for album child linking or pinning
  let key = result.message_key();
  ```

  ### Quote context best practices

  * Always use `prepare_for_quote()` to avoid nested quote chains
  * Use `build_quote_context_with_info` instead of `build_quote_context` for cross-platform compatibility — it correctly resolves the `participant` field for newsletters and emits `remote_jid` only for cross-chat quotes (matching WhatsApp Web)
  * Preserve original media fields when quoting media messages
</Steps>

## Next Steps

* [Receiving Messages](/guides/receiving-messages) - Handle incoming messages and events
* [Media Handling](/guides/media-handling) - Upload and download media
* [Polls](/guides/polls) - Create polls and process votes
* [Group Management](/guides/group-management) - Work with group chats
* [Community management](/guides/communities) - CAG reactions and channel comments
