Skip to main content
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.

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} or wait for a webhook. 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.

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

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 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)

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.

Removed and deprecated fields (422)

The removed batchSize / batch_size field uses a different 422. There is no details array:
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):
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. 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

400

Invalid JSON on generate is always JSON, with no Accept header required:
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) 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, not 400.

401

Generate, jobs, assets, styles, and node apps:
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:
Top up at krea.ai/app/api/ ↗. In-flight jobs finish. See API keys and billing. Other 402 message values on the public API: A generate call with Zero Data Retention on a workspace that does not have it enabled returns this body, always JSON:
The Agent operation API uses its own 402 bodies. Those draw on the workspace compute balance, not the API balance.

403

404

Every 404 is { "message" }, for example an unknown job id:
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. 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:
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:
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. 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:
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. 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. 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. 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:
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.
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.

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

Next steps

Job lifecycle

Poll status until the job completes, fails, or is cancelled

Webhooks

Verify the signature on a failed-job delivery

Rate limits

When extra jobs are backlogged and when they return 429

API keys and billing

Top up the API balance after a 402