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.
The flow
Section titled “The flow”TypeScript
Section titled “TypeScript”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 itconst 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 bashset -euo pipefail
FILE="./photo.png"WORKSPACE_ID="ws_XXXX"
# 1. ReserveRESERVE=$(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. Completeflora assets complete --asset-id "$ASSET_ID"
# 4. Use the assetFINAL_URL=$(flora assets retrieve --asset-id "$ASSET_ID" --jq '.url' -r)echo "asset URL: $FINAL_URL"Retry on expired uploads
Section titled “Retry on expired uploads”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}Attach the asset to a Project
Section titled “Attach the asset to a Project”So uploaded assets show up in the FLORA canvas alongside generated work:
await client.projects.assets.attach(projectId, asset.asset_id);Use it as a Technique input
Section titled “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',});- Wait for
status: 'ready'before referencing the asset URL in a Technique run. Using it earlier may return404or409. - 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
Section titled “Related”- Generate a grid — use the uploaded asset as an input.
- Iterate on outputs — combine uploaded references with generated outputs.