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

# Media

> Upload a photo or a file, then attach it to a message by id.

A message carries attachments as media. Media has an id, not a URL. The bytes live in private
storage, and the API gives you a short-lived signed URL each time you read the media.

## Upload

Upload in three steps.

<Steps>
  <Step title="Reserve the media">
    Send the mime type. The response holds a `Media` and an `upload`.

    ```bash theme={null}
    curl -X POST "$BASE_URL/api/media" \
      -H "Authorization: Bearer $LALA_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"mimeType":"image/jpeg","sizeBytes":184320,"width":1080,"height":1440}'
    ```

    ```json theme={null}
    {
      "media": {
        "id": "4b1e2f3a-5c6d-4e7f-8a9b-0c1d2e3f4a5b",
        "kind": "image",
        "mimeType": "image/jpeg",
        "sizeBytes": 184320,
        "width": 1080,
        "height": 1440,
        "durationMs": null,
        "fileName": null,
        "status": "pending",
        "url": null,
        "urlExpiresAt": null,
        "createdAt": "2026-08-03T09:14:02.000Z"
      },
      "upload": {
        "url": "https://<project>.supabase.co/storage/v1/object/upload/sign/chat-media/...",
        "method": "PUT",
        "expiresAt": "2026-08-03T09:29:02.000Z"
      }
    }
    ```
  </Step>

  <Step title="Send the bytes">
    `PUT` the raw bytes to `upload.url`. Do not send an `Authorization` header. The URL is signed and
    it carries its own token.

    ```bash theme={null}
    curl -X PUT "$UPLOAD_URL" \
      -H "Content-Type: image/jpeg" \
      --data-binary @photo.jpg
    ```

    The URL expires in 15 minutes. If it expires, reserve a new media and start again.
  </Step>

  <Step title="Attach it to a message">
    Send the media id in `mediaIds`.

    ```bash theme={null}
    curl -N -X POST "$BASE_URL/api/chat/send" \
      -H "Authorization: Bearer $LALA_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"message":"bu soruyu çözemedim","mediaIds":["4b1e2f3a-5c6d-4e7f-8a9b-0c1d2e3f4a5b"]}'
    ```
  </Step>
</Steps>

The server confirms the upload when you send the message, so step 3 needs no extra call. Media
that never reached storage is dropped from the message. The send does not fail.

## Read

`GET /api/media/{id}` returns the media with a fresh signed URL. It also confirms a pending
upload, so it answers the question "did my upload land?".

```bash theme={null}
curl "$BASE_URL/api/media/4b1e2f3a-5c6d-4e7f-8a9b-0c1d2e3f4a5b" \
  -H "Authorization: Bearer $LALA_TOKEN"
```

Every message read gives you the same fields inside `media`, so a normal chat load needs no extra
request.

<Warning>
  `url` expires. It is valid for one hour by default. Do not cache it, and do not store it in a
  database. Read the media or the message again for a new one.
</Warning>

## Fields

| Field | Type | Notes |
| - | - | - |
| `id` | uuid | Send this in `mediaIds`. |
| `kind` | `image`, `audio`, `video`, or `file` | Derived from the mime type. |
| `mimeType` | string | Corrected from storage after the upload. |
| `sizeBytes` | integer or null | Corrected from storage after the upload. |
| `width`, `height` | integer or null | What you sent at step 1. The server does not read them from the file. |
| `durationMs` | integer or null | What you sent at step 1. |
| `fileName` | string or null | Metadata only. It is never part of the storage path. |
| `status` | `pending` or `ready` | `ready` means the bytes are in storage. |
| `url` | string or null | Signed. Null while the media is `pending`. |
| `urlExpiresAt` | date-time or null | When `url` stops working. |

## Limits

* 25 MB for one media. A larger `sizeBytes` returns `400` with `{"error":"media too large"}`.
* 10 media on one message.
* Lala reads at most 4 images from one message. The rest stay on the message for the student.

## What Lala sees

Lala reads image attachments. It does not read audio, video, or other files today.

A message that has attachments and no text reaches the model as a placeholder, such as `[görsel]`.
The placeholder exists only in the history of the model. On the wire, `content` for such a message
is the empty string. An empty `content` means that the student wrote no caption, so there is
nothing to show next to the attachment.

<Note>
  Media belongs to the student who created it. A media id from another account is unknown to your
  account, and the send drops it.
</Note>


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