--- title: Errors | FLORA API description: 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 shape ``` { "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":"..."}} ``` ## All error codes ### `400 invalid_json` 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. ### `400 input_validation_error` 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 } } ``` ### `401 unauthorized` No `Authorization` header was sent. **Fix:** Include `Authorization: Bearer sk_live_XXXX` on every request. ### `401 invalid_api_key` 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**. ### `401 invalid_token` (MCP only) The OAuth token expired and couldn’t refresh, or was revoked from FLORA → Connected apps. **Fix:** Reconnect the MCP client. See [MCP troubleshooting](/mcp/troubleshooting/index.md). ### `402 insufficient_credits` 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. ### `403 forbidden` 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`. ### `404 not_found` 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. ### `409 conflict` 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. ### `422 idempotency_conflict` 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](/platform/idempotency/index.md). ### `429 rate_limited` 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 ``` ### `500 internal_error` Something broke on our side. The request didn’t complete. **Fix:** Retry with backoff. If it persists, send the `request-id` to support. ### `503 service_unavailable` The API is overloaded or down for maintenance. **Fix:** Retry with exponential backoff. Check our status page if reachable. ## Run-level errors 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: | Code | Meaning | | ------------------- | ----------------------------------------------------------------- | | `model_timeout` | The model didn’t return in time. Usually transient — retry. | | `safety_blocked` | The model refused the request on safety grounds. Adjust prompts. | | `input_unreachable` | A URL input (image/video) couldn’t be fetched. Check the URL. | | `internal_error` | Generic execution failure. Retry; if persistent, contact support. | Always handle `status === 'failed'` when polling — the run won’t auto-retry. ## Recovery patterns ### Retry on transient errors ``` async function withRetry(fn: () => Promise, max = 3): Promise { 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; } ``` ### Don’t retry user errors `400`, `401`, `403`, `404`, `409`, `422` are caller mistakes. Retrying them produces the same error — surface them to the user/log instead. ## Contacting support 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. ## Related - **[Authentication](/platform/authentication/index.md)** — 401 errors and key management. - **[Idempotency](/platform/idempotency/index.md)** — safe retry semantics.