Skip to content

Errors

FLORA returns conventional HTTP status codes and a structured error body on every non-2xx response.

{
"error": {
"code": "input_validation_error",
"message": "prompt: prompt is required.",
"fields": [{ "field": "prompt", "message": "prompt is required." }]
}
}

For validation errors, fields lists each rejected field when field-level details are available.

Every response also includes a request-id header. Always capture it — support needs it to investigate.

Terminal window
curl -i https://app.flora.ai/api/v1/techniques/thumbnail-v3/runs \
-H "Authorization: Bearer $FLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"inputs": [], "mode": "async"}'
# < HTTP/2 400
# < request-id: req_abc123...
# < content-type: application/json
#
# {"error":{"code":"input_validation_error","message":"..."}}

The request body wasn’t valid JSON.

Fix: Validate JSON before sending. If using a SDK, this almost always indicates a serialization bug in your code — not a server issue.

The request body parsed, but a field is missing, has the wrong type, uses the wrong public ID prefix, or doesn’t match the Technique’s input schema.

Fix: Use the fields array and message to correct the payload. Common fixes:

  • Use public API IDs with the expected prefixes: ws_ for workspaces, prj_ for projects, tech_ for techniques, run_ for runs.
  • For generations, use type: "image" | "video" | "audio" | "text"; don’t use t2i, i2v, or text-to-image as type.
  • Pass model as a model endpoint ID string from GET /api/v1/models or MCP list_models.
  • For asset uploads, pass source as a string: "signed-url" or an allowlisted HTTPS URL.
  • For technique runs by slug, call GET /api/v1/techniques/{slug} first and match each input id and type exactly.
import { Flora, FloraError } from "@flora-ai/flora"
// The API error envelope rides on `err.body`
const errorCode = (err: FloraError) =>
(err.body as { error?: { code?: string } } | undefined)?.error?.code
try {
await client.techniques.runs.create({
techniqueId: "thumbnail-v3",
inputs: [],
mode: "async",
})
} catch (err) {
if (err instanceof Flora.BadRequestError && errorCode(err) === "input_validation_error") {
// Re-fetch the schema and rebuild inputs
}
}

No Authorization header was sent.

Fix: Include Authorization: Bearer ak_XXXX on every request.

The API key is malformed, revoked, or doesn’t exist.

Fix: Check the key value (no whitespace, no Bearer prefix duplicated, correct env var). If the key was revoked, create a new one in Settings → API Keys.

The OAuth token expired and couldn’t refresh, or was revoked from FLORA → Connected apps.

Fix: Reconnect the MCP client. See MCP troubleshooting.

The workspace balance (USD) is too low to start the run.

Fix: The workspace balance is a limit of its current plan; see About FLORA credits. The charged_cost from previous completed runs and the run_cost on GET /techniques/{slug} show your burn rate.

The key is valid but lacks permission for this operation. Common cases:

  • Creating a Project with a read-only key.
  • Accessing a workspace your account was removed from.
  • Calling a Technique that was archived.

Fix: Check the FLORA app to confirm the user/key role. If the role looks right, contact support with the request-id.

The operation needs a feature the workspace plan does not include — for example sharing a project for editing (access: "edit" on POST /projects/{project_id}/share).

Fix: Use the variant the workspace plan allows (share for viewing instead), or ask a workspace administrator about the plan. See About FLORA credits.

The resource ID, Technique slug, or run ID doesn’t exist (or doesn’t belong to your workspace).

Fix:

  • For Techniques: verify the slug. The URL https://app.flora.ai/techniques/{slug} is authoritative.
  • For runs: the run ID was created in a different workspace, or you typo’d it.
  • For assets/projects: confirm the ID by listing the parent collection first.

The resource is in a state that doesn’t allow this operation. Examples: completing an asset that’s already complete, retrying an asset that didn’t fail.

Fix: Inspect the resource first (GET /assets/{id}) and only call the action if the current state allows it.

The brand exists and you can read it, but nothing is published yet. Agents read published releases only, never drafts. The message names the brand, its review counts and the page where a person publishes it.

Fix: Relay the message to the user and retry after they publish. Do not substitute another brand without asking.

You sent the same idempotency_key with a different request body. Idempotency keys must be paired with identical bodies.

Fix: Either reuse the original body (and get the cached response) or choose a new idempotency key. See Idempotency.

You’ve exceeded the per-workspace request rate.

Fix:

  • Back off with exponential delay before retrying.
  • Reduce parallelism in batch jobs.
  • The retry-after response header (when present) is the recommended wait in seconds.
const retryAfter = parseInt(err.rawResponse?.headers.get("retry-after") ?? "5", 10)
await new Promise((r) => setTimeout(r, retryAfter * 1000))
// retry

Something broke on our side. The request didn’t complete.

Fix: Retry with backoff. If it persists, send the request-id to support.

The API is overloaded or down for maintenance.

Fix: Retry with exponential backoff. Check our status page if reachable.

A run that starts successfully can still fail during execution. The HTTP request returns 200, but the run status becomes failed:

{
"run_id": "run_abc...",
"status": "failed",
"error_code": "model_timeout",
"error_message": "Underlying model exceeded the 60s budget"
}

Common error_code values:

CodeMeaning
model_timeoutThe model didn’t return in time. Usually transient — retry.
safety_blockedThe model refused the request on safety grounds. Adjust prompts.
input_unreachableA URL input (image/video) couldn’t be fetched. Check the URL.
internal_errorGeneric execution failure. Retry; if persistent, contact support.

Always handle status === 'failed' when polling — the run won’t auto-retry.

async function withRetry<T>(fn: () => Promise<T>, max = 3): Promise<T> {
let lastErr: unknown
for (let attempt = 0; attempt < max; attempt++) {
try {
return await fn()
} catch (err) {
if (err instanceof FloraError) {
if ([429, 500, 503].includes(err.statusCode ?? 0)) {
lastErr = err
const delay = Math.min(1000 * 2 ** attempt, 30_000)
await new Promise((r) => setTimeout(r, delay))
continue
}
}
throw err
}
}
throw lastErr
}

400, 401, 403, 404, 409, 422 are caller mistakes. Retrying them produces the same error — surface them to the user/log instead.

When you open a ticket, include:

  • The request-id from response headers (or the failing run’s run_id).
  • The endpoint and HTTP method.
  • The error code and message.
  • Approximate UTC timestamp.
  • Your workspace ID.

The more of these we have, the faster we can diagnose. Without request-id we usually can’t find the request at all.