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

# Attachments

> Photos, videos, and files on any message, both directions.

Any message can carry attachments, on both `createTicket` and `sendMessage`,
in both directions (the user sends; agent-sent media renders in the thread).

## Limits

| Rule         | Value           |
| ------------ | --------------- |
| Per message  | ≤ 4 attachments |
| Image        | ≤ 10 MB         |
| Video / file | ≤ 50 MB         |

## How upload works

The SDK runs a two-phase upload so bytes go straight to storage, never through
the ClarioDesk API:

<Steps>
  <Step title="Sign">
    The SDK requests a presigned upload target for each file.
  </Step>

  <Step title="Normalize & upload">
    Images are normalized to JPEG client-side (this strips HEIC/EXIF and caps
    dimensions). The SDK then PUTs the bytes directly to Cloudflare R2, or to
    Cloudflare Stream for video, which transcodes to universal H.264 and
    produces a free poster frame.
  </Step>

  <Step title="Bind">
    On message send, the SDK binds the uploaded ids to the message.
    `Message.attachments` then carries the rendered payloads.
  </Step>
</Steps>

## Progress

Each upload exposes per-tile progress so you can render a progress ring:

<CodeGroup>
  ```dart Flutter theme={null}
  // ValueListenable<double> per upload
  final progress = ClarioDesk.attachmentUploadProgress(localId);
  ```

  ```ts React Native theme={null}
  const progress = ClarioDesk.attachmentUploadProgress(stubId);
  ```
</CodeGroup>

## Sending

<CodeGroup>
  ```dart Flutter theme={null}
  await ClarioDesk.sendMessage(
    ticketId,
    'see screenshot',
    attachments: [OutgoingAttachment.file(path)],
  );
  ```

  ```ts React Native theme={null}
  await ClarioDesk.sendMessage(ticketId, 'see screenshot', {
    attachments: [{ uri, mime: 'image/jpeg', kind: 'image' }],
  });
  ```
</CodeGroup>

In React Native, pass a `MediaPicker` / `ImageNormalizer` via
`init({ attachments: { picker, normalizer } })`, or build and upload
`OutgoingAttachment`s yourself.

## Reading

Presigned GET URLs expire. If an image fails to load because its URL aged out,
re-presign it:

```text theme={null}
refreshAttachmentUrl(attachmentId)
```

<Note>
  Video playback is external in v1 on Flutter and React Native. An in-app HLS
  player would ship a heavy native dependency to every host, so video uses
  external/system playback there. The Swift SDK plays video in-app via
  `AVPlayer` (free and native on iOS). The in-app image viewer is a
  lightweight, zero-dependency zoomable view on all three.
</Note>

## iOS permission strings

The native media picker requires usage strings in your iOS app's `Info.plist`:
`NSPhotoLibraryUsageDescription`, `NSCameraUsageDescription`, and
`NSMicrophoneUsageDescription` (for video). The React Native Expo config plugin
can add these for you. See the [RN installation guide](/react-native/installation).
