Platform

Errors

Predictable error shapes and codes across the API.

Every failed request returns a non-2xx HTTP status and a JSON body describing the failure. The API uses two error shapes, chosen by the endpoint family: the native Krallo endpoints wrap errors in the same { … , "meta": { "request_id" } } envelope they use for success, while the OpenAI-compatible endpoints emit the exact error shape the OpenAI SDKs and coding agents expect. Both are stable contracts — the machine-readable slug never changes for a given failure.

Two error shapes

Native error shape

All native Krallo endpoints — /ping, /me, /guilds, /usage, /personas, /memories, and /webhooks/deliveries — return errors as an error object carrying a stable code and a human-readable message, alongside a meta block with the request’s correlation id. Program against error.code; treat message as display text that may change.

{
  "error": {
    "code": "insufficient_scope",
    "message": "Missing required scope.",
    "required_scope": "ai:invoke"
  },
  "meta": {
    "request_id": "9f2c1ab4-e7d8-4b3a-a1c5-6e0f2d8b7c94"
  }
}

Some codes attach extra fields inside the error object to help you recover programmatically — for example required_scope on insufficient_scope, and required_units / balance on insufficient_credits. Unknown fields may be added over time, so parse defensively.

OpenAI-compatible error shape

The OpenAI-compatible endpoints — chat completions (POST /api/v1/chat/completions), GET /api/v1/models, and image generation (POST /api/v1/ai/imagine) — return the OpenAI error shape instead, so a stock OpenAI client, Aider, Continue, nanocoder, or opencode parses failures without special handling. The machine-readable slug lives in type, the message in message, and there is no meta block.

{
  "error": {
    "message": "Unknown model 'gpt-5'. Valid models: krallo-chat, krallo-code, krallo-fast, krallo-reasoning.",
    "type": "model_not_found"
  }
}

If you call both families, key your error handling off the response: read error.code for native endpoints and error.type for the OpenAI-compatible ones. Two deliberate carve-outs on the OpenAI-compatible endpoints use the native shape: failures from the shared auth gate (the 401 token codes, 403 app_disabled / owner_suspended / insufficient_scope, and the 404 while the API is disabled), and the 402 from image generation — see Insufficient credits.

HTTP status codes

The status code tells you the class of failure; the body’s code/type tells you the specific reason. A 404 is also what you get for any request while the developer API is disabled, so an integration that suddenly 404s everywhere should assume the surface is dark rather than that a single route was removed.

StatusMeaningTypical codes
400Bad request — the payload or a parameter is malformed or out of range.invalid_request, invalid_request_error, model_not_found
401Unauthenticated — the bearer token is missing, unknown, revoked, or expired.unauthorized, invalid_token, token_revoked, token_expired
402Payment required — over the free quota and the owner wallet cannot cover the call.insufficient_credits
403Forbidden — the app is disabled, the owner is suspended, or the key lacks the scope.app_disabled, owner_suspended, insufficient_scope
404Not found — unknown route, an id you don’t own, or the API is disabled.not_found
405Method not allowed — the HTTP method isn’t supported on that path.method_not_allowed
409Conflict — the resource is locked, disabled, or a competing operation is in flight.persona_locked, memory_disabled, delivery_in_flight
422Unprocessable — a business rule rejected an otherwise well-formed request (mostly the management portal).invalid_scope, app_cap_reached, key_cap_reached, unknown_event
429Rate limited — the per-key or per-IP limit was exceeded. Carries Retry-After and X-RateLimit-* headers.see Rate limits
500Internal error — an unexpected server-side failure. Safe to retry with backoff.internal_error
502Upstream error — the model provider or image backend was unavailable. Retry.upstream_error
504Timeout — a synchronous job (image generation) did not finish within its wait budget and was cancelled and refunded.timeout

The 429 body is written by the shared rate limiter and is not guaranteed to use either error envelope — always branch on the 429 status itself and honour the Retry-After header. Full details, including the per-tier limits and the advisory X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers sent on every response, are in Rate limits.

Error codes

The full set of stable slugs. “Shape” indicates whether the slug appears as error.code (native), error.type (OpenAI-compatible), or both. Match on the slug, not the message.

