> ## Documentation Index
> Fetch the complete documentation index at: https://www.krea.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and debugging

> HTTP errors and failed-job codes for the Krea API: response shapes, status codes, concurrency 429s, and what to send support.

A call can fail in two places. The request can be rejected immediately with an HTTP error, or it can be accepted and later finish with `status: "failed"`. Failed and cancelled jobs are not billed. You pay for completed jobs only. In a node app, the app's own job is never billed. Each generation or LLM node is charged when its own job completes. A failure does not cancel parallel nodes or other branches, so a node that completes after the app has already failed is charged too, and those charges are not refunded. A node that fails, is cancelled, or never starts is not charged. See [API keys and billing](https://www.krea.ai/docs/developers/api-keys-and-billing.md).

## Request-time errors and failed jobs

**Request-time errors** return a `4xx` or `5xx` status. No job is created. Read the body as described below.

**Failed jobs** return a success status at submit time. Poll [GET `/jobs/{id}`](https://www.krea.ai/docs/developers/job-lifecycle) or wait for a [webhook](https://www.krea.ai/docs/developers/webhooks.md). When the job finishes, `status` is `"failed"` and the failure is the top-level `error` object. Do not read `result` on a failed job: it is left out (or `null`). It holds the output only when `status` is `"completed"`. A `cancelled` job has neither `result` nor `error`.

Branch on `error.code`. `message` can be omitted, and for `provider_error` and `internal` it is free-form provider text. Do not parse `message`.

```javascript theme={"theme":{"light":"github-light-default","dark":"vesper"}}
if (job.status === "failed") {
  switch (job.error?.code ?? "internal") {
    case "content_policy":
    case "model_unavailable":
      // Retrying the same request will most likely fail again. Change the input or the model.
      break;
    case "insufficient_balance":
      // Top up the API balance, then submit again.
      break;
    default:
      // timeout, auto_cancelled, provider_error, internal, node_app_node_failed,
      // or an unknown code: submit again with backoff.
  }
}
```

## Response bodies

On generate, jobs, assets, styles, and node apps, read `message ?? error` when the body is JSON. One of those two fields carries the summary. Some `5xx` bodies are not JSON, including a `502` whose body is the plain text `error code: 502`. Parse defensively: if the body is not JSON, branch on the HTTP status.

Most failures use `{ "message": "..." }`, sometimes with extra fields (`limit`, `jobType`, `modelAccessDenial`). Invalid JSON and a few handlers below use `{ "error": "..." }` instead.

MCP endpoints such as `/mcp` return JSON-RPC errors, `{ "jsonrpc", "id", "error": { "code", "message" } }`, not the HTTP bodies on this page.

### Send `Accept: application/json`

> [!WARNING]
>
> Without `Accept: application/json`, some errors come back as an HTML page and a JSON parser will fail.

`{ "message" }` errors, including `401` and `404`, are JSON when `Accept` is `application/json` or `*/*`. When `Accept` is missing, or is `text/html`, those statuses are an HTML page (`Content-Type: text/html`) and a JSON parser will fail.

This matters for clients that send no `Accept` header. Python's `urllib` is one of them. curl sends `Accept: */*` by default, and Node's `fetch` does too, so both receive JSON unless you override `Accept`.

Unauthenticated `POST /generate/...`, `GET /jobs/{id}`, `GET /styles`, and `GET /assets` return `401` `{"message":"Unauthorized"}` for `*/*` and `application/json`, and an HTML page for a missing `Accept` or `text/html`. An authenticated `GET /jobs/{id}` for an unknown id does the same with the status: `404` JSON `{"message":"Job not found or unauthorized"}` when `Accept` asks for JSON, and `404` `text/html` when `Accept` is missing.

Invalid JSON (`400`) and validation (`422`) use `{ "error" }` and stay JSON even when `Accept` is missing. The same is true of removed-field `422`, sunset `410`, the Zero Data Retention `402`, node-app `{ "error" }` responses, and the mapped [Agent operation API](#agent-operation-api) errors. `402` and `429` from admitting a `POST /generate/...` job are JSON as well, including for a browser `Accept`.

`413` and `500` use `{ "message" }` and follow `Accept`, like `401` and `404`.

### Validation (`422`)

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "error": "Validation failed",
  "details": [
    {
      "field": "width",
      "message": "Too small: expected number to be >0",
      "code": "too_small"
    },
    {
      "field": "",
      "message": "Unrecognized key: \"foo\"",
      "code": "unrecognized_keys"
    },
    {
      "field": "prompt",
      "message": "Enter a prompt.",
      "code": "custom"
    }
  ]
}
```

`details` lists every problem. `field` is the path joined with `.` (`"image_style_references.0.url"`). Object-level errors use an empty string, as with the unknown key above. Several unknown keys produce `Unrecognized keys: ...` and the same empty `field`. `expected` is added only when `code` is `"invalid_type"`.

An invalid `X-Webhook-URL` header uses this same `422`.

`GET /jobs` with invalid query parameters also returns this `422`. It is not a `400`.

A Zero Data Retention input check that fails inside the tool returns `422` with the explanation in `message` (the `{ "message" }` shape, so `Accept` matters). See [Zero Data Retention](https://www.krea.ai/docs/developers/zdr.md).

### Removed and deprecated fields (`422`)

The removed `batchSize` / `batch_size` field uses a different `422`. There is no `details` array:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "error": "invalid_request",
  "reason": "removed_field",
  "field": "batchSize",
  "message": "Generation batch size is no longer supported. Submit one request per output.",
  "value": 2
}
```

`batch_size` is removed the same way. `field` is whichever name you sent.

A deprecated field alias uses `reason` `"deprecated_field"` or `"conflicting_fields"`, plus `replacement`, `sunset_date`, and `migration_url` when they apply. Branch on `error` and `reason`. The sentence to show a person is `message`.

### Sunset models (`410`)

A model past its sunset date returns `410` with `Deprecation: true` and a `Sunset` header (an RFC 1123 date):

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "error": "model_unavailable",
  "reason": "deprecated",
  "message": "The model '<path>' was deprecated on <sunsetDate>. <reason> Use '<replacement>' instead.",
  "sunset_date": "YYYY-MM-DD",
  "replacement": "<optional>",
  "migration_url": "<optional>"
}
```

`error` is the stable value `"model_unavailable"`. `replacement` and `migration_url` are omitted when there is no recommended replacement. Retrying the same model will keep returning `410`. Switch to `replacement` when it is set, and see [Deprecations](https://www.krea.ai/docs/developers/deprecations.md).

A model that is deprecated but still live sends `Deprecation` and `Sunset` on every response, including errors. Using a deprecated field alias does the same, including on its own `422`. When a migration guide exists, a `Link: <migration_url>; rel="deprecation"` header is sent too.

## Status codes

| Status | Meaning |
| - | - |
| `400` | A generate body is not valid JSON, a node-app input URL could not be used, or asset or node-app parameters are invalid. Invalid JSON on node-app execute is `500`. |
| `401` | The credentials are missing or invalid. |
| `402` | The request was refused before a job started. See [402](#402-payment-required). |
| `403` | The caller is authenticated and is not allowed to use this resource. |
| `404` | No route matched, the model path is unknown or not available to you, or a job, asset, style, or node app is missing. |
| `405` | The HTTP method is not allowed for this path. |
| `410` | The model is sunset. See [Sunset models](#sunset-models-410). |
| `413` | The request body is too large. See [413](#413-payload-too-large). |
| `422` | Validation failed, a removed or deprecated field was sent, or an input was permanently rejected. |
| `429` | Over the concurrency cap and the job cannot be backlogged. The cap is approximate. |
| `500` | Unhandled server error. `{ "message": "Internal Error" }`. No exception text. Follows `Accept`. |
| `502`, `504` | Retryable. A `502` arrives as Cloudflare plain text `error code: 502`, not JSON, so branch on the status code. `504` may be plain text too. See [Input downloads](#input-downloads). |

### `400`

Invalid JSON on generate is always JSON, with no `Accept` header required:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "error": "Invalid JSON: Expected property name or '}' in JSON at position 1 (line 1 column 2)"
}
```

