---
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": "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":"..."}}
```

## 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, 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.

```
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 balance (USD) is too low to start the run.

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

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

Common `error_code` 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<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;
}
```

### 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 `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.

## Related

- **[Authentication](/platform/authentication/index.md)** — 401 errors and key management.
- **[Idempotency](/platform/idempotency/index.md)** — safe retry semantics.
