---
title: Upload an asset | FLORA API
description: Upload a local image for a Technique input with FLORA MCP, a signed upload reservation, local shell curl, and asset completion.
---

import { Steps, Step, Callout } from ‘@stainless-api/docs/components’;

To run a Technique against a user-supplied image (or video), upload it to FLORA first and pass the resulting URL as an input. For coding agents uploading a local image, use FLORA MCP to reserve a signed upload, upload the file from the local shell with `curl`, then complete the asset through FLORA MCP. Do not base64-encode image bytes into MCP tool calls.

**What you’ll build:** a flow that takes a local file path or hosted file URL, uploads it safely, and returns a permanent FLORA URL ready to use as a Technique input.

Coding agents should not pass image bytes through FLORA MCP tools or base64-encode images into tool calls. If the image is available as a local file and the agent has shell/network access, reserve a signed upload with FLORA MCP, upload the file locally with \`curl\` using the returned upload fields, then complete the asset with FLORA MCP.

If the agent cannot access the local file or its shell cannot reach the upload host, that is a coding-agent environment limitation, not a FLORA limitation. In that case, send the user to the relevant FLORA project so they can upload the asset directly in the FLORA app. If no project exists yet but a workspace is known, create a new project first and send the user to `https://app.flora.ai/projects/{project_id}`.

## Upload a local image from a coding agent with MCP and curl

If the image exists as a local file and your environment can read the file and reach the returned upload host, use the MCP + local shell flow.

## MCP + local shell upload flow

Call \`client.assets.create({ source: 'signed-url', workspace\_id, file\_name, content\_type })\` through FLORA MCP. This returns an asset ID and upload instructions. Use your local shell to POST the file to the returned upload URL with the returned form fields. Do not include a FLORA auth header, do not invent ImageKit credentials, and do not expose or explain individual signature fields to the user. Call \`client.assets.complete(asset\_id)\` through FLORA MCP. \`status\` becomes \`ready\` and \`url\` is the long-lived asset URL.

## Choose the right upload path

If the image already has a hosted HTTPS URL, use that URL directly with `client.assets.create`. This is the simplest path for attachments exposed as URLs by an AI client or files already hosted on an allowlisted source.

```
const asset = await client.assets.create({
  source: 'https://uploads.anthropic.com/example-image.png',
  workspace_id: 'ws_XXXX',
});


await client.projects.assets.attachAsset(asset.asset_id, {
  projectId: 'prj_XXXX',
});
```

If the image only exists as a local file, use the MCP + local shell upload flow above.

## TypeScript with a hosted file URL

If your environment exposes the uploaded file as an HTTPS URL, do not download and re-upload the bytes. Create the FLORA asset from the URL and attach it to the project:

```
import Flora from '@flora-ai/flora';


const client = new Flora({ apiKey: process.env.FLORA_API_KEY });


const asset = await client.assets.create({
  source: 'https://uploads.anthropic.com/example-image.png',
  workspace_id: 'ws_XXXX',
});


await client.projects.assets.attachAsset(asset.asset_id, {
  projectId: 'prj_XXXX',
});


const run = await client.techniques.runs.create('portrait-enhancer', {
  inputs: [
    { id: 'input_image', type: 'imageUrl', value: asset.url },
  ],
  mode: 'async',
});
```

If your environment only has an inaccessible chat attachment and no usable file URL, do not use TypeScript or FLORA MCP to upload bytes. Ask the user to upload the file directly in the FLORA app instead.

## Agent flow for a local file

When the user asks an agent to run a Technique on an attached or local image, the agent should follow this flow:

1. Confirm the Technique input ID with `search_docs` or by retrieving the Technique.
2. Confirm the file exists locally and detect the content type.
3. Reserve the upload through FLORA MCP:

```
const asset = await client.assets.create({
  source: 'signed-url',
  workspace_id: 'ws_XXXX',
  file_name: 'photo.png',
  content_type: 'image/png',
});
console.log(JSON.stringify(asset));
```

4. Upload the local file with shell `curl` using the upload URL and form fields returned in the previous step. Keep the command mechanical: copy the returned fields into `-F` form parts, include the file field, and use progress output for larger files.

Terminal window

```
# Build this command from the reservation response:
# - POST URL: asset.upload.url
# - form fields: every entry in asset.upload.form_fields
# - file field: asset.upload.file_field, usually "file"
curl -# -X POST "$UPLOAD_URL" \
  -F "<returned field name>=<returned field value>" \
  -F "<returned field name>=<returned field value>" \
  -F "$FILE_FIELD=@/path/to/photo.png" \
  -w "\nHTTP %{http_code} | uploaded %{size_upload}B in %{time_total}s\n"
