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.
| Status | Meaning | Typical codes |
|---|---|---|
| 400 | Bad request — the payload or a parameter is malformed or out of range. | invalid_request, invalid_request_error, model_not_found |
| 401 | Unauthenticated — the bearer token is missing, unknown, revoked, or expired. | unauthorized, invalid_token, token_revoked, token_expired |
| 402 | Payment required — over the free quota and the owner wallet cannot cover the call. | insufficient_credits |
| 403 | Forbidden — the app is disabled, the owner is suspended, or the key lacks the scope. | app_disabled, owner_suspended, insufficient_scope |
| 404 | Not found — unknown route, an id you don’t own, or the API is disabled. | not_found |
| 405 | Method not allowed — the HTTP method isn’t supported on that path. | method_not_allowed |
| 409 | Conflict — the resource is locked, disabled, or a competing operation is in flight. | persona_locked, memory_disabled, delivery_in_flight |
| 422 | Unprocessable — a business rule rejected an otherwise well-formed request (mostly the management portal). | invalid_scope, app_cap_reached, key_cap_reached, unknown_event |
| 429 | Rate limited — the per-key or per-IP limit was exceeded. Carries Retry-After and X-RateLimit-* headers. | see Rate limits |
| 500 | Internal error — an unexpected server-side failure. Safe to retry with backoff. | internal_error |
| 502 | Upstream error — the model provider or image backend was unavailable. Retry. | upstream_error |
| 504 | Timeout — 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.
| Code | HTTP | Shape | Meaning |
|---|---|---|---|
unauthorized | 401 | native | No bearer token was supplied. Send Authorization: Bearer krallo_live_…. |
invalid_token | 401 | native | The token is not recognised (wrong value, or the key never existed). |
token_revoked | 401 | native | The key was revoked in the console. Mint a new one. |
token_expired | 401 | native | The key is past its expires_at. Rotate or mint a new key. |
app_disabled | 403 | native | The owning app is inactive. Re-enable it or use a key from an active app. |
owner_suspended | 403 | native | The owner account (checked across every linked identity) is suspended. |
insufficient_scope | 403 | native | The key lacks the required scope. The missing scope is in required_scope. |
insufficient_credits | 402 | both | Over the free quota and the wallet can’t cover the call. Includes required_units and balance. See Billing & quotas. |
model_not_found | 400 | OpenAI | The model field names an unknown model. The message lists the valid ids. |
invalid_request | 400 | native | Validation failed on a native write (missing field, value out of range). The message names the field. |
invalid_request_error | 400 | OpenAI | The OpenAI-endpoint counterpart of invalid_request — e.g. non-JSON body, empty messages, or an unusable persona. |
persona_not_found | 404 | flat (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_locked | 409 | native | The persona is locked and can’t be edited or deleted. |
memory_disabled | 409 | native | Memory is turned off for the account. Enable it before saving facts. See Memories. |
not_found | 404 | native | Unknown route, an unknown or foreign resource id (persona, memory, webhook delivery/endpoint), or the API is disabled. |
delivery_in_flight | 409 | native | A webhook delivery replay was requested while a delivery attempt is still in flight. Retry once it settles. |
method_not_allowed | 405 | both | The HTTP method isn’t supported on that path. |
invalid_scope | 422 | flat (portal) | A key mint requested a scope that isn’t grantable (management portal; sent as a bare {"error":"invalid_scope"} body). See Scopes. |
upstream_error | 502 | OpenAI | The model provider or image backend was unavailable, or a stream failed. The call was not billed. Retry. |
timeout | 504 | OpenAI | A synchronous image job didn’t finish in time; it was cancelled and refunded. Retry, optionally with a larger wait_timeout_ms. |
internal_error | 500 | native | An 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:
- Never parse the message for control flow — match
error.code(native) orerror.type(OpenAI). Messages are display text and may be reworded. - Not retry
4xxother than429— they are deterministic (fix the request, scope, or credits).401/403won’t succeed on retry with the same key. - Retry
429,500,502, and504with exponential backoff and jitter. For429, wait at least theRetry-Afterseconds. - Capture
meta.request_idfrom native responses. When you contact support about a failure, quote it — it lets us find the exact request in the access log. It may be an empty string if the request was rejected before correlation was assigned; the OpenAI-compatible endpoints do not return one.
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
- Authentication — token lifecycle behind the
401codes. - Scopes — the grants behind
insufficient_scopeandinvalid_scope. - Rate limits — the
429contract and rate-limit headers. - Billing & quotas — quota resets and the wallet behind
insufficient_credits.