Errors
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:
{ "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:
| Endpoint | Shape |
|---|---|
/v1/chat/completions | OpenAI: {"error": {"message": "...", "type": "..."}} |
/v1/messages | Anthropic: {"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
| HTTP | type | Cause | Fix |
|---|---|---|---|
| 400 | invalid_request | The request itself is broken, such as invalid JSON or no model | Correct the request |
| 401 | authentication_error | The API key is absent, malformed or has been deleted | Check the key and the header (Authentication) |
| 402 | insufficient_balance | Not enough available funds for the cost or the reservation | Add funds |
| 403 | permission_error | The 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 |
| 404 | not_found | No model or task with that id | Compare the id with Models |
| 422 | invalid_request | input breaks the model's input_schema | The message says which field is wrong |
| 429 | rate_limit | The key sent too many requests | Back off, then try again |
| 429 | rate_limit_error | The account had 25 failed generations in the past 24 hours, so new /v1/generate tasks are on hold | Wait for the seconds given in Retry-After, and adjust the prompt or parameters |
| 502 | upstream_error | The model's provider answered with an error | Try again after a moment |
| 504 | timeout | No answer from the model in time | Try again; for chat, ask for a shorter output |
Default rate limits
These apply to each API key unless stated otherwise:
| What | Limit |
|---|---|
POST /v1/generate | 60 per minute |
/v1/chat/completions, /v1/messages | 600 per minute |
Failed POST /v1/generate tasks | 25 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:
{ "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 theRetry-Aftertime is over. Rework the prompt or the parameters instead.
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()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.
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.























