Skip to content
FLORA DocsGo to app
Platform

Errors

Every FLORA API error code, what it means, and how to recover.

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

{
"error": {
"code": "input_validation_error",
"message": "Input 'prompt' is required but was not provided."
}
}

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, or doesn’t match the Technique’s input schema.

Fix: Call GET /api/v1/techniques/{slug} first and match each input id and type exactly. The message field tells you which input failed.

try {
await client.techniques.runs.create('thumbnail-v3', { inputs: [], mode: 'async' });
} catch (err) {
if (err instanceof APIError && err.code === 'input_validation_error') {
// Re-fetch the schema and rebuild inputs
}
}

No Authorization header was sent.

Fix: Include Authorization: Bearer sk_live_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 has run out of credits and can’t start the run.

Fix: Top up in Settings → Billing, then retry. The chargedCost from previous completed runs and the runCost 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 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.

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.headers['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:

{
"runId": "run_abc...",
"status": "failed",
"errorCode": "model_timeout",
"errorMessage": "Underlying model exceeded the 60s budget"
}

Common errorCode 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 APIError) {
if ([429, 500, 503].includes(err.status)) {
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 runId).
  • 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.