> ## 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 saved list across conversations, and listen for save events with the CometChat Flutter SDK.

<Accordion title="AI Integration Quick Reference">
  | Field | Value |
  | - | - |
  | Package | `cometchat_sdk` |
  | Import | `import 'package:cometchat_sdk/cometchat_sdk.dart';` |
  | Purpose | Save and unsave messages privately, fetch the saved list across conversations, and listen for save events with the CometChat Flutter SDK. |
  | Key methods | `saveMessage()` · `unsaveMessage()` · `isSaveMessageEnabled()` · `getSavedMessagesLimit()` · `addMessageListener()` |
  | Key classes | `MessagesRequestBuilder` · `BaseMessage` · `CometChatException` |
  | Key fields | `BaseMessage.savedAt` · `MessagesRequestBuilder.saved` |
  | Listener callbacks | `onMessageSaved()` · `onMessageUnsaved()` |
  | Prerequisites | SDK initialised via [`CometChat.init()`](/docs/sdk/flutter/setup) and a logged-in user via [`CometChat.login()`](/docs/sdk/flutter/authentication-overview). |
  | Constraints | Saves are private to the logged-in user. The saved list spans every conversation — do **not** set `uid` or `guid` on the builder. |
  | Related | [Pin A Message](/docs/sdk/flutter/pin-message) · [Pin A Conversation](/docs/sdk/flutter/pin-conversation) |
  | Full reference | [`BaseMessage`](/docs/sdk/reference/messages#basemessage) · [`CometChatException`](/docs/sdk/reference/auxiliary#cometchatexception) |
</Accordion>

<Note>
  **Available from Chat SDK v5.0.7.** These APIs require `cometchat_sdk` v5.0.7 or later. See [Setup](/docs/sdk/flutter/setup) to upgrade.
</Note>

Saving bookmarks a message for the logged-in user. Unlike a [pin](/docs/sdk/flutter/pin-message), a save is **private and cross-conversation**: nobody else is notified or can see it, no role is required, and the saved list spans every conversation the user is part of — their own messages and everyone else's.

<Note>
  `savedAt` is per-viewer. It is only ever populated in the acting user's own context — you will never see another user's saves on a message.
</Note>

## Save a Message

Call `saveMessage()` with the message's ID. On success, `onSuccess` receives the full updated message, with `savedAt` set.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    int messageId = 103;

    CometChat.saveMessage(messageId, onSuccess: (BaseMessage message) {
      debugPrint("Message saved at: ${message.savedAt}");
    }, onError: (CometChatException e) {
      debugPrint("Message saving failed with exception: ${e.message}");
    });
    ```
  </Tab>
</Tabs>

Saving is idempotent — saving an already saved message succeeds rather than failing.

## Unsave a Message

Call `unsaveMessage()`. The returned message comes back with `savedAt` cleared to `null`.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    int messageId = 103;

    CometChat.unsaveMessage(messageId, onSuccess: (BaseMessage message) {
      debugPrint("Message unsaved successfully");
    }, onError: (CometChatException e) {
      debugPrint("Message unsaving failed with exception: ${e.message}");
    });
    ```
  </Tab>
</Tabs>

## Fetch Saved Messages

Build a `MessagesRequest` with the `saved` field of the `MessagesRequestBuilder` set to `true`. The saved list is **user-level**, so unlike the pinned list you do **not** set `uid` or `guid` — leaving both unset is what makes it cross-conversation.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    MessagesRequest messageRequest = (MessagesRequestBuilder()
          ..saved = true
          ..limit = 50)
        .build();

    messageRequest.fetchPrevious(onSuccess: (List<BaseMessage> list) {
      debugPrint("Saved messages fetched: ${list.length}");
    }, onError: (CometChatException e) {
      debugPrint("Saved message fetching failed with exception: ${e.message}");
    });
    ```
  </Tab>
</Tabs>

The list is ordered by **save time, most recently saved first** — not by when the messages were sent. Call `fetchPrevious()` again on the same object to page through older entries; `fetchNext()` needs a message ID or timestamp cursor and fails with `ERR_FILTERS_MISSING` without one. The `setSaved(true)` setter is equivalent to the `saved` field.

Because the list spans conversations, every row carries its own context — use `conversationId`, `receiverType` and `receiverUid` to route a tap on a saved message back to the right conversation. For a one-on-one message the logged-in user *received*, `receiverUid` is their own UID, so open the conversation with `sender` instead.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    for (final BaseMessage message in list) {
      debugPrint("${message.conversationId} "
          "${message.receiverType} " // "user" or "group"
          "${message.receiverUid}");
    }
    ```
  </Tab>
</Tabs>

## Check if a Message is Saved

A `null` `savedAt` means the logged-in user has not saved the message.

| Field | Type | Description |
| - | - | - |
| `savedAt` | `DateTime?` | When the logged-in user saved the message, or `null` when they have not. |

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    if (message.savedAt != null) {
      debugPrint("Saved at: ${message.savedAt}");
    }
    ```
  </Tab>
</Tabs>

## Real-time Save Events

Because saves are private, save events reach only the **logged-in user's own devices**. Register a `MessageListener` using `addMessageListener()` and override the `onMessageSaved()` and `onMessageUnsaved()` callbacks.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    class Class_Name with MessageListener {

      //CometChat.addMessageListener("listenerId", this);

      @override
      void onMessageSaved(BaseMessage message) {
        debugPrint("Message saved: ${message.id}");
      }

      @override
      void onMessageUnsaved(BaseMessage message) {
        debugPrint("Message unsaved: ${message.id}");
      }
    }
    ```
  </Tab>
</Tabs>

<Note>
  In Flutter, the device that performed the save **also** receives these callbacks — the SDK fans them out locally as soon as the call succeeds and drops the server's echo of the same action. The user's other devices receive them over the socket. Update your saved list from the listener alone and you cover both.
</Note>

## Save Limit

A user may save a capped number of messages across all conversations. Read the cap rather than hard-coding it.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    int? limit = CometChat.getSavedMessagesLimit();

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

`getSavedMessagesLimit()` is synchronous and returns `null` when the backend did not serve a limit — show generic copy in that case rather than guessing a number.

## Error Handling

`saveMessage()` and `unsaveMessage()` report failures through `onError` with a `CometChatException`. Branch on `e.code` rather than the message text:

| Code | Meaning |
| - | - |
| `ERR_SAVED_MESSAGES_LIMIT_EXCEEDED` | The user's save cap was reached. The cap is in `errorParams["limit"]`. |
| `ERR_MESSAGE_ID_NOT_FOUND` | No message with that ID — it never existed, or it was deleted. |
| `ERR_MESSAGE_NO_ACCESS` | The user has no access to that message — for example they are not a participant in its conversation, or it is a just-sent message still held by moderation. |

Saving is per-user, so there is no role gate: any participant can save a message they have access to, up to the cap.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    CometChat.saveMessage(messageId, onSuccess: (BaseMessage message) {
      debugPrint("Message saved");
    }, onError: (CometChatException e) {
      if (e.code == 'ERR_SAVED_MESSAGES_LIMIT_EXCEEDED') {
        final limit = e.errorParams?['limit'];
        debugPrint("You can only save $limit messages. Unsave one to save another.");
      }
    });
    ```
  </Tab>
</Tabs>

## Feature Availability

Check whether Save Message is enabled for your app before showing save actions. `isSaveMessageEnabled()` is synchronous and never throws.

<Tabs>
  <Tab title="Dart">
    ```dart theme={null}
    if (CometChat.isSaveMessageEnabled()) {
      // Show the Save option
    }
    ```
  </Tab>
</Tabs>

When neither the login payload nor the app settings carry the flag, `isSaveMessageEnabled()` returns `true`, so the feature is not disabled on a backend that predates the flag.

***

## Next Steps

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

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

  <Card title="Retrieve Conversations" icon="comments" href="/docs/sdk/flutter/retrieve-conversations">
    Fetch and order the conversation list
  </Card>

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.