PL Log inCreate account
Launching in October 2026. Accounts, model catalog, calculator and docs are live. Studio is available in preview; generation is awaiting activation.

Errors

All documentation pages

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:

json
{ "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:

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

message is meant for humans and may change. Branch on the HTTP status and type.

Error types

HTTPtypeMeaningWhat to do
400invalid_requestMalformed request, for example broken JSON or missing modelFix the request
401authentication_errorMissing, malformed or deleted API keyCheck the key and header, see Authentication
402insufficient_balanceAvailable balance is lower than the cost or holdTop up
403permission_errorThe 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
404not_foundUnknown model id or task idCheck the id against Models
422invalid_requestinput does not match the model's input_schemaRead the message, it names the field
429rate_limitToo many requests for this keySlow down and retry with backoff
429rate_limit_error25 failed generations on the account in the last 24 hours; new /v1/generate tasks are pausedWait the number of seconds in the Retry-After header and change the prompt or parameters
502upstream_errorThe model provider returned an errorRetry after a short pause
504timeoutThe model did not answer in timeRetry, for chat consider a shorter output

Rate limits

Default limits per API key:

EndpointLimit
POST /v1/generate60 requests per minute
/v1/chat/completions, /v1/messages600 requests per minute
POST /v1/generate, failed generations25 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:

json
{ "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.

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['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()
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.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).

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