Node-app execute uses `{ "error" }` for an input URL it cannot use: `Invalid URL for input "<key>"`, or `Failed to download URL for <key>: …`. The download message covers temporary failures too, so unlike generate (see [Input downloads](#input-downloads)) a node-app download failure is always `400`. Retry it once if the URL is valid.

Invalid JSON on `POST /node-apps/{id}/execute` is not the generate `400` above. It becomes a `500` `{ "message": "Internal Error" }`, with no exception text, and it follows `Accept`.

Other `400` responses on assets and node apps are `{ "message": "..." }` with a short explanation, for example `No file provided` on a presigned upload or `Invalid pagination parameters` on node-app versions. A failed asset upload passes the upload service's status through (`400`, `401`, `404`, or `500`) and can place its response text in `message`. That text is sometimes a JSON string. Invalid query parameters on `GET /jobs` are [`422`](#validation-422), not `400`.

### `401`

Generate, jobs, assets, styles, and node apps:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{ "message": "Unauthorized" }
```

Presigned uploads return `{ "message": "Presigned upload URL expired" }` or `{ "message": "Invalid presigned upload URL" }`.

### `402` Payment required

For an API-billed client (an API key, or MCP using an API key) whose API balance does not cover the job:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "message": "Your API balance is separate from your workspace compute balance. Please top up your API balance to continue using the API."
}
```

Top up at [krea.ai/app/api/ ↗](https://krea.ai/app/api/). In-flight jobs finish. See [API keys and billing](https://www.krea.ai/docs/developers/api-keys-and-billing.md).

Other `402` `message` values on the public API:

| `message` | When |
| - | - |
| `This tool is not available on your current plan.` | Your plan or workspace allows no concurrent jobs for this model. |
| `MEMBER_USAGE_LIMIT_REACHED` | The plan enforces member usage limits and this member's budget would be exceeded. |
| `WORKSPACE_USAGE_LIMIT_REACHED` | The plan enforces member usage limits and the workspace budget would be exceeded. |
| `MEMBER_LIMIT_VIOLATION` | The workspace has more members than its plan allows. |
| `K3_ULTRA_ACCESS_REQUIRED` | Krea 3 with `thinking_mode` `"ultra"` (or the legacy `"max"`) and image references, on a plan without Krea 3 Ultra. Without image references, Ultra runs as `"fast"`. |
| `ENHANCER_RESOLUTION_LIMIT_REACHED` | An upscale is larger than the free plan allows. |

A generate call with Zero Data Retention on a workspace that does not have it enabled returns this body, always JSON:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{ "error": "Zero data retention is not enabled for this workspace." }
```

The [Agent operation API](#agent-operation-api) uses its own `402` bodies. Those draw on the workspace compute balance, not the API balance.

### `403`

| `message` or `error` | When |
| - | - |
| `Use an API key or a plugin session` | Realtime, called without an API key or an OAuth or plugin session. `{ "message" }`. |
| `This endpoint needs a workspace service API key` | Usage API, credential is not a workspace service API key. `{ "message" }`. |
| `Your plan is not entitled to the usage API` | Usage API, plan is not entitled. `{ "message" }`. |
| `FEATURE_RESTRICTED` | Model policy denial. The body also includes `modelAccessDenial`. `{ "message" }`. |
| `MODEL_ACCESS_REQUIRED` | The model is internal-only. `{ "message" }`. |
| `This node app cannot be used via the API.` | Node-app execute. `{ "error" }`, always JSON. |
| `Free users cannot run node apps` | Node-app execute on a free-plan workspace. `{ "message" }`. |
| `You do not own this style` | Changing or sharing a style you do not own. `{ "message" }`. |

### `404`

Every `404` is `{ "message" }`, for example an unknown job id:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{ "message": "Job not found or unauthorized" }
```

A missing `Accept` header makes these HTML, except the `Endpoint not found` for a model your key or plan cannot use, which is always JSON. An authenticated `GET /jobs/{id}` for an unknown id with no `Accept` is `404` `text/html`.

| `message` | When |
| - | - |
| `Not Found` | No route matched the path. |
| `Endpoint not found` | A model path under `/generate` that is unknown, or that this API key or plan cannot use. |
| `Job not found or unauthorized` | Missing or invalid job id, including `DELETE /jobs/{id}`. |
| `Asset not found` | Missing asset. |
| `Style not found` | Missing style. Changing or sharing one you do not own is `Style not found or you are not the owner`. |
| `Node app not found` | Missing node app. |
| `Workspace not found` | Usage API, workspace is missing. |

An unknown Agent operation is also `404` `{ "message": "Endpoint not found" }`, not the Agent `{ "error", "code" }` shape.

### `405`

The body is plain text with no `Content-Type` header, not JSON, including when `Accept` is `application/json`. The text is the method plus ` method not allowed`, for example `GET method not allowed`. An `Allow` header lists the methods the path accepts.

### `413` Payload too large

A request body that is too large is rejected with `413`. From the API itself the body follows `Accept`:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{ "message": "Payload Too Large" }
```

A very large body can be rejected before it reaches the API, with an HTML `413` page. Do not send files inline. Upload the file with `POST /assets` and pass the returned URL in the generate request.

### Input downloads

A host that returns `404` or `410` is a permanent `422`. The body is JSON, and `Accept` matters:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{ "message": "Input file rejected: fetching <url> returned HTTP 404" }
```

A host `410` uses the same sentence with `410`. Other permanent rejections use `Input file rejected: …`.

A retryable download failure is `502` or `504`. Branch on the status code, not the body, and retry.

| Status | What you receive | Retry? |
| - | - | - |
| `422` | JSON `{ "message": "Input file rejected: fetching <url> returned HTTP 404" }` when the host returns `404`. A host `410` uses the same sentence with `410`. | No. The failure is permanent. |
| `502` | Cloudflare plain text `error code: 502`, with `Content-Type: text/plain; charset=UTF-8`. Confirmed when the file host returns `503`, `500`, or `429`, and when the domain does not resolve. | Yes. Branch on the status code. |
| `504` | Not live-tested. It may also arrive as Cloudflare plain text. | Yes. Branch on the status code. |

A `502` that keeps repeating for the same URL likely means the URL is blocked or broken. Change the URL.

3D export conversion failures are `502` as well. That path was not live-tested, so the body may also arrive as Cloudflare plain text `error code: 502`. Branch on the status code and retry. A `502` that keeps repeating for the same model URL likely means the URL is blocked or broken.

### `429` concurrency

When the job cannot be backlogged, a request over the concurrency cap can be rejected with `429` and `Content-Type: application/json`:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "message": "CONCURRENCY_LIMIT_REACHED",
  "limit": 4,
  "jobType": "k2"
}
```

`limit` is your concurrency cap for this kind of job. It is counted per user, not per workspace. It is approximate: a short burst can be accepted above it, so don't count on request `limit + 1` being the one that fails. `jobType` is an internal job category, not the public model path. Don't branch on it.

Krea 2 can also return only `{ "message": "CONCURRENCY_LIMIT_REACHED" }`, with no `limit` or `jobType`, when its shared queue is full. That is not your cap. Back off and retry.

Backlogging is on only when the plan includes it (Pro and above), the model supports it, and the request is not [Zero Data Retention](https://www.krea.ai/docs/developers/zdr.md). Krea 2 requests are not backlogged on any plan right now, so they return `429` over the cap. When it is on, the extra job is accepted with `status: "backlogged"` and starts once a slot is free. There is no separate backlog cap. See [Rate limits](https://www.krea.ai/docs/developers/rate-limits.md).

The API does not send `Retry-After` or `X-RateLimit-*`. The response does include `cf-ray`. On `429`, retry with exponential backoff after in-flight jobs finish (poll `GET /jobs/{id}`, or use a webhook).

If a later completion exhausts the balance, jobs still sitting in the backlog fail with `error.code` `"insufficient_balance"`. That is a failed job, not an HTTP `402`. Those jobs do not send a webhook, so poll `GET /jobs/{id}` for backlogged jobs.

## Agent operation API

`POST /agent/{operation}` and `GET /agent/session/{sessionId}` return `{ "error", "code"?, "allowedModels"? }`. Only `error` is required. `allowedModels` is camelCase. A turn that fails after it has been accepted shows up as `status: "failed"` on the session, not as one of these HTTP errors. See [Agent](https://www.krea.ai/docs/developers/agent.md).

| Status | Body |
| - | - |
| `400` | `{ "error": "Invalid JSON body" }` |
| `401` | `{ "error": "Unauthorized" }` |
| `402` | `{ "error": "Insufficient compute balance for an Agent turn", "code": "compute_limit" }`, or `{ "error": "Free plan Agent message limit reached. Upgrade to keep using the Agent", "code": "free_message_limit" }`. |
| `403` | `{ "error": "Model is not included in the workspace plan", "code": "model_not_entitled", "allowedModels": ["..."] }`. |
| `404` | Unknown operation: `{ "message": "Endpoint not found" }`. Otherwise `{ "error": "Session not found or not accessible", "code": "not_found" }`, or `{ "error": "<file reference error>", "code": "attachment_not_found" }`. |
| `409` | `{ "error": "...", "code": "conflict" }`. |
| `422` | Bad input: `{ "error": "Validation failed", "details": [...] }` as in [Validation](#validation-422). Other mapped errors are `{ "error": "<message>" }` plus `code` when one exists. A disconnected desktop app has no `code`; the message starts with `That desktop app is not connected`. |

An unrecognized Agent error code is still `422` with `{ "error", "code" }`. The unknown-operation `404` and an unhandled `500` use `{ "message" }` and follow `Accept`.

## Failed-job `error.code`

`GET /jobs` and `GET /jobs/{id}` set `error` only when `status` is `"failed"`. There is no usable `result`:

```json theme={"theme":{"light":"github-light-default","dark":"vesper"}}
{
  "status": "failed",
  "error": {
    "code": "content_policy",
    "message": "An external provider's moderation systems rejected the generation."
  }
}
```

> [!WARNING]
>
> Branch on `error.code`. Never parse `message`.
>
> `message` can be omitted. `provider_error` and `internal` messages are free-form, and an `internal` message can be raw provider text such as `failed to download media from the provided URL`. Treat any `code` you do not recognize as `internal`.

| `code` | `message` | What to do |
| - | - | - |
| `content_policy` | `Krea's moderation systems rejected the generation.` or `An external provider's moderation systems rejected the generation.` | Change the prompt or inputs. The same request will most likely be rejected again. |
| `timeout` | `An external provider did not complete the generation in sufficient time.` | Retry the job. |
| `insufficient_balance` | Omitted. A node app can instead return `You are out of compute units. Add more compute units to continue running this node app.` | Top up, then submit again. This job was accepted earlier; the HTTP call did not return `402`. |
| `auto_cancelled` | `Job repeatedly failed healthchecks and was automatically cancelled.` | Submit the job again. |
| `model_unavailable` | Usually the provider's own text. `The provider retired this model. It no longer accepts new generations.` when there is none. | Stop calling this model. Use the replacement on [Deprecations](https://www.krea.ai/docs/developers/deprecations.md) when there is one. |
| `provider_error` | Usually the provider's own text. | Retry with backoff. |
| `internal` | Free-form, or omitted. | Retry later. Quote `job_id` if you contact support. |
| `node_app_node_failed` | `Node "<type>" failed: <detail>` or `Node "<type>" failed.` | Read `message`. If the node hit a temporary problem, such as a timeout, run the app again. Otherwise fix that node first. The app's own job is never billed. Each generation or LLM node is charged when its own job completes. A failure does not cancel parallel nodes or other branches, so a node that completes after the app has already failed is charged too, and those charges are not refunded. A node that fails, is cancelled, or never starts is not charged. The node id and node type are not on the public job. |

Webhook deliveries for a failed job use the same job object, including `error`. A cancelled delivery has no `error`. A payload larger than 256 KiB is not sent, and no further deliveries are made for that job, so poll `GET /jobs/{id}` instead. Verify the signature before you trust the body. See [Webhooks](https://www.krea.ai/docs/developers/webhooks.md).

## Debugging

There is no Krea request-id header. Every response does include Cloudflare's `cf-ray` header. No `Retry-After` or `X-RateLimit-*` header is sent.

When you write to [support@krea.ai](mailto:support@krea.ai), include:

* `job_id`, when the call created or addressed a job
* The time and the endpoint
* For a request-time error, the `cf-ray` value

`Access-Control-Allow-Origin` is `*`. On generate, jobs, assets, styles, and node apps, `Access-Control-Expose-Headers` is not set, so browser `fetch` cannot read `cf-ray`, `Deprecation`, or `Sunset`. Copy `cf-ray` from the network log, or from a server-side client.

On any response, check `Deprecation` and `Sunset` before the model stops accepting calls. See [Deprecations](https://www.krea.ai/docs/developers/deprecations.md).

## Next steps

> **[Job lifecycle](https://www.krea.ai/docs/developers/job-lifecycle.md)**
>
> Poll status until the job completes, fails, or is cancelled

> **[Webhooks](https://www.krea.ai/docs/developers/webhooks.md)**
>
> Verify the signature on a failed-job delivery

> **[Rate limits](https://www.krea.ai/docs/developers/rate-limits.md)**
>
> When extra jobs are backlogged and when they return 429

> **[API keys and billing](https://www.krea.ai/docs/developers/api-keys-and-billing.md)**
>
> Top up the API balance after a 402


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.