Skip to content
FLORA DocsGo to app

Upload an asset

Upload a local image and use it as a Technique input.

import { Steps, Step } 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. The upload is a three-step dance: reserve, upload, complete.

What you’ll build: a function that takes a file path, uploads it, and returns a permanent FLORA URL ready to use as a Technique input.

`POST /assets` returns a signed URL you upload bytes to. PUT or POST the file to the signed URL (no FLORA auth header on this call). `POST /assets/{id}/complete` finalizes the asset. `status` becomes `ready` and `url` is the public, long-lived URL.
import Flora from '@flora-ai/flora';
import fs from 'node:fs';
const client = new Flora({ apiKey: process.env.FLORA_API_KEY });
async function uploadAsset(filePath: string, workspaceId: string): Promise<string> {
// 1. Reserve
const asset = await client.assets.create({
source: 'signed-url',
workspace_id: workspaceId,
file_name: filePath.split('/').pop()!,
content_type: mimeFor(filePath),
folder: 'api-uploads',
});
// 2. Upload bytes (no FLORA auth on this request)
const fileBuffer = await fs.promises.readFile(filePath);
const formData = new FormData();
for (const [k, v] of Object.entries(asset.upload.formFields)) {
formData.append(k, v as string);
}
formData.append(asset.upload.fileField, new Blob([fileBuffer]));
const uploadRes = await fetch(asset.upload.url, { method: 'POST', body: formData });
if (!uploadRes.ok) throw new Error(`upload failed: ${uploadRes.status}`);
// 3. Complete
const completed = await client.assets.complete(asset.asset_id);
if (completed.status !== 'ready') {
throw new Error(`asset not ready: ${completed.status}`);
}
return completed.url;
}
function mimeFor(filePath: string): string {
const ext = filePath.split('.').pop()?.toLowerCase();
return {
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
webp: 'image/webp',
mp4: 'video/mp4',
}[ext ?? ''] ?? 'application/octet-stream';
}
// Use it
const imageUrl = await uploadAsset('./photo.png', 'ws_XXXX');
const run = await client.techniques.runs.create('portrait-enhancer', {
inputs: [
{ id: 'input_image', type: 'imageUrl', value: imageUrl },
],
mode: 'async',
});
func uploadAsset(ctx context.Context, c *flora.Client, filePath, workspaceID string) (string, error) {
asset, err := c.Assets.New(ctx, flora.AssetNewParams{
Source: param.Field(flora.AssetNewParamsSourceSignedURL),
WorkspaceID: param.Field(workspaceID),
FileName: param.Field(filepath.Base(filePath)),
ContentType: param.Field(mimeFor(filePath)),
Folder: param.Field("api-uploads"),
})
if err != nil {
return "", err
}
// Upload bytes via multipart POST to asset.Upload.URL
if err := uploadBytes(asset.Upload.URL, asset.Upload.FormFields, asset.Upload.FileField, filePath); err != nil {
return "", err
}
completed, err := c.Assets.Complete(ctx, asset.AssetID)
if err != nil {
return "", err
}
if completed.Status != "ready" {
return "", fmt.Errorf("asset not ready: %s", completed.Status)
}
return completed.URL, nil
}
#!/usr/bin/env bash
set -euo pipefail
FILE="./photo.png"
WORKSPACE_ID="ws_XXXX"
# 1. Reserve
RESERVE=$(flora assets create \
--source signed-url \
--workspace-id "$WORKSPACE_ID" \
--file-name "$(basename "$FILE")" \
--content-type image/png \
--folder api-uploads)
ASSET_ID=$(echo "$RESERVE" | jq -r '.asset_id')
UPLOAD_URL=$(echo "$RESERVE" | jq -r '.upload.url')
FILE_FIELD=$(echo "$RESERVE" | jq -r '.upload.fileField')
# 2. Upload via curl (no FLORA auth header here)
FORM_ARGS=$(echo "$RESERVE" | jq -r '.upload.formFields | to_entries[] | "-F \(.key)=\(.value)"')
eval curl -s -o /dev/null -w '%{http_code}' \
$FORM_ARGS \
-F "$FILE_FIELD=@$FILE" \
"$UPLOAD_URL"
# 3. Complete
flora assets complete --asset-id "$ASSET_ID"
# 4. Use the asset
FINAL_URL=$(flora assets retrieve --asset-id "$ASSET_ID" --jq '.url' -r)
echo "asset URL: $FINAL_URL"

If the signed URL expires before you finish uploading, ask for a new one:

try {
await fetch(asset.upload.url, { method: 'POST', body: formData });
} catch (err) {
const retry = await client.assets.retry(asset.asset_id);
// retry.upload.url is a fresh signed URL — try again
}

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

await client.projects.assets.attach(projectId, asset.asset_id);

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',
});
  • 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.