Errors
Jantaris uses standard HTTP status codes. A 2xx means the request was accepted. Anything else carries a JSON body with an error type you can branch on.
Format
/v1/generate, /v1/tasks, /v1/models and /v1/balance return:
{ "error": { "type": "insufficient_balance", "message": "Available balance is lower than the estimated cost of this task." } }The chat endpoints keep the format of the SDK they are compatible with, so existing error handling keeps working:
| Endpoint | Error format |
|---|---|
/v1/chat/completions | OpenAI: {"error": {"message": "...", "type": "..."}} |
/v1/messages | Anthropic: {"type": "error", "error": {"type": "...", "message": "..."}} |
message is meant for humans and may change. Branch on the HTTP status and type.
Error types
| HTTP | type | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_request | Malformed request, for example broken JSON or missing model | Fix the request |
| 401 | authentication_error | Missing, malformed or deleted API key | Check the key and header, see Authentication |
| 402 | insufficient_balance | Available balance is lower than the cost or hold | Top up |
| 403 | permission_error | The key's monthly spend limit is reached. Message: This key reached its monthly spend limit. Create a new key with a higher limit in the dashboard or use another key. | Create a new key with a higher limit or use another key |
| 404 | not_found | Unknown model id or task id | Check the id against Models |
| 422 | invalid_request | input does not match the model's input_schema | Read the message, it names the field |
| 429 | rate_limit | Too many requests for this key | Slow down and retry with backoff |
| 429 | rate_limit_error | 25 failed generations on the account in the last 24 hours; new /v1/generate tasks are paused | Wait the number of seconds in the Retry-After header and change the prompt or parameters |
| 502 | upstream_error | The model provider returned an error | Retry after a short pause |
| 504 | timeout | The model did not answer in time | Retry, for chat consider a shorter output |
Rate limits
Default limits per API key:
| Endpoint | Limit |
|---|---|
POST /v1/generate | 60 requests per minute |
/v1/chat/completions, /v1/messages | 600 requests per minute |
POST /v1/generate, failed generations | 25 per account in 24 hours (rolling window), then 429 rate_limit_error with a Retry-After header |
When you exceed a limit you get 429 rate_limit. Spread requests out or use separate keys for separate workloads.
Failed tasks are not HTTP errors
A generation that starts and then fails does not produce an HTTP error. POST /v1/generate returns 202, and later the task has status: "failed" with an error object:
{ "id": "task_3xq1...", "status": "failed", "error": { "type": "upstream_error", "message": "..." }, "cost": { "amount": "0.00", "currency": "USD" } }The hold is released and you pay 0. Check status on every finished task, see Tasks.
When to retry
Retry 429, 502 and 504 with exponential backoff. Do not retry 400, 401, 402, 403, 404 or 422 without changing something first, because the same request will fail the same way.
Do not retry tasks that end with status: "failed" in a loop. Failed generations cost nothing, but after 25 failures within 24 hours the account gets 429 rate_limit_error and new tasks wait until the time in the Retry-After header has passed. Change the prompt or parameters before you try again.
import os, random, time, requests
RETRY = {429, 502, 504}
def call(method: str, url: str, **kwargs) -> requests.Response:
headers = {"Authorization": f"Bearer {os.environ['JANTARIS_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://jantaris.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.JANTARIS_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();
}With the OpenAI and Anthropic SDKs you get retries built in. Both retry rate limit and server errors a couple of times by default, and you can tune it with max_retries (Python) or maxRetries (JavaScript).
from openai import OpenAI
client = OpenAI(base_url="https://jantaris.com/v1", api_key="sk-jt-...", max_retries=4)Retrying /v1/generate safely
Each successful call to /v1/generate creates a new task and holds funds for it. If a request timed out on your side and you are not sure whether it reached the server, do not blindly send it again in a loop. Log the situation, and if you use webhooks, wait for the delivery before starting a duplicate.