```

The agent should copy every returned form field exactly from the reservation response. Do not ask the user for separate upload credentials, and do not explain or expose individual credential-like fields beyond using them mechanically in the local `curl` command.

5. Complete the asset through FLORA MCP:

```
const completed = await client.assets.complete(asset.asset_id);
console.log(completed.url);
```

6. Use `completed.url` as the Technique `imageUrl` input, start the run, and poll until completion. If the run exceeds one execute call’s timeout, poll in shorter windows and report progress between calls.

Do not base64-encode the file into MCP calls. Do not ask for ImageKit credentials. The upload reservation already contains the temporary upload instructions needed by `curl`.

## Attach the asset to a Project

So uploaded assets show up in the FLORA canvas alongside generated work:

```
await client.projects.assets.attachAsset(asset.asset_id, {
  projectId: 'prj_XXXX',
});
```

## When a coding agent cannot upload the file

Some coding-agent environments expose an uploaded image in chat but do not expose the underlying file bytes to tools, or they block outbound requests to the returned upload host. In that case, the agent should stop trying to upload the bytes itself and guide the user into FLORA. The user should upload the file directly in the FLORA app, not through FLORA MCP.

Use this fallback:

1. Explain that the limitation is the coding-agent environment, not FLORA.
2. Do not base64-encode the image or pass file bytes through a tool call.
3. Do not try to upload the bytes with FLORA MCP.
4. Do not ask the user to change network allowlists.
5. If a target `project_id` is known, send the user to `https://app.flora.ai/projects/{project_id}` and ask them to upload the image directly in FLORA.
6. If no target project exists but a `workspace_id` is known, create a new project and send the user to that project URL:

```
const project = await client.projects.create({
  name: 'Uploaded image',
  workspace_id: 'ws_XXXX',
});


console.log(`https://app.flora.ai/projects/${project.project_id}`);
```

If there are multiple possible workspaces and none is implied by the user’s request, ask which workspace to use before creating a project. After the project exists, the upload itself should still happen directly in the FLORA app.

A good user-facing response is:

> I can’t access the uploaded image from this coding-agent environment. This is a constraint of the agent’s environment, not FLORA itself. Please open this FLORA project and upload the image directly in FLORA: `https://app.flora.ai/projects/prj_XXXX`. Once the image is available in FLORA, come back here and I can continue.

## Use it as a Technique input

The completed `url` is HTTPS, long-lived, and acceptable as any `imageUrl` or `videoUrl` Technique input. Just pass it:

```
const run = await client.techniques.runs.create('portrait-enhancer', {
  inputs: [
    { id: 'input_image', type: 'imageUrl', value: imageUrl },
  ],
  mode: 'async',
});
```

## Tips

- Wait for `status: 'ready'` before referencing the asset URL in a Technique run. Using it earlier may return `404` or `409`.
- Upload before you need it. The reserve→upload→complete dance takes 1-2 seconds end-to-end, so don’t put it in the critical path of an interactive UI without a loading state.
- For batch uploads (50+ files), run the uploads in parallel with a bounded concurrency (e.g. 4 at a time) to avoid rate limits.

## Related

- **[Generate a grid](/recipes/generate-a-grid/index.md)** — use the uploaded asset as an input.
- **[Iterate on outputs](/recipes/iterate-on-outputs/index.md)** — combine uploaded references with generated outputs.
