> ## Documentation Index
> Fetch the complete documentation index at: https://whatsapp-rust.jlucaso.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat actions

> Archive, pin, mute, delete chats, star messages, and mark chats as read

The `ChatActions` feature provides methods for managing chat organization through archiving, pinning, muting, starring messages, marking chats as read, deleting chats, and deleting individual messages. These operations sync across all your devices via WhatsApp's app state sync mechanism.

## Access

Access chat action operations through the client:

```rust theme={null}
let chat_actions = client.chat_actions();
```

## Archive

### archive\_chat

Archive a chat to hide it from the main chat list.

```rust theme={null}
pub async fn archive_chat(
    &self,
    jid: &Jid,
    message_range: Option<SyncActionMessageRange>,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to archive
* `message_range` - Optional message range for multi-device conflict resolution. Pass `None` in most cases

**Example:**

```rust theme={null}
let jid: Jid = "15551234567@s.whatsapp.net".parse()?;
client.chat_actions().archive_chat(&jid, None).await?;
```

### unarchive\_chat

Unarchive a chat to show it in the main chat list.

```rust theme={null}
pub async fn unarchive_chat(
    &self,
    jid: &Jid,
    message_range: Option<SyncActionMessageRange>,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to unarchive
* `message_range` - Optional message range for multi-device conflict resolution. Pass `None` in most cases

**Example:**

```rust theme={null}
client.chat_actions().unarchive_chat(&jid, None).await?;
```

## Pin

### pin\_chat

Pin a chat to keep it at the top of the chat list.

```rust theme={null}
pub async fn pin_chat(&self, jid: &Jid) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to pin

**Example:**

```rust theme={null}
let jid: Jid = "15551234567@s.whatsapp.net".parse()?;
client.chat_actions().pin_chat(&jid).await?;
```

<Note>
  WhatsApp limits the number of pinned chats. Attempting to pin too many chats may fail.
</Note>

### unpin\_chat

Unpin a chat.

```rust theme={null}
pub async fn unpin_chat(&self, jid: &Jid) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to unpin

**Example:**

```rust theme={null}
client.chat_actions().unpin_chat(&jid).await?;
```

## Mute

### mute\_chat

Mute a chat indefinitely.

```rust theme={null}
pub async fn mute_chat(&self, jid: &Jid) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to mute

**Example:**

```rust theme={null}
let jid: Jid = "15551234567@s.whatsapp.net".parse()?;
client.chat_actions().mute_chat(&jid).await?;
```

### mute\_chat\_until

Mute a chat until a specific time.

```rust theme={null}
pub async fn mute_chat_until(
    &self,
    jid: &Jid,
    mute_end_timestamp_ms: i64
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to mute
* `mute_end_timestamp_ms` - Unix timestamp in milliseconds when mute expires (must be in the future)

**Example:**

```rust theme={null}
use chrono::{Utc, Duration};

let jid: Jid = "15551234567@s.whatsapp.net".parse()?;

// Mute for 8 hours
let mute_until = Utc::now() + Duration::hours(8);
client.chat_actions()
    .mute_chat_until(&jid, mute_until.timestamp_millis())
    .await?;

// Mute for 1 week
let mute_until = Utc::now() + Duration::weeks(1);
client.chat_actions()
    .mute_chat_until(&jid, mute_until.timestamp_millis())
    .await?;
```

### unmute\_chat

Unmute a chat.

```rust theme={null}
pub async fn unmute_chat(&self, jid: &Jid) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to unmute

**Example:**

```rust theme={null}
client.chat_actions().unmute_chat(&jid).await?;
```

## Star messages

### star\_message

Star a message to mark it as important.

```rust theme={null}
pub async fn star_message(
    &self,
    chat_jid: &Jid,
    participant_jid: Option<&Jid>,
    message_id: &str,
    from_me: bool
) -> Result<(), AppStateError>
```

**Parameters:**

* `chat_jid` - The chat containing the message
* `participant_jid` - For group messages from others, pass `Some(&sender_jid)`. For 1-on-1 chats or your own messages, pass `None`
* `message_id` - The message ID to star
* `from_me` - Whether the message was sent by you

**Example:**

```rust theme={null}
// Star your own message in a 1-on-1 chat
client.chat_actions()
    .star_message(&chat_jid, None, "MESSAGE_ID", true)
    .await?;

// Star someone else's message in a 1-on-1 chat
client.chat_actions()
    .star_message(&chat_jid, None, "MESSAGE_ID", false)
    .await?;

// Star someone else's message in a group
let sender_jid: Jid = "15559876543@s.whatsapp.net".parse()?;
client.chat_actions()
    .star_message(&group_jid, Some(&sender_jid), "MESSAGE_ID", false)
    .await?;
```

<Warning>
  For group messages not sent by you, `participant_jid` is required. The method will return an error if it's not provided.
</Warning>

### unstar\_message

Remove the star from a message.

```rust theme={null}
pub async fn unstar_message(
    &self,
    chat_jid: &Jid,
    participant_jid: Option<&Jid>,
    message_id: &str,
    from_me: bool
) -> Result<(), AppStateError>
```

**Parameters:**
Same as `star_message`.

**Example:**

```rust theme={null}
client.chat_actions()
    .unstar_message(&chat_jid, None, "MESSAGE_ID", true)
    .await?;
```

## Mark chat as read

### mark\_chat\_as\_read

Mark a chat as read or unread. This is distinct from `mark_as_read` (IQ receipts) — it syncs the read/unread state across all linked devices.

```rust theme={null}
pub async fn mark_chat_as_read(
    &self,
    jid: &Jid,
    read: bool,
    message_range: Option<SyncActionMessageRange>,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to mark
* `read` - `true` to mark as read, `false` to mark as unread
* `message_range` - Optional message range for multi-device conflict resolution. Pass `None` in most cases

**Example:**

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

// Mark chat as read
client.chat_actions().mark_chat_as_read(&jid, true, None).await?;

// Mark chat as unread
client.chat_actions().mark_chat_as_read(&jid, false, None).await?;
```

<Note>
  This syncs the read/unread badge across linked devices via app state sync (`regular_low` collection). To send read receipts to the sender, use `client.mark_as_read()` instead.
</Note>

## Delete chat

### delete\_chat

Delete a chat from the chat list across all linked devices.

```rust theme={null}
pub async fn delete_chat(
    &self,
    jid: &Jid,
    delete_media: bool,
    message_range: Option<SyncActionMessageRange>,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to delete
* `delete_media` - Whether to also delete downloaded media files
* `message_range` - Optional message range for multi-device conflict resolution. Pass `None` in most cases

**Example:**

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

// Delete chat and its media
client.chat_actions().delete_chat(&jid, true, None).await?;

// Delete chat but keep media files
client.chat_actions().delete_chat(&jid, false, None).await?;
```

<Warning>
  This operation is not reversible. The chat and optionally its media will be removed from all linked devices.
</Warning>

## Clear chat

### clear\_chat

Clear a chat's messages while **keeping the chat** itself (WhatsApp Web's "Clear chat"). Unlike [`delete_chat`](#delete_chat), the chat stays in the list — only its messages are removed. Syncs across all linked devices.

```rust theme={null}
pub async fn clear_chat(
    &self,
    jid: &Jid,
    delete_starred: bool,
    delete_media: bool,
    message_range: Option<SyncActionMessageRange>,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The chat JID to clear
* `delete_starred` - Also remove starred messages
* `delete_media` - Also remove downloaded media files
* `message_range` - Optional message range for multi-device conflict resolution. Pass `None` in most cases

Both flags are encoded in the mutation index (not the proto body), matching WhatsApp Web's `clearChat` action.

**Example:**

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

// Clear all messages, including starred ones and downloaded media
client.chat_actions().clear_chat(&jid, true, true, None).await?;

// Clear messages but keep starred messages and media
client.chat_actions().clear_chat(&jid, false, false, None).await?;
```

A clear performed on another linked device arrives as an [`Event::ClearChatUpdate`](/concepts/events#clearchatupdate).

## Save and remove contacts

### save\_contact

Save or rename a contact, syncing the name to your other linked devices (WhatsApp Web's contact-sync action).

```rust theme={null}
pub async fn save_contact(
    &self,
    jid: &Jid,
    full_name: Option<String>,
    first_name: Option<String>,
    save_on_primary_addressbook: bool,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The contact's JID. **Must be a bare phone-number JID** — LIDs and device-specific JIDs are rejected (LID contacts use a separate path on WhatsApp Web).
* `full_name` - Full display name, or `None`
* `first_name` - Short name, or `None` (omitted when absent; WhatsApp Web derives no default)
* `save_on_primary_addressbook` - Whether to save the name to the phone's address book

**Example:**

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

client.chat_actions()
    .save_contact(&jid, Some("Jane Doe".into()), Some("Jane".into()), true)
    .await?;
```

### remove\_contact

Delete a saved contact, syncing the removal to your other linked devices.

```rust theme={null}
pub async fn remove_contact(&self, jid: &Jid) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The contact's JID. **Must be a bare phone-number JID**, the same rule as `save_contact`.

**Example:**

```rust theme={null}
client.chat_actions().remove_contact(&jid).await?;
```

<Note>
  Unlike `save_contact`, which writes a `Set`, `remove_contact` sends a syncd **`Remove`** for the same `["contact", jid]` index. WhatsApp Web picks the operation from an `isDelete` flag when building this mutation. Its receiving side then branches on the operation: a `Set` with an empty `ContactAction` gets applied as a rename to the empty string, not a deletion, so only a genuine `Remove` deletes the contact. WhatsApp Web still builds the action value before choosing the operation, so `remove_contact` sends an all-default `ContactAction` alongside the `Remove` operation.
</Note>

## Status mute

### set\_user\_status\_mute

Mute or unmute a contact, group, or channel's **status updates** across linked devices (WhatsApp Web's `userStatusMute`). This is distinct from [`mute_chat`](#mute_chat), which silences a chat's message notifications.

```rust theme={null}
pub async fn set_user_status_mute(
    &self,
    jid: &Jid,
    muted: bool,
) -> Result<(), AppStateError>
```

**Parameters:**

* `jid` - The entity whose status updates to mute/unmute
* `muted` - `true` hides their status updates, `false` unmutes

**Example:**

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

client.chat_actions().set_user_status_mute(&jid, true).await?;  // mute status
client.chat_actions().set_user_status_mute(&jid, false).await?; // unmute
```

A status-mute change on another linked device arrives as an [`Event::UserStatusMuteUpdate`](/concepts/events#userstatusmuteupdate).

## Delete message for me

### delete\_message\_for\_me

Delete a specific message locally (not for the other party). This is different from `revoke_message` which deletes for everyone.

```rust theme={null}
pub async fn delete_message_for_me(
    &self,
    chat_jid: &Jid,
    participant_jid: Option<&Jid>,
    message_id: &str,
    from_me: bool,
    delete_media: bool,
    message_timestamp: Option<i64>,
) -> Result<(), AppStateError>
```

**Parameters:**

* `chat_jid` - The chat containing the message
* `participant_jid` - For group messages from others, pass `Some(&sender_jid)`. For 1-on-1 chats or your own messages, pass `None`
* `message_id` - The ID of the message to delete
* `from_me` - Whether the message was sent by you
* `delete_media` - Whether to also delete the associated media file
* `message_timestamp` - Optional timestamp of the message

**Example:**

```rust theme={null}
// Delete your own message in a 1-on-1 chat
client.chat_actions()
    .delete_message_for_me(&chat_jid, None, "MESSAGE_ID", true, true, None)
    .await?;

// Delete someone else's message in a group
let sender_jid: Jid = "15559876543@s.whatsapp.net".parse()?;
client.chat_actions()
    .delete_message_for_me(&group_jid, Some(&sender_jid), "MESSAGE_ID", false, true, None)
    .await?;
```

<Warning>
  For group messages not sent by you, `participant_jid` is required. The method will return an error if it's not provided.
</Warning>

<Note>
  This only removes the message from your own devices. The other party can still see the message. To delete for everyone, use `client.revoke_message()` instead.
</Note>

## Helper functions

### message\_range

Construct a `SyncActionMessageRange` for multi-device conflict resolution. In most cases you can pass `None` instead — only WhatsApp Web with a full message database populates this.

```rust theme={null}
pub fn message_range(
    last_message_timestamp: i64,
    last_system_message_timestamp: Option<i64>,
    messages: Vec<(wa::MessageKey, i64)>,
) -> SyncActionMessageRange
```

### message\_key

Construct a `MessageKey` for use with `message_range`.

```rust theme={null}
pub fn message_key(
    id: impl Into<String>,
    remote_jid: &Jid,
    from_me: bool,
    participant: Option<&Jid>,
) -> wa::MessageKey
```

## Generic app state action

### send\_app\_state\_action

Send any syncd (app state) `Set` action, driven by a generated schema from the `whatsapp_rust::schemas` registry. Use this as the escape hatch when there isn't a dedicated helper yet (for example, `clear_chat`, `favorites`). Most typed methods on `ChatActions`, `Labels`, `QuickReplies`, and `AppStateSettings` are thin wrappers over this same call. The one exception is [`ChatActions::remove_contact`](#save-and-remove-contacts), which wraps [`remove_app_state_action`](#remove_app_state_action) below instead, since contact removal needs a `Remove` rather than a `Set`.

```rust theme={null}
pub async fn send_app_state_action(
    &self,
    schema: &Schema,
    index_args: &[&str],
    value: &wa::SyncActionValue,
) -> Result<(), AppStateError>
```

**Parameters:**

* `schema` — A `&Schema` constant from `whatsapp_rust::schemas` (re-exported from `wacore::appstate::schemas`). The schema decides the collection, action version, and index shape.
* `index_args` — The non-literal index parts in the order declared by `schema.index_parts`. Literal slots (the action name prefix) are filled automatically.
* `value` — A `wa::SyncActionValue` with the matching action sub-field set and a `timestamp` in epoch milliseconds.

**When to use it:**

* The action you need does not have a typed helper on `ChatActions`, `Labels`, `QuickReplies`, or `AppStateSettings`.
* You need to interoperate with a schema added to the registry without waiting for a new helper to land.

Prefer the typed wrappers (`pin_chat`, `mute_chat`, `archive_chat`, label methods, etc.) whenever they exist — they handle the timestamp, conflict-resolution fields, and index args for you.

**Example:**

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

let value = wa::SyncActionValue {
    clear_chat_action: Some(Default::default()),
    timestamp: Some(1_700_000_000_000),
    ..Default::default()
};

// CLEAR_CHAT's non-literal index parts are [chatJid, deleteStarred, deleteMedia].
client
    .send_app_state_action(
        &schemas::CLEAR_CHAT,
        &["15551234567@s.whatsapp.net", "0", "0"],
        &value,
    )
    .await?;
```

<Note>
  Index arguments are positional and must match `schema.index_parts` length and order, excluding `IndexPart::Literal` slots. Mismatched arity returns an error before any patch is sent.
</Note>

### remove\_app\_state\_action

Send an app-state action as a syncd `Remove` rather than a `Set`. Sibling of `send_app_state_action` above, for the small set of actions that model deletion as their own operation instead of a `deleted` flag inside a `Set` value.

```rust theme={null}
pub async fn remove_app_state_action(
    &self,
    schema: &Schema,
    index_args: &[&str],
    value: &wa::SyncActionValue,
) -> Result<(), AppStateError>
```

**Parameters:** same as [`send_app_state_action`](#send_app_state_action) — `schema`, `index_args`, and `value` are used identically. Only the wire operation differs.

<Warning>
  Most delete-like actions — labels, quick replies — use a `deleted` flag inside a `Set` value, not a syncd `Remove`. Check the action's WhatsApp Web builder before reaching for this method. Sending a `Remove` for one of those actions drops the record from the collection locally, but the linked devices never see the deletion, since they only watch for the `deleted` flag on a `Set`. [`ChatActions::remove_contact`](#save-and-remove-contacts) is the one action in this codebase that genuinely needs `Remove`, and it is already wrapped for you. Reach for `remove_app_state_action` directly only when adding support for a new action of this shape.
</Warning>

**Example:**

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

let value = wa::SyncActionValue {
    contact_action: Some(Default::default()),
    timestamp: Some(1_700_000_000_000),
    ..Default::default()
};

client
    .remove_app_state_action(&schemas::CONTACT, &["15551234567@s.whatsapp.net"], &value)
    .await?;
```

## App state sync

All chat actions are synced across devices using WhatsApp's app state synchronization:

| Action                | Collection             |
| --------------------- | ---------------------- |
| Archive               | `regular_low`          |
| Pin                   | `regular_low`          |
| Mark chat as read     | `regular_low`          |
| Mute                  | `regular_high`         |
| Star                  | `regular_high`         |
| Delete chat           | `regular_high`         |
| Delete message for me | `regular_high`         |
| Clear chat            | `regular_high`         |
| Status mute           | `regular_high`         |
| Save contact          | `critical_unblock_low` |
| Remove contact        | `critical_unblock_low` |

<Note>
  App state sync requires encryption keys to be available. These are typically obtained during initial sync after authentication. Actions may fail if called immediately after pairing before sync completes.
</Note>

<Note>
  As of PR [#1158](https://github.com/oxidezap/whatsapp-rust/pull/1158), losing an app-state version race no longer silently drops your mutation. A version race happens when another linked device already advanced the same collection. The server now answers with the winning patches instead of an error. The client applies them, rebuilds your mutation on the new base, and resends. It retries up to 5 times, matching WhatsApp Web's own cap. Previously, the call returned `Ok(())` even though the conflict response was ignored and the mutation never took effect. This conflict-resolution path only returns `Err` after those 5 attempts are exhausted, or if the server rejects the patch outright (not a version conflict). The other failure causes in [Error handling](#error-handling) below — invalid input, missing sync keys, network errors — are unrelated to this change and still apply as before.
</Note>

## Events

Chat action changes are emitted as events that you can handle:

```rust theme={null}
use wacore::types::events::Event;

.on_event(|event, _client| async move {
    match &*event {
        Event::MuteUpdate(update) => {
            println!("Chat {} muted: {:?}", update.jid, update.action.muted);
        }
        Event::PinUpdate(update) => {
            println!("Chat {} pinned: {:?}", update.jid, update.action.pinned);
        }
        Event::ArchiveUpdate(update) => {
            println!("Chat {} archived: {:?}", update.jid, update.action.archived);
        }
        Event::StarUpdate(update) => {
            println!("Message {} starred: {:?}", update.message_id, update.action.starred);
        }
        Event::MarkChatAsReadUpdate(update) => {
            println!("Chat {} marked as read: {:?}", update.jid, update.action.read);
        }
        Event::DeleteChatUpdate(update) => {
            println!("Chat {} deleted (media: {})", update.jid, update.delete_media);
        }
        Event::ClearChatUpdate(update) => {
            println!("Chat {} cleared (media: {})", update.jid, update.delete_media);
        }
        Event::UserStatusMuteUpdate(update) => {
            println!("Status of {} muted: {}", update.jid, update.muted);
        }
        Event::DeleteMessageForMeUpdate(update) => {
            println!("Message {} in {} deleted for me", update.message_id, update.chat_jid);
        }
        Event::ContactRemoved(update) => {
            println!("Contact {} removed", update.jid);
        }
        _ => {}
    }
})
```

`Event::ContactRemoved` is distinct from `Event::ContactUpdate` — see [ContactRemoved](/concepts/events#contactremoved).

## Error handling

All methods return `Result<(), AppStateError>`:

```rust theme={null}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AppStateError {
    #[error("invalid app-state request: {0}")]
    InvalidRequest(String),
    #[error("{0}")]
    Internal(#[from] anyhow::Error),
}
```

You'll encounter these most often:

* `InvalidRequest` — you passed an invalid timestamp to `mute_chat_until` or omitted `participant_jid` for a group operation
* `Internal` — no app state sync key is available yet (sync not complete), a network error occurred, or (as of PR #1158) an app-state version conflict with another device could not be resolved after 5 rebuild-and-resend attempts

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

match client.chat_actions().mute_chat_until(&jid, 0).await {
    Ok(_) => println!("Muted"),
    Err(AppStateError::InvalidRequest(msg)) => eprintln!("Validation error: {}", msg),
    Err(e) => eprintln!("Failed: {}", e),
}
```

## Complete example

```rust theme={null}
use whatsapp_rust::Client;
use wacore_binary::jid::Jid;
use chrono::{Utc, Duration};
use std::sync::Arc;

async fn organize_chats(client: &Arc<Client>) -> anyhow::Result<()> {
    let important_chat: Jid = "15551234567@s.whatsapp.net".parse()?;
    let noisy_group: Jid = "123456789@g.us".parse()?;
    let old_chat: Jid = "15559876543@s.whatsapp.net".parse()?;
    
    // Pin important conversations
    client.chat_actions().pin_chat(&important_chat).await?;
    
    // Mute noisy group for 1 week
    let mute_until = Utc::now() + Duration::weeks(1);
    client.chat_actions()
        .mute_chat_until(&noisy_group, mute_until.timestamp_millis())
        .await?;
    
    // Archive old conversations
    client.chat_actions().archive_chat(&old_chat, None).await?;
    
    // Star an important message
    client.chat_actions()
        .star_message(&important_chat, None, "IMPORTANT_MSG_ID", false)
        .await?;
    
    // Mark chat as read across all devices
    client.chat_actions()
        .mark_chat_as_read(&important_chat, true, None)
        .await?;
    
    // Delete a message locally (not for everyone)
    client.chat_actions()
        .delete_message_for_me(&old_chat, None, "OLD_MSG_ID", false, true, None)
        .await?;
    
    // Delete an old chat and its media
    client.chat_actions().delete_chat(&old_chat, true, None).await?;
    
    Ok(())
}
```

## See also

* [Events](/concepts/events) - Handle chat action update events
* [Groups](/api/groups) - Group management operations
* [Client](/api/client) - Core client API
* [Labels](/api/labels) - Chat and message label operations built on the same app-state send path
* [Quick Replies](/api/quick-replies) - Saved reply shortcuts built on the same app-state send path
