--- title: Upload an asset | FLORA API description: 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 \`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. ## 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 { // 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', }); ``` ## Go ``` 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 } ``` ## CLI ``` #!/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" ``` ## 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 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 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.