PL Log inOpen Studio
Preview version. Model catalog, prices, calculator and docs are live. Generation goes live at launch.

Errors

Docs: every page

The API sticks to regular HTTP semantics. Any 2xx response means your request was taken. For everything else you get a JSON body containing an error type, which is the value your code should switch on.

What an error looks like

On /v1/generate, /v1/tasks, /v1/models and /v1/balance the body has this shape:

json
{ "error": { "type": "insufficient_balance", "message": "Available balance is lower than the estimated cost of this task." } }

The two chat endpoints copy the error shape of the SDK they imitate, which means handlers you already wrote for those SDKs do not need changes:

EndpointShape
/v1/chat/completionsOpenAI: {"error": {"message": "...", "type": "..."}}
/v1/messagesAnthropic: {"type": "error", "error": {"type": "...", "message": "..."}}

Treat message as text for people to read. Its wording can change, so make decisions based on the status code and type only.

Reference table

HTTPtypeCauseFix
400invalid_requestThe request itself is broken, such as invalid JSON or no modelCorrect the request
401authentication_errorThe API key is absent, malformed or has been deletedCheck the key and the header (Authentication)
402insufficient_balanceNot enough available funds for the cost or the reservationAdd funds
403permission_errorThe key used up its monthly spend cap. Message: This key reached its monthly spend limit. Create a new key with a higher limit in the dashboard or use another key.Switch keys or make one with a higher cap
404not_foundNo model or task with that idCompare the id with Models
422invalid_requestinput breaks the model's input_schemaThe message says which field is wrong
429rate_limitThe key sent too many requestsBack off, then try again
429rate_limit_errorThe account had 25 failed generations in the past 24 hours, so new /v1/generate tasks are on holdWait for the seconds given in Retry-After, and adjust the prompt or parameters
502upstream_errorThe model's provider answered with an errorTry again after a moment
504timeoutNo answer from the model in timeTry again; for chat, ask for a shorter output

Default rate limits

These apply to each API key unless stated otherwise:

WhatLimit
POST /v1/generate60 per minute
/v1/chat/completions, /v1/messages600 per minute
Failed POST /v1/generate tasks25 per account in any 24-hour window, after which 429 rate_limit_error with Retry-After

Going over a per-key limit returns 429 rate_limit. Space out your calls, or split different workloads across different keys.

A failed task is not an HTTP error

When a generation starts and later goes wrong, the HTTP layer does not report it. POST /v1/generate still answers 202, and the task ends up with status: "failed" plus an error object:

json
{ "id": "task_3xq1...", "status": "failed", "error": { "type": "upstream_error", "message": "..." }, "cost": { "amount": "0.00", "currency": "USD" } }

Its reservation is returned and the charge is zero. Always look at status once a task is done; Tasks explains the fields.

Retry rules

  • Retry with exponential backoff: 429, 502, 504.
  • Change something first, then retry: 400, 401, 402, 403, 404, 422. Sending the identical request again will only get the identical error.
  • Do not loop on failed tasks. They are free, but 25 failures in 24 hours trigger 429 rate_limit_error, and new tasks stay blocked until the Retry-After time is over. Rework the prompt or the parameters instead.
python
import os, random, time, requests

RETRY = {429, 502, 504}

def call(method: str, url: str, **kwargs) -> requests.Response:
    headers = {"Authorization": f"Bearer {os.environ['KADRARIS_API_KEY']}"}
    for attempt in range(5):
        r = requests.request(method, url, headers=headers, timeout=130, **kwargs)
        if r.status_code not in RETRY:
            break
        time.sleep(min(2 ** attempt, 30) + random.random())
    if r.status_code >= 400:
        err = r.json().get("error", {})
        raise RuntimeError(f"{r.status_code} {err.get('type')}: {err.get('message')}")
    return r

task = call("POST", "https://kadraris.com/v1/generate",
            json={"model": "gpt-image-2.5", "input": {"prompt": "A red bicycle"}}).json()
javascript
const RETRY = new Set([429, 502, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function call(url, init = {}) {
  let res;
  for (let attempt = 0; attempt < 5; attempt++) {
    res = await fetch(url, {
      ...init,
      headers: {
        Authorization: `Bearer ${process.env.KADRARIS_API_KEY}`,
        "Content-Type": "application/json",
        ...init.headers,
      },
    });
    if (!RETRY.has(res.status)) break;
    await sleep(Math.min(2 ** attempt * 1000, 30000) + Math.random() * 1000);
  }
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${res.status} ${error?.type}: ${error?.message}`);
  }
  return res.json();
}

If you use the OpenAI or Anthropic SDK, retrying is already handled. Out of the box they repeat rate-limited and server-error requests a few times; set max_retries in Python or maxRetries in JavaScript to change how many.

python
from openai import OpenAI
client = OpenAI(base_url="https://kadraris.com/v1", api_key="sk-kd-...", max_retries=4)

Avoiding duplicate generations

Every accepted /v1/generate call is a separate task with its own reservation. Suppose your client timed out and you cannot tell whether the request got through. Resending it automatically may start a second, paid task. Record the event instead, and if a webhook is configured, give the delivery time to arrive before you launch the job again.