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

# Save A Message

> Save and unsave messages privately, fetch the cross-conversation saved list, and listen for save events with the CometChat Android SDK.

<Accordion title="AI Integration Quick Reference">
  ```kotlin theme={null}
  // Save / unsave a message
  CometChat.saveMessage(messageId, callbackListener)     // -> BaseMessage
  CometChat.unsaveMessage(messageId, callbackListener)   // -> BaseMessage

  // Fetch the saved list — user-level, so do NOT set a UID or GUID
  val request = MessagesRequest.MessagesRequestBuilder()
      .setSaved(true)
      .setLimit(50)
      .build()
  request.fetchNext(callbackListener)                    // List<BaseMessage>

  // Read save state off a message
  val isSaved = message.isSaved                          // savedAt > 0

  // Cap and availability
  val limit = CometChat.getSaveMessageLimit()            // -1 when unspecified
  val enabled = CometChat.isSaveMessageEnabled()         // Boolean
  ```
</Accordion>

Let users bookmark messages for later. Saving a message is **private to the logged-in user** — nobody else in the conversation can see it — and works **across conversations**: a user's saved messages from all of their chats appear in one list, synced across all of their devices. Let's see how to work with saved messages in CometChat's Android SDK.

<Note>
  Saving a message with the SDK requires the Save Message feature to be enabled for your app. You can check its availability at runtime using the [feature flag](#feature-availability).
</Note>

## Save a Message

To save a message, use the `saveMessage` method and pass the ID of the message. On success, the callback returns the updated `BaseMessage` with its `savedAt` attribute set.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    long messageId = 1;

    CometChat.saveMessage(messageId, new CometChat.CallbackListener<BaseMessage>() {
      @Override
      public void onSuccess(BaseMessage message) {
          Log.d(TAG, "Message saved at: " + message.getSavedAt());
      }

      @Override
      public void onError(CometChatException e) {
          Log.e(TAG, "Failed to save message: " + e.getMessage());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val messageId = 1L

    CometChat.saveMessage(messageId, object : CometChat.CallbackListener<BaseMessage>() {
      override fun onSuccess(message: BaseMessage?) {
          Log.d(TAG, "Message saved at: ${message?.savedAt}")
      }

      override fun onError(e: CometChatException?) {
          Log.e(TAG, "Failed to save message: ${e?.message}")
      }
    })
    ```
  </Tab>
</Tabs>

Saving is **idempotent** — saving an already saved message succeeds and refreshes `getSavedAt()` rather than failing.

<Warning>
  **A just-sent message may not be pinnable or savable yet.** On an app with moderation enabled, the server stamps a new message as moderation-pending and clears it a moment later. While it is pending, the call is rejected with `ERR_MESSAGE_NO_ACCESS` — the **same code returned for a genuine permission refusal**, so you cannot tell the two apart from the error alone.

  The window runs from **send**, not from the user's tap, and clears within a few seconds. Do not disable the control on this error: by the time someone opens a menu and taps, moderation has usually finished. Prefer a retry or a transient "not ready yet" message over telling the user they lack permission. Moderation is configured **per app**, so this never reproduces on an app that has it switched off. (The CometChat UI Kits already withhold the Pin/Save options on a moderation-pending, disapproved, deleted or not-yet-sent message; this only concerns custom UI built directly on the SDK.)
</Warning>

<Info>
  Unlike pinning, saving has no role restrictions — every user can save any message they have access to. Deleted messages cannot be saved.
</Info>

## Unsave a Message

To remove a message from the user's saved list, use the `unsaveMessage` method. On success, the callback returns the updated `BaseMessage` with its `savedAt` attribute cleared.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    long messageId = 1;

    CometChat.unsaveMessage(messageId, new CometChat.CallbackListener<BaseMessage>() {
      @Override
      public void onSuccess(BaseMessage message) {
          Log.d(TAG, "Message unsaved. isSaved: " + message.isSaved());
      }

      @Override
      public void onError(CometChatException e) {
          Log.e(TAG, "Failed to unsave message: " + e.getMessage());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val messageId = 1L

    CometChat.unsaveMessage(messageId, object : CometChat.CallbackListener<BaseMessage>() {
      override fun onSuccess(message: BaseMessage?) {
          Log.d(TAG, "Message unsaved. isSaved: ${message?.isSaved}")
      }

      override fun onError(e: CometChatException?) {
          Log.e(TAG, "Failed to unsave message: ${e?.message}")
      }
    })
    ```
  </Tab>
</Tabs>

## Fetch Saved Messages

To fetch all messages the logged-in user has saved, create a `MessagesRequest` with the `setSaved(true)` filter of the `MessagesRequestBuilder`. Because saved messages are user-level and span conversations, you must **not** set a `UID` or `GUID`. The returned list is sorted by the time of saving, most recently saved first.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    MessagesRequest messagesRequest = new MessagesRequest.MessagesRequestBuilder()
      .setSaved(true)
      .setLimit(50)
      .build();

    messagesRequest.fetchNext(new CometChat.CallbackListener<List<BaseMessage>>() {
      @Override
      public void onSuccess(List<BaseMessage> messages) {
          Log.d(TAG, "Saved messages: " + messages.size());
      }

      @Override
      public void onError(CometChatException e) {
          Log.e(TAG, "Saved messages fetch failed: " + e.getMessage());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val messagesRequest = MessagesRequest.MessagesRequestBuilder()
      .setSaved(true)
      .setLimit(50)
      .build()

    messagesRequest.fetchNext(object : CometChat.CallbackListener<List<BaseMessage>>() {
      override fun onSuccess(messages: List<BaseMessage>?) {
          Log.d(TAG, "Saved messages: ${messages?.size}")
      }

      override fun onError(e: CometChatException?) {
          Log.e(TAG, "Saved messages fetch failed: ${e?.message}")
      }
    })
    ```
  </Tab>
</Tabs>

The list is ordered by **save time, most recently saved first** — not by when the messages were sent. Page it with `fetchNext()` until a call returns an empty list; the cursor rides on `savedAt` instead of `sentAt`, and the SDK swaps it for you.

<Info>
  If the user loses access to a conversation (for example, they are removed from a group), messages saved from it are cleaned up and no longer returned.
</Info>

## Check if a Message is Saved

Every fetched message carries the logged-in user's save state on the `BaseMessage` itself. These values are **per-user**: the same message shows different values to different users.

| Method         | Description                                                                            |
| -------------- | -------------------------------------------------------------------------------------- |
| `isSaved()`    | Returns `true` if the logged-in user has saved this message.                           |
| `getSavedAt()` | The timestamp at which the logged-in user saved the message. `0` when it is not saved. |

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    if (message.isSaved()) {
        Log.d(TAG, "Saved at " + message.getSavedAt());
    }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    if (message.isSaved) {
        Log.d(TAG, "Saved at ${message.savedAt}")
    }
    ```
  </Tab>
</Tabs>

## Real-time Save Events

Because saving is private, save events are never delivered to other participants. Register a `MessageListener` and override the save callbacks; each event delivers the full updated `BaseMessage`.

Today these callbacks fire on the **acting device** when a save or unsave succeeds. Delivery to the user's other devices activates once server-side real-time delivery for save events is rolled out — until then, other sessions pick up save changes on their next fetch.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    private String listenerID = "UNIQUE_LISTENER_ID";

    CometChat.addMessageListener(listenerID, new CometChat.MessageListener() {
      @Override
      public void onMessageSaved(BaseMessage message) {
          Log.d(TAG, "Message saved: " + message.getId());
      }

      @Override
      public void onMessageUnsaved(BaseMessage message) {
          Log.d(TAG, "Message unsaved: " + message.getId());
      }
    });
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val listenerID = "UNIQUE_LISTENER_ID"

    CometChat.addMessageListener(listenerID, object : CometChat.MessageListener() {
      override fun onMessageSaved(message: BaseMessage) {
          Log.d(TAG, "Message saved: ${message.id}")
      }

      override fun onMessageUnsaved(message: BaseMessage) {
          Log.d(TAG, "Message unsaved: ${message.id}")
      }
    })
    ```
  </Tab>
</Tabs>

To stop listening, remove the listener with `CometChat.removeMessageListener(listenerID)`.

## Save Limit

A user can save a capped number of messages, configurable per app. Read the cap rather than hard-coding it — it is tenant-overridable and will drift.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    int limit = CometChat.getSaveMessageLimit();

    if (limit != Settings.LIMIT_UNSPECIFIED && savedCount >= limit) {
        // Disable the save control instead of letting the user hit the error
    }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    val limit = CometChat.getSaveMessageLimit()

    if (limit != Settings.LIMIT_UNSPECIFIED && savedCount >= limit) {
        // Disable the save control instead of letting the user hit the error
    }
    ```
  </Tab>
</Tabs>

The call is synchronous and never throws. It returns `Settings.LIMIT_UNSPECIFIED` (`-1`) when the app settings carry no value or when it is called before `init()` completes — show generic copy in that case rather than guessing a number. Unlike pinning, saving has no separate system cap.

When the limit is breached, the SDK surfaces the server error through `onError`, and the applicable limit is carried in the exception's `errorParams`:

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    @Override
    public void onError(CometChatException e) {
        Object limit = e.getErrorParams() != null ? e.getErrorParams().get("limit") : null;
        if (limit != null) {
            Log.e(TAG, "You can save up to " + limit + " messages.");
        }
    }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    override fun onError(e: CometChatException?) {
        val limit = e?.errorParams?.get("limit")
        if (limit != null) {
            Log.e(TAG, "You can save up to $limit messages.")
        }
    }
    ```
  </Tab>
</Tabs>

## Error Handling

| Error                               | Meaning                                                                                                              |
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `ERR_SAVED_MESSAGES_LIMIT_EXCEEDED` | The user's save cap was reached. The cap is in `errorParams["limit"]` when the server includes it.                   |
| `ERR_MESSAGE_NO_ACCESS`             | The user cannot act on this message — either a genuine access refusal or a message still held by moderation (above). |

## Feature Availability

Check whether the Save Message feature is enabled for your app before showing save actions in your UI. The method is synchronous and safe to call from the UI layer.

<Tabs>
  <Tab title="Java">
    ```java theme={null}
    if (CometChat.isSaveMessageEnabled()) {
        // show the Save option
    }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    if (CometChat.isSaveMessageEnabled()) {
        // show the Save option
    }
    ```
  </Tab>
</Tabs>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Pin A Message" icon="thumbtack" href="/docs/sdk/android/v5/pin-message">
    Highlight a message for everyone in the conversation
  </Card>

  <Card title="Pin A Conversation" icon="list" href="/docs/sdk/android/v5/pin-conversation">
    Pin a conversation to the top of the list
  </Card>

  <Card title="All Real Time Listeners" icon="tower-broadcast" href="/docs/sdk/android/v5/real-time-listeners">
    Every listener the SDK exposes, in one place
  </Card>

  <Card title="Additional Message Filtering" icon="filter" href="/docs/sdk/android/v5/additional-message-filtering">
    Filter messages by pinned, saved, type, tags and more
  </Card>
</CardGroup>