CodeHTTPShapeMeaning
unauthorized401nativeNo bearer token was supplied. Send Authorization: Bearer krallo_live_….
invalid_token401nativeThe token is not recognised (wrong value, or the key never existed).
token_revoked401nativeThe key was revoked in the console. Mint a new one.
token_expired401nativeThe key is past its expires_at. Rotate or mint a new key.
app_disabled403nativeThe owning app is inactive. Re-enable it or use a key from an active app.
owner_suspended403nativeThe owner account (checked across every linked identity) is suspended.
insufficient_scope403nativeThe key lacks the required scope. The missing scope is in required_scope.
insufficient_credits402bothOver the free quota and the wallet can’t cover the call. Includes required_units and balance. See Billing & quotas.
model_not_found400OpenAIThe model field names an unknown model. The message lists the valid ids.
invalid_request400nativeValidation failed on a native write (missing field, value out of range). The message names the field.
invalid_request_error400OpenAIThe OpenAI-endpoint counterpart of invalid_request — e.g. non-JSON body, empty messages, or an unusable persona.
persona_not_found404flat (portal)An app tried to pin a persona id that isn’t yours (management portal; sent as a bare {"error":"persona_not_found"} body). On the bearer /personas/{id} route a missing persona returns not_found instead.
persona_locked409nativeThe persona is locked and can’t be edited or deleted.
memory_disabled409nativeMemory is turned off for the account. Enable it before saving facts. See Memories.
not_found404nativeUnknown route, an unknown or foreign resource id (persona, memory, webhook delivery/endpoint), or the API is disabled.
delivery_in_flight409nativeA webhook delivery replay was requested while a delivery attempt is still in flight. Retry once it settles.
method_not_allowed405bothThe HTTP method isn’t supported on that path.
invalid_scope422flat (portal)A key mint requested a scope that isn’t grantable (management portal; sent as a bare {"error":"invalid_scope"} body). See Scopes.
upstream_error502OpenAIThe model provider or image backend was unavailable, or a stream failed. The call was not billed. Retry.
timeout504OpenAIA synchronous image job didn’t finish in time; it was cancelled and refunded. Retry, optionally with a larger wait_timeout_ms.
internal_error500nativeAn unexpected server-side failure. Any provisional charge is rolled back. Retry with backoff.

Metered calls that fail after being charged are automatically refunded — a refused (402/403/404), errored, or timed-out call never keeps your credits or burns free quota. You do not need to reconcile anything on your side.

Insufficient credits (402)

A 402 means the app has exhausted its free daily quota and the owner’s prepaid wallet cannot cover the call. The body always includes required_units (the weighted credit units this call needed) and balance (the wallet balance at the time of the failure), so you can surface a precise top-up prompt. The shape follows the endpoint family.

Native endpoints and image generation return the native shape. required_units is the flat per-call price for native endpoints (default 1) or the fixed image cost for /ai/imagine (default 10):

{
  "error": {
    "code": "insufficient_credits",
    "message": "Insufficient credit balance for this request.",
    "required_units": 10,
    "balance": 3
  },
  "meta": {
    "request_id": "9f2c1ab4-e7d8-4b3a-a1c5-6e0f2d8b7c94"
  }
}

Chat completions returns the OpenAI shape. Because chat is token-metered, the reservation is pre-authorised before inference runs, so required_units is the pre-authorisation reserve (default 250) rather than a per-call flat price — the call 402s before the model runs:

{
  "error": {
    "message": "Insufficient credit balance for this request.",
    "type": "insufficient_credits",
    "required_units": 250,
    "balance": 3
  }
}

Free quota resets at 00:00 UTC, so a 402 may clear on its own at the next UTC day boundary even without a top-up. Check remaining quota and the wallet balance any time via GET /api/v1/usage — that endpoint is deliberately unmetered so it stays reachable while you’re over quota. Full details in Billing & quotas.

Handling errors

Branch on the HTTP status first, then on the slug. A robust client should:

Node.js

const res = await fetch("https://exe.krallo.com/api/v1/me", {
  headers: { Authorization: `Bearer ${process.env.KRALLO_API_KEY}` },
});

if (!res.ok) {
  const body = await res.json().catch(() => ({}));
  // Native endpoints: error.code + meta.request_id.
  // OpenAI-compatible endpoints (/chat/completions, /models, /ai/imagine): error.type.
  const slug = body?.error?.code ?? body?.error?.type ?? "unknown";
  const requestId = body?.meta?.request_id;

  if (res.status === 402) {
    // Out of credits — surface a top-up. required_units / balance are on error.
    throw new Error(`Insufficient credits (needed ${body.error.required_units}, balance ${body.error.balance})`);
  }
  if (res.status === 429 || res.status >= 500) {
    // Transient — retry with backoff, honouring Retry-After on 429.
    throw new RetryableError(slug, res.headers.get("retry-after"));
  }
  throw new Error(`${res.status} ${slug}${requestId ? ` (request_id=${requestId})` : ""}`);
}

See also