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

# Upload media

> Upload a file and get a file_uuid to send it in an action.

To send images, video, audio, or documents, you first upload the file and get a
`file_uuid`, which you then pass to a send action.

<Steps>
  <Step title="Request an upload URL">
    ```http theme={null}
    POST /files/request-upload
    ```

    With `{ filename, mime_type }`. It returns `201` with
    `file_uuid`, `upload_url`, `upload_token`, `storage_path`, and `expires_at`.
  </Step>

  <Step title="Upload the binary">
    ```http theme={null}
    PUT {upload_url}
    ```

    Upload the file to the `upload_url` you received, before it expires
    (`expires_at`).
  </Step>

  <Step title="Confirm">
    ```http theme={null}
    POST /files/confirm
    ```

    With `{ file_uuid }`. It returns `200` with `file_uuid`, `mime_type`,
    `size_bytes`, `requires_transcription`, `download_url`, and `expires_at`.
    From here the `file_uuid` is sendable.
  </Step>
</Steps>

<Note>
  You no longer declare `size_bytes` when requesting the URL: the server measures
  the real size on confirm. In the confirm response, `requires_transcription`
  signals whether this `file_uuid` will require a transcription when used in a
  send — because the file type isn't auto-transcribable or it exceeds 20 MB. It's
  a hint so you can prepare it ahead of time.

  Validate the file size locally before uploading: the per-type limit and the storage quota are enforced at confirm, with the binary already transferred. A business with its storage disabled (`quota_bytes = 0`) or already over its cap is rejected at `request-upload` with `413` before the transfer.
</Note>

## Size limits

| Category     | Maximum                             |
| ------------ | ----------------------------------- |
| Image        | 5 MB                                |
| Video        | 16 MB                               |
| Audio        | 16 MB                               |
| Document     | 100 MB                              |
| `text/plain` | 5 MB (own cap, lower than Document) |

<Note>
  **Concurrent pending uploads:** a business can have at most **100 unconfirmed uploads** at a time. Beyond that, `request-upload` returns `429 TOO_MANY_PENDING_UPLOADS` with `Retry-After` — confirm or let your pending uploads expire before requesting more. The normal flow (request → PUT → confirm) never hits it; it only affects integrations that request many `request-upload` up front without confirming.
</Note>

## Send the file

Once confirmed, pass the `file_uuid` to a send action — as direct media in
`send_message`, or as a template header:

```jsonc theme={null}
{
  "conversation": { "phone": "+525512345678", "channel": "ch_7A9K2M4Q" },
  "actions": [
    {
      "type": "send_message",
      "template": {
        "name": "promo_verano",
        "language": "es_MX",
        "header": {
          "type": "document",
          "file_uuid": "7b8a1c2d-3e4f-5678-90ab-cdef12345678"
        },
        "body_variables": [{ "index": 1, "value": "Ana" }]
      }
    }
  ]
}
```

The `header.type` ∈ `image` | `video` | `document` carries `file_uuid`
(`document` accepts an optional `filename`) and a `transcription` that is **required**
when vision can't transcribe the file (the `requires_transcription` above anticipates
it). In `send_message` media mode, the `file_uuid` goes at the action level with `body`
as an optional caption (audio doesn't accept a caption) and the same `transcription` rule.

<Tip>
  If the preflight rejects the group, the response is a synchronous `4xx` and no
  action runs. See [Errors](/en/errors).
</Tip>

<Warning>
  **Uploaded media is ephemeral.** It is not permanent storage: under storage quota
  pressure, the system may automatically delete files **by last-use age** (the ones longest
  without being sent go first; reusing a `file_uuid` in any conversation refreshes its
  "last use" and keeps it alive for all of them). Keep your own copy if you need a durable
  record. The API does not offer a stable download by `file_uuid`; the only download is the
  signed `download_url` returned by `/files/confirm`, valid for a limited time. Resending an
  already-deleted `file_uuid` responds `FILE_NOT_FOUND`.

  **Reuse across conversations.** The same `file_uuid` can be sent to several
  conversations without re-uploading, but it is a single file: if quota cleanup deletes
  it, it is lost in **all** conversations that reference it. If you need independence
  between conversations, upload one file per conversation.
</Warning>

## Executable example

The full flow in three calls. The `file_uuid` confirmed in step 3 is the one you
then pass to a send action.

**1 · Request an upload URL**

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://app.1to1ai.com/api/v1/public/{slug}/files/request-upload" \
    -H "Authorization: Bearer sk_1to1_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "filename": "receipt.pdf",
      "mime_type": "application/pdf"
    }'
  ```

  ```json 201 Response theme={null}
  {
    "file_uuid": "7b8a1c2d-3e4f-5678-90ab-cdef12345678",
    "upload_url": "https://<project>.supabase.co/storage/v1/object/upload/sign/business-files/...",
    "upload_token": "eyJ...",
    "storage_path": "<business_uuid>/api/pending/1729383000_7b8a1c2d....pdf",
    "expires_at": "2026-07-22T15:30:00Z"
  }
  ```
</CodeGroup>

**2 · Upload the binary** to the `upload_url` before it expires

```bash theme={null}
curl -X PUT "{upload_url}" \
  -H "Content-Type: application/pdf" \
  --data-binary @receipt.pdf
```

**3 · Confirm**

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://app.1to1ai.com/api/v1/public/{slug}/files/confirm" \
    -H "Authorization: Bearer sk_1to1_your_api_key" \
    -H "Content-Type: application/json" \
    -d '{
      "file_uuid": "7b8a1c2d-3e4f-5678-90ab-cdef12345678"
    }'
  ```

  ```json 200 Response theme={null}
  {
    "file_uuid": "7b8a1c2d-3e4f-5678-90ab-cdef12345678",
    "mime_type": "application/pdf",
    "size_bytes": 48213,
    "requires_transcription": false,
    "download_url": "https://<project>.supabase.co/storage/v1/object/sign/...",
    "expires_at": "2026-07-22T16:30:00Z"
  }
  ```
</CodeGroup>

## With required transcription

If the file isn't auto-transcribable by vision (an unsupported type such as
`.docx`/`.xlsx`, or larger than 20 MB), the `/confirm` response
anticipates it with `requires_transcription: true`:

```json 200 Response (transcription required) theme={null}
{
  "file_uuid": "9c1b2a3d-4e5f-6789-01ab-cdef23456789",
  "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "size_bytes": 91240,
  "requires_transcription": true,
  "download_url": "https://<project>.supabase.co/storage/v1/object/sign/...",
  "expires_at": "2026-07-22T16:30:00Z"
}
```

In that case, when you send it you must include `transcription` — as the primary
media in `send_message` or as `template.header.transcription` — or the action
fails with `TRANSCRIPTION_REQUIRED` / `TEMPLATE_HEADER_TRANSCRIPTION_MISSING`:

```bash Send with transcription theme={null}
curl -X POST "https://app.1to1ai.com/api/v1/public/{slug}/conversation/actions" \
  -H "Authorization: Bearer sk_1to1_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation": { "phone": "+525512345678", "channel": "ch_7A9K2M4Q" },
    "actions": [
      {
        "type": "send_message",
        "file_uuid": "9c1b2a3d-4e5f-6789-01ab-cdef23456789",
        "body": "Your contract 📄",
        "transcription": "Service contract (3 pages): monthly Pro plan, automatic renewal, cancelable with 30 days' notice."
      }
    ]
  }'
```
