> For the complete documentation index, see [llms.txt](https://docs.flora.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.flora.ai/mcp/recipes/upload-files.md).

# Upload your own images, video and files

Bring your own images, video, audio and PDFs into FLORA from Claude, Claude Code, ChatGPT, Codex or Cursor: by URL, by shell upload, or with an upload link.

Your agent can bring your files into a FLORA workspace so you can put them on a canvas, use them as references, or feed them to a Technique. [FLORA MCP](https://flora.ai/mcp) offers three ways in, and the right one depends on where the file is and whether your client has a shell. Uploading doesn't cost anything; only the generations you run afterwards do.

**Works in:** Claude · Claude Code · ChatGPT · Codex · Cursor

**You need:** a connected client and a workspace

**Use when:** the image, clip, audio or PDF you want to work with isn't in FLORA yet.

**Don't use when:** the file is already a FLORA output (`https://media.flora.ai/…`) or a canvas node; use its URL or node ID directly.

## Ask your agent

> Upload these product photos to FLORA, put them on my "Fall PDP" canvas, then make a 9:16 version of each one. Show me the cost first and wait for my OK before generating anything.

<a href="https://claude.ai/new?q=Upload%20these%20product%20photos%20to%20FLORA%2C%20put%20them%20on%20my%20%22Fall%20PDP%22%20canvas%2C%20then%20make%20a%209%3A16%20version%20of%20each%20one.%20Show%20me%20the%20cost%20first%20and%20wait%20for%20my%20OK%20before%20generating%20anything." class="button primary">Open in Claude</a> <a href="https://chatgpt.com/?q=Upload%20these%20product%20photos%20to%20FLORA%2C%20put%20them%20on%20my%20%22Fall%20PDP%22%20canvas%2C%20then%20make%20a%209%3A16%20version%20of%20each%20one.%20Show%20me%20the%20cost%20first%20and%20wait%20for%20my%20OK%20before%20generating%20anything." class="button secondary">Open in ChatGPT</a>

## Pick a path

| Where the file is                                              | Path                                                   | Works in                   |
| -------------------------------------------------------------- | ------------------------------------------------------ | -------------------------- |
| A public HTTPS link, or a file you shared in Claude or ChatGPT | [1. HTTPS URL](#path-1-an-https-url)                   | Every client               |
| On your computer, and the agent has a shell                    | [2. Shell upload](#path-2-shell-upload)                | Claude Code, Codex, Cursor |
| On your computer, and the agent has no shell                   | [3. Upload link](#path-3-upload-link-for-chat-clients) | Claude, ChatGPT            |

Files you attach in Claude or ChatGPT are already hosted (on `uploads.anthropic.com`, `claude.ai`, `files.openai.com` or `cdn.openai.com`), so path 1 works for them. So do FLORA's own hosts, `media.flora.ai` and `ik.imagekit.io`. Whether the agent can see an attachment's link depends on the client: if the link is visible to it, the agent passes it as `source`; if it only sees the image itself, with no link, it uses the [upload link](#path-3-upload-link-for-chat-clients) instead.

{% hint style="warning" %}
Never paste a file as base64, and never upload from a code sandbox (it has no outbound network). Use one of the three paths.
{% endhint %}

Every `flora_create_asset` call needs `workspace_id`, even when you also pass `project_id`: the workspace owns the asset, and the project is the canvas it lands on. The project must be in that workspace, or the call is refused. Get the ID from `flora_list_workspaces`, or from `flora_get_project`, which returns the project's `workspace_id`.

### Path 1: an HTTPS URL

`flora_create_asset` with `source` set to the URL. FLORA fetches the file on its side (up to 50 MB):

```json
{
  "workspace_id": "ws_123",
  "project_id": "prj_abc",
  "source": "https://cdn.example.com/products/mug-front.jpg"
}
```

The response has `asset_id`, `url` and `status`. A successful fetch returns `status: "ready"`, and the asset is done; no completion step is needed. With `project_id`, the file is also placed on that project's canvas.

The URL must serve the file itself, not a web page or player that shows it. If the host answers FLORA with 401, 403 or 404 (login walls, hotlink protection), don't retry the URL: download the file and use path 2 or 3.

### Path 2: shell upload

For a file on disk, reserve an upload slot with `source: "signed-url"`. `file_name` and `content_type` are required:

```json
{
  "workspace_id": "ws_123",
  "project_id": "prj_abc",
  "source": "signed-url",
  "file_name": "mug-front.png",
  "content_type": "image/png"
}
```

The response carries an `upload` descriptor (`url`, `form_fields`, `file_field`), an `upload_page` link and an `expires_at`. Send a multipart POST to `upload.url` with every `form_fields` entry exactly as given, then the file in `file_field`, last. The endpoint and field names depend on the storage backend, so read them from the descriptor instead of hardcoding them. With the response saved as `asset.json`:

```bash
url=$(jq -r '.upload.url' asset.json)
file_field=$(jq -r '.upload.file_field' asset.json)

args=()
while IFS= read -r pair; do
  args+=(--form-string "$pair")
done < <(jq -r '.upload.form_fields | to_entries[] | "\(.key)=\(.value)"' asset.json)

# Form fields first, the file last, with the content_type you reserved.
curl -sS -X POST "$url" "${args[@]}" -F "${file_field}=@./mug-front.png;type=image/png"
```

`--form-string` sends each value verbatim, even one that starts with `@` or `<`.

No `jq` or `curl`? The same upload with the Python standard library (Python 3.7 or later, which keeps the field order):

```python
import json, urllib.request, uuid
from pathlib import Path

upload = json.loads(Path("asset.json").read_text())["upload"]
path, content_type = Path("mug-front.png"), "image/png"  # the content_type you reserved

boundary = uuid.uuid4().hex
body = b""
for name, value in upload["form_fields"].items():  # every field, verbatim, in order
    body += (f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n'
             f"{value}\r\n").encode()
body += (f'--{boundary}\r\nContent-Disposition: form-data; name="{upload["file_field"]}"; '
         f'filename="{path.name}"\r\nContent-Type: {content_type}\r\n\r\n').encode()
body += path.read_bytes() + f"\r\n--{boundary}--\r\n".encode()  # the file goes last

request = urllib.request.Request(
    upload["url"], data=body, method="POST",
    headers={"Content-Type": f"multipart/form-data; boundary={boundary}"},
)
with urllib.request.urlopen(request) as response:
    print(response.status)  # any 2xx means the storage accepted the file
```

Then finish the asset with `flora_complete_asset`:

```json
{
  "asset_id": "asset_abc123"
}
```

It returns the final `url` and `status: "ready"`, or `failed` with a `failure_message`. If the reservation had a `project_id`, completing it places the file on that project's canvas. Signed uploads take files up to 350 MB.

### Path 3: upload link for chat clients

Claude and ChatGPT can't POST files. The agent reserves the slot exactly as in path 2, then gives you the `upload_page` link from the response. Open it in your browser, choose the file and submit. The link is valid for 15 minutes. When you say it's done, the agent calls `flora_complete_asset` with the `asset_id`.

If `flora_complete_asset` reports the bytes missing (you didn't submit, or the link expired), the agent calls `flora_create_asset` again for a fresh link instead of reusing the old one.

## Upload many files at once

Pass `sources` instead of `source`: up to 25 entries in one call. `workspace_id`, `project_id` and `folder` apply to every entry, and each entry can mix URLs and signed-url slots.

```json
{
  "workspace_id": "ws_123",
  "project_id": "prj_abc",
  "folder": "Fall PDP",
  "sources": [
    { "source": "https://cdn.example.com/products/mug-front.jpg" },
    { "source": "signed-url", "file_name": "mug-side.png", "content_type": "image/png" },
    { "source": "signed-url", "file_name": "mug-top.png", "content_type": "image/png" }
  ]
}
```

The response is `{ "assets": [...] }`: each entry echoes its `index` and `source`, and has either the create response (with its own `upload` descriptor and `upload_page` for signed-url entries) or `ok: false` with an `error`. One failed entry never stops the rest. Finish all the signed-url entries in one call:

```json
{
  "asset_ids": ["asset_side123", "asset_top456"]
}
```

One `sources` call is better than many parallel calls: every call counts against the workspace's shared rate limit.

## Allowed file types

| Kind     | MIME types                                                                                     |
| -------- | ---------------------------------------------------------------------------------------------- |
| Image    | `image/jpeg`, `image/png`, `image/webp`, `image/gif`, `image/avif`, `image/heic`, `image/heif` |
| Video    | `video/mp4`, `video/webm`, `video/quicktime`                                                   |
| Audio    | `audio/mpeg`, `audio/wav`, `audio/ogg`, `audio/mp4`                                            |
| Document | `application/pdf`                                                                              |

`image/jpg`, `audio/x-m4a` and `application/x-pdf` are accepted and normalized onto the list. Anything else is rejected: rasterize SVG to PNG first. 3D models don't upload here; use the 3D flows in [generate](/mcp/recipes/generate.md#video-3d-audio-and-text).

## Use what you uploaded

* **Put it on a canvas.** Pass `project_id` to `flora_create_asset` and the file lands on that project's canvas as a node: right away for a URL source, and when `flora_complete_asset` finishes a signed upload (single or batch). Placement is best effort, so the upload still succeeds if the canvas write fails; check with `flora_get_canvas`, where the node's ID starts with `mcp_upload_`.

  Use `flora_attach_asset` only for an asset created without `project_id`, or to put it on another project in the same workspace. The asset must be `ready`:

  ```json
  {
    "asset_id": "asset_abc123",
    "project_id": "prj_abc"
  }
  ```

  It returns a `node_id` (it starts with `api_asset_`) and the `canvas_url`. Calling it for an asset that's already on that canvas adds a second node. Either node ID works with [`flora_arrange_canvas`](/mcp/recipes/canvas.md) and in `reference_node_ids`.
* **Use it as a reference.** Pass the file's node ID (`mcp_upload_…` or `api_asset_…`) in `reference_node_ids`, or the asset's `url` in the model's `image_url` / `image_urls` field. See [Generate from reference images](/mcp/recipes/generate.md#generate-from-reference-images).
* **Feed it to a Technique.** Pass the asset's `url` as the value of a media input. See [Run your team's Technique](/mcp/recipes/run-a-technique.md).
* **Animate it.** Put the `url` in a video model's `image_url`. See [Turn a product photo into video](/mcp/recipes/image-to-video.md).

`flora_list_assets` lists a workspace's or project's assets, and `flora_get_asset` reads one (with `failure_message` if its upload didn't finish).

## Check it worked

* The asset reports `status: "ready"` and has a `url`.
* If you passed `project_id` (or called `flora_attach_asset`), the file shows up as one node on that project's canvas. `flora_get_canvas` lists it.

## If it fails

| Symptom                                              | Cause                                                                                        | Fix                                                                                                                                |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| URL source fails with 401, 403 or 404                | The host blocks FLORA's anonymous fetch                                                      | Don't retry the URL. Download the file and use path 2 or 3.                                                                        |
| URL source rejected as not media                     | The link is a web page or player, not the file                                               | Use the direct file URL, or upload the file.                                                                                       |
| Rejected content type                                | The file isn't on the allowed list (SVG, for example)                                        | Convert it (SVG to PNG) and try again.                                                                                             |
| `flora_complete_asset` says the bytes are missing    | The upload never happened, or the 15-minute link expired                                     | Call `flora_create_asset` again for a fresh slot and link.                                                                         |
| Upload POST rejected by storage                      | A form field was changed, reordered after the file, or left out                              | Send every `form_fields` entry verbatim, file last.                                                                                |
| `flora_attach_asset` rejected                        | The asset is still `pending_upload` or `failed`, or belongs to another workspace             | Complete the upload, check `flora_get_asset`, and use a project in the same workspace.                                             |
| Uploaded with `project_id` but no node on the canvas | Canvas placement is best effort and the write failed, or a signed upload was never completed | Complete it with `flora_complete_asset`, then check `flora_get_canvas`. If it's still missing, place it with `flora_attach_asset`. |
| The same file shows up twice on the canvas           | `flora_attach_asset` was called for an asset already placed with `project_id`                | Remove the extra node with `flora_remove_from_canvas`, if you asked for that.                                                      |
| A local path or `file://` URL fails                  | FLORA can't reach your computer                                                              | Use path 2 or 3.                                                                                                                   |

Every argument is in the [asset tools reference](/mcp/tools/assets.md#flora_create_asset).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.flora.ai/mcp/recipes/upload-files.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
