> ## Documentation Index
> Fetch the complete documentation index at: https://cometchat-22654f5b-docs-multi-attachment-js-sdk.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Files & Send Attachments

> Upload files directly to storage with per-file progress, cancel, and retry — then send them as one or more media messages with multiple attachments.

`CometChat.uploadFiles()` uploads one or more files **directly to storage** and reports **per-file progress, success, and failure** through a listener. It is **decoupled from sending**: uploading returns you an [`Attachment`](/sdk/reference/auxiliary#attachment) (carrying a hosted `url`) for each file, which you then attach to a [`MediaMessage`](/sdk/reference/messages#mediamessage) and send with [`sendMediaMessage()`](/sdk/javascript/send-message#media-message).

This is the recommended way to build a **multi-attachment composer**: upload a batch of files, show a progress bar per file, let the user cancel or retry individual files, and once they're ready, send them as a single media message with multiple attachments (or split across several messages).

<Info>
  **Why upload separately instead of passing files to `sendMediaMessage()`?**

  The classic path (passing a file, or `FileList`, straight to the `MediaMessage` constructor) uploads and sends in one blocking call — you get no progress, no per-file cancel, and no per-file retry. `uploadFiles()` moves the upload out of the send call so you can drive a rich composer UI, then send instantly because the files are already hosted.
</Info>

## The upload-then-send flow

<Steps>
  <Step title="Collect files">
    Get platform-native file objects (e.g. `File`/`Blob` from an `<input type="file">` on web).
  </Step>

  <Step title="Upload">
    Call `CometChat.uploadFiles(files, receiverId, receiverType, listener)`. The **recipient** (`receiverId` + `receiverType`) is required — the server uses it to apply role- and scope-based access control before issuing upload URLs. It returns **synchronously** with a `batchId` and a `fileId` per file (in input order) so you can map callbacks back to your UI rows.
  </Step>

  <Step title="Track progress & handle failures">
    Your `MediaUploadListener` receives `onProgress` for each file, then `onFileUploaded` (success), `onFileError` (rejected — not retryable), or `onFileFailure` (failed — retryable). Use `cancelFileUpload()` / `retryFileUpload()` as the user acts.
  </Step>

  <Step title="Collect attachments">
    Each `onFileUploaded` (and the final `onComplete`) hands you an `Attachment` with a hosted `url`. Gather the ones you want to send.
  </Step>

  <Step title="Build & send the message">
    Put the attachments on a `MediaMessage` with `setAttachments([...])` and call `sendMediaMessage()`. Because the attachments already have URLs, the message is sent as JSON — no re-upload.
  </Step>

  <Step title="Clean up">
    Call `clearUploadGroup(batchId)` after a successful send (or to abandon the composer) to release the batch from memory.
  </Step>
</Steps>

## Upload files

```html theme={null}
<input type="file" name="files" id="files" multiple />
```

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const files: FileList = (document.getElementById("files") as HTMLInputElement).files!;

    const receiverId = "UID";                              // recipient uid or guid
    const receiverType = CometChat.RECEIVER_TYPE.USER;     // "user" or "group"

    const listener = new CometChat.MediaUploadListener({
      onProgress: (fileId: string, loaded: number, total: number, percent: number) => {
        console.log(`${fileId}: ${percent}%`);
      },
      onFileUploaded: (fileId: string, attachment: CometChat.Attachment) => {
        console.log(`${fileId} uploaded ->`, attachment.getUrl());
      },
      onFileError: (fileId: string, error: CometChat.CometChatException) => {
        console.log(`${fileId} rejected (not retryable):`, error.code);
      },
      onFileFailure: (fileId: string, error: CometChat.CometChatException) => {
        console.log(`${fileId} failed (retryable):`, error.code);
      },
      onComplete: (result: CometChat.UploadResult) => {
        console.log("Batch settled:", result);
      },
    });

    const { batchId, fileIds } = CometChat.uploadFiles(
      Array.from(files),
      receiverId,
      receiverType,
      listener
    );
    console.log("Upload group:", batchId, fileIds);
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    const files = document.getElementById("files").files;

    const receiverId = "UID";                              // recipient uid or guid
    const receiverType = CometChat.RECEIVER_TYPE.USER;     // "user" or "group"

    const listener = new CometChat.MediaUploadListener({
      onProgress: (fileId, loaded, total, percent) => {
        console.log(`${fileId}: ${percent}%`);
      },
      onFileUploaded: (fileId, attachment) => {
        console.log(`${fileId} uploaded ->`, attachment.getUrl());
      },
      onFileError: (fileId, error) => {
        console.log(`${fileId} rejected (not retryable):`, error.code);
      },
      onFileFailure: (fileId, error) => {
        console.log(`${fileId} failed (retryable):`, error.code);
      },
      onComplete: (result) => {
        console.log("Batch settled:", result);
      },
    });

    const { batchId, fileIds } = CometChat.uploadFiles(
      Array.from(files),
      receiverId,
      receiverType,
      listener
    );
    console.log("Upload group:", batchId, fileIds);
    ```
  </Tab>
</Tabs>

`uploadFiles()` accepts these arguments:

| Parameter      | Type                  | Description                                                                                                       | Required |
| -------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- | -------- |
| `files`        | `Array<File \| Blob>` | Platform-native file objects to upload.                                                                           | Yes      |
| `receiverId`   | `string`              | Intended recipient — a user's `uid` or a group's `guid`. The server needs it to authorize the upload (see below). | Yes      |
| `receiverType` | `string`              | `CometChat.RECEIVER_TYPE.USER` (`"user"`) or `CometChat.RECEIVER_TYPE.GROUP` (`"group"`).                         | Yes      |
| `listener`     | `MediaUploadListener` | Callback bundle for progress, per-file outcomes, and batch completion.                                            | Yes      |
| `options`      | `UploadOptions`       | Optional `{ concurrency?, batchId? }` — see [Upload options](#upload-options).                                    | No       |

<Warning>
  **The recipient must match the message you'll eventually send.** `receiverId` / `receiverType` are sent to the upload-authorization (presign) endpoint, which enforces the sender's **role- and scope-based access control** for that conversation *before* any bytes transfer. If the sender isn't allowed to send media to that user or group, each file is **rejected** via `onFileError` with `ERR_PERMISSION_DENIED` (not retryable). Pass the same recipient you'll set on the `MediaMessage` at send time — uploading against one recipient and sending to another can pass the upload check but is not the intended usage.
</Warning>

It returns **synchronously**:

| Field     | Type            | Description                                                                                        |
| --------- | --------------- | -------------------------------------------------------------------------------------------------- |
| `batchId` | `string`        | Identifier for this upload group. Use it with cancel / retry / clear methods.                      |
| `fileIds` | `Array<string>` | One id per input file, in the **same order** as `files`. Each id looks like `"<batchId>:<index>"`. |

<Note>
  Validation, presigning, and the actual byte transfer all run **asynchronously** after the method returns. Use the `fileId`s to line each file up with its UI row, then update that row as the listener fires.
</Note>

## The MediaUploadListener

Pass a `MediaUploadListener` with only the callbacks you need — all are optional. Unlike [`MessageListener`](/sdk/javascript/receive-message) or `CallListener`, it is **not** registered globally with a string id; it lives for the duration of the upload batch.

| Callback         | Signature                                  | Fires when                                                                                                      |
| ---------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| `onProgress`     | `(fileId, loaded, total, percent) => void` | Transfer progress for a file (throttled to \~1% increments).                                                    |
| `onFileUploaded` | `(fileId, attachment) => void`             | A file finished uploading. `attachment` is a ready-to-send [`Attachment`](/sdk/reference/auxiliary#attachment). |
| `onFileError`    | `(fileId, error) => void`                  | A file was **rejected** — **not retryable** (fix the input or permissions).                                     |
| `onFileFailure`  | `(fileId, error) => void`                  | A file's transfer **failed** — **retryable** via `retryFileUpload()`.                                           |
| `onComplete`     | `(result) => void`                         | The batch drained (no files in flight). Delivers an aggregate [`UploadResult`](#uploadresult).                  |

<Warning>
  **Rejected vs. failed — the key distinction.** `onFileError` (rejected) means the request itself is unacceptable — invalid file object, over the size limit, or the sender isn't permitted to upload to this recipient (`ERR_PERMISSION_DENIED`). Retrying won't help; the user must swap the file or you must fix the recipient/permissions. `onFileFailure` (failed) means a transient transport problem — network drop, an expired presigned URL, or a stalled upload. These **can** be retried with `retryFileUpload()`.
</Warning>

### UploadResult

`onComplete` receives the group's settled state. `onComplete` fires **each time** the group drains — including after a retry adds new work and it drains again.

```typescript theme={null}
interface UploadResult {
  batchId: string;
  successful: Array<{ fileId: string; attachment: Attachment }>; // uploaded, ready to send
  rejected:   Array<{ fileId: string; error: CometChatException }>; // reported via onFileError — not retryable
  failed:     Array<{ fileId: string; error: CometChatException }>; // reported via onFileFailure — retryable
}
```

## Upload options

Pass an optional third argument to tune the batch:

| Option        | Type     | Default | Description                                                                                                   |
| ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `concurrency` | `number` | `1`     | How many files upload simultaneously. Defaults to sequential (`1`); raise it to upload in parallel.           |
| `batchId`     | `string` | —       | Pass an **existing** group's `batchId` to add more files to it (incremental) instead of starting a new group. |

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    // Upload 3 files at a time
    const { batchId } = CometChat.uploadFiles(firstFiles, receiverId, receiverType, listener, { concurrency: 3 });

    // Later, add more files to the same group (same recipient)
    CometChat.uploadFiles(moreFiles, receiverId, receiverType, listener, { batchId });
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    // Upload 3 files at a time
    const { batchId } = CometChat.uploadFiles(firstFiles, receiverId, receiverType, listener, { concurrency: 3 });

    // Later, add more files to the same group (same recipient)
    CometChat.uploadFiles(moreFiles, receiverId, receiverType, listener, { batchId });
    ```
  </Tab>
</Tabs>

## Cancel, retry & clear

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    // Abort and remove a single in-flight file (no-op if already uploaded)
    CometChat.cancelFileUpload(batchId, fileId);

    // Re-upload a single FAILED file (re-presigns automatically if the URL expired).
    // No-op for rejected files — those aren't retryable.
    CometChat.retryFileUpload(batchId, fileId);

    // Release the whole group from memory, aborting anything still in flight.
    // Call this after a successful send, or to abandon the composer.
    CometChat.clearUploadGroup(batchId);
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    // Abort and remove a single in-flight file (no-op if already uploaded)
    CometChat.cancelFileUpload(batchId, fileId);

    // Re-upload a single FAILED file (re-presigns automatically if the URL expired).
    // No-op for rejected files — those aren't retryable.
    CometChat.retryFileUpload(batchId, fileId);

    // Release the whole group from memory, aborting anything still in flight.
    // Call this after a successful send, or to abandon the composer.
    CometChat.clearUploadGroup(batchId);
    ```
  </Tab>
</Tabs>

| Method                              | Behavior                                                                                                        |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `cancelFileUpload(batchId, fileId)` | Aborts and removes one file from the group. No-op if that file has already finished uploading.                  |
| `retryFileUpload(batchId, fileId)`  | Re-queues one **failed** file, re-presigning first if the earlier upload URL expired. No-op for rejected files. |
| `clearUploadGroup(batchId)`         | Aborts any in-flight uploads and drops the group from SDK memory.                                               |

<Note>
  All active upload groups are also cleared automatically on [`CometChat.logout()`](/sdk/javascript/authentication-overview).
</Note>

## Send the uploaded files as a media message

Once your files are uploaded, collect their `Attachment`s (from `onFileUploaded` or from `result.successful` in `onComplete`), set them on a `MediaMessage`, and send. One upload batch can go out as a single multi-attachment message, or you can split the attachments across several messages.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const uploaded: CometChat.Attachment[] = [];

    // Declare the recipient once — used for BOTH the upload authorization and the message.
    const receiverID = "UID";
    const receiverType = CometChat.RECEIVER_TYPE.USER;

    const listener = new CometChat.MediaUploadListener({
      onProgress: (fileId, loaded, total, percent) => updateRow(fileId, percent),
      onFileUploaded: (fileId, attachment) => uploaded.push(attachment),
      onFileFailure: (fileId, error) => markRetryable(fileId, error),
      onFileError: (fileId, error) => markRejected(fileId, error), // incl. ERR_PERMISSION_DENIED
      onComplete: async (result) => {
        if (result.successful.length === 0) return;

        const mediaMessage = new CometChat.MediaMessage(
          receiverID,
          "",            // no raw file — attachments already carry hosted URLs
          CometChat.MESSAGE_TYPE.FILE,
          receiverType
        );
        mediaMessage.setAttachments(result.successful.map((s) => s.attachment));

        try {
          const sent = await CometChat.sendMediaMessage(mediaMessage);
          console.log("Media message sent successfully", sent);
          CometChat.clearUploadGroup(result.batchId);
        } catch (error) {
          console.log("Media message sending failed with error", error);
        }
      },
    });

    const files = (document.getElementById("files") as HTMLInputElement).files!;
    CometChat.uploadFiles(Array.from(files), receiverID, receiverType, listener, { concurrency: 3 });
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    const uploaded = [];

    // Declare the recipient once — used for BOTH the upload authorization and the message.
    const receiverID = "UID";
    const receiverType = CometChat.RECEIVER_TYPE.USER;

    const listener = new CometChat.MediaUploadListener({
      onProgress: (fileId, loaded, total, percent) => updateRow(fileId, percent),
      onFileUploaded: (fileId, attachment) => uploaded.push(attachment),
      onFileFailure: (fileId, error) => markRetryable(fileId, error),
      onFileError: (fileId, error) => markRejected(fileId, error), // incl. ERR_PERMISSION_DENIED
      onComplete: async (result) => {
        if (result.successful.length === 0) return;

        const mediaMessage = new CometChat.MediaMessage(
          receiverID,
          "",            // no raw file — attachments already carry hosted URLs
          CometChat.MESSAGE_TYPE.FILE,
          receiverType
        );
        mediaMessage.setAttachments(result.successful.map((s) => s.attachment));

        try {
          const sent = await CometChat.sendMediaMessage(mediaMessage);
          console.log("Media message sent successfully", sent);
          CometChat.clearUploadGroup(result.batchId);
        } catch (error) {
          console.log("Media message sending failed with error", error);
        }
      },
    });

    const files = document.getElementById("files").files;
    CometChat.uploadFiles(Array.from(files), receiverID, receiverType, listener, { concurrency: 3 });
    ```
  </Tab>
</Tabs>

<Note>
  `sendMediaMessage()` enforces the **maximum attachments per message** (see [Limits](#limits)). If a batch has more successful uploads than the limit allows, split them across multiple `MediaMessage`s. See [Multiple Attachments in a Media Message](/sdk/javascript/send-message#multiple-attachments-in-a-media-message).
</Note>

## Limits

Two independent checks guard the flow, each using a limit read from your app settings (with a built-in fallback):

| Limit                       | Enforced in          | App setting      | Default | On breach                                                              |
| --------------------------- | -------------------- | ---------------- | ------- | ---------------------------------------------------------------------- |
| **Per-file size**           | `uploadFiles()`      | `file.size.max`  | 100 MB  | That file is rejected via `onFileError` with `ERR_FILE_SIZE_EXCEEDED`. |
| **Attachments per message** | `sendMediaMessage()` | `file.count.max` | 10      | `sendMediaMessage()` rejects with `ERR_FILE_COUNT_EXCEEDED`.           |

Read the current attachment-count limit at runtime with `getMaxAttachmentCount()` — useful for disabling the "add file" button in your composer once the user hits the cap:

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const max: number = await CometChat.getMaxAttachmentCount();
    console.log("Max attachments per message:", max);
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript theme={null}
    const max = await CometChat.getMaxAttachmentCount();
    console.log("Max attachments per message:", max);
    ```
  </Tab>
</Tabs>

<Note>
  The **per-file size** limit is checked in `uploadFiles()` (before the byte transfer), while the **attachment count** limit is checked in `sendMediaMessage()` (when you send). A batch can therefore succeed at upload time but still be rejected at send time if it exceeds the count limit — plan your composer around both.
</Note>

## Reliability behavior

The SDK handles a few transport edge cases for you:

* **Stalled uploads** — if a file makes no progress for 30 seconds, its upload is aborted and reported through `onFileFailure` with `ERR_UPLOAD_STALLED`. Retry it with `retryFileUpload()`.
* **Expired upload URLs** — the pre-signed upload URL for each file is valid for 15 minutes. `retryFileUpload()` automatically requests a fresh URL if the original has expired, so retries keep working even after a long delay.
* **Storage errors** — if storage rejects the upload, the failure surfaces through `onFileFailure` with `ERR_S3_UPLOAD_FAILED` and, where available, the storage's own message.

## Error handling

Every error delivered to `onFileError` / `onFileFailure` (and rejections from `sendMediaMessage()`) is a [`CometChatException`](/sdk/reference/auxiliary#cometchatexception). The upload-specific codes:

| Code                        | Surfaced via                   | Retryable | Meaning                                                                                                                                                                                    |
| --------------------------- | ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ERR_INVALID_FILE_OBJECT`   | `onFileError`                  | No        | The file is invalid or its size can't be determined.                                                                                                                                       |
| `ERR_FILE_SIZE_EXCEEDED`    | `onFileError`                  | No        | The file is larger than `file.size.max`.                                                                                                                                                   |
| `ERR_PERMISSION_DENIED`     | `onFileError`                  | No        | The sender isn't allowed to upload media to this `receiverId` / `receiverType` (role/scope access control).                                                                                |
| `ERR_FILE_COUNT_EXCEEDED`   | `sendMediaMessage()`           | No        | More attachments on the message than `file.count.max` allows.                                                                                                                              |
| `ERR_S3_UPLOAD_FAILED`      | `onFileFailure`, `onFileError` | Yes / No  | Storage rejected the upload or the transport errored (`onFileFailure`, retryable); also the fallback when the server declines a file with no specific code (`onFileError`, not retryable). |
| `ERR_PRESIGNED_URL_EXPIRED` | `onFileFailure`                | Yes       | The pre-signed URL expired (a retry re-presigns automatically).                                                                                                                            |
| `ERR_UPLOAD_STALLED`        | `onFileFailure`                | Yes       | No progress for 30s; the upload was aborted.                                                                                                                                               |
| `ERR_UPLOAD_CANCELLED`      | —                              | —         | The upload was cancelled via `cancelFileUpload()`.                                                                                                                                         |

`ERR_PERMISSION_DENIED` is returned by the server's access-control check, so its exact code and message come from your backend. See the full list on the [Error Codes](/sdk/javascript/error-codes#media-upload-errors) page.

## Next Steps

<CardGroup cols={2}>
  <Card title="Send A Message" icon="paper-plane" href="/sdk/javascript/send-message">
    Send text, media, and custom messages
  </Card>

  <Card title="Multiple Attachments" icon="paperclip" href="/sdk/javascript/send-message#multiple-attachments-in-a-media-message">
    Send several attachments in one media message
  </Card>

  <Card title="Error Codes" icon="triangle-exclamation" href="/sdk/javascript/error-codes#media-upload-errors">
    Media upload error reference
  </Card>

  <Card title="Receive Messages" icon="inbox" href="/sdk/javascript/receive-message">
    Listen for incoming messages in real-time
  </Card>
</CardGroup>
