Skip to navigation

Errors and retries

Every error an adapter hits, whether to retry it, and the in-product fallback.

Every /v1 error uses the same envelope. Read body.detail.code to branch in code, show body.detail.message to your operator logs, and treat body.detail.fix as the one-line remediation hint (it is written for a coding agent):

{
"detail": {
"code": "budget_exceeded",
"message": "app credit limit reached",
"fix": "Raise the app budget through the console/account plane or preview provisioning channel, or wait; see GET /v1/rates."
}
}

The HTTP status is authoritative for retry policy; code is authoritative for which thing went wrong. The two rules that hold across the whole surface:

  • 404, never 403, for resources — an id you do not own looks identical to one that does not exist (timing-safe, so an attacker cannot probe other tenants). A 403 is only ever about your key or a demo-key restriction, not about a missing resource.
  • 402 fires before the expensive work — over-budget message sends and image generations are rejected pre-flight, so a 402 means nothing was charged and no model ran.

A handful of envelope edge cases are not the {code, message, fix} shape — the two auth failures (401, and the invalid/revoked 403) and the two infrastructure 503s return a plain-string detail. They are called out explicitly in the tables below so your client does not assume detail.code always exists.

Retry decision in one line

  • 429, 502, 500 internal_error, and the two infrastructure 503s are retryable — back off and try again (the 500/502/503 cases are transient; 429 after its window).
  • Everything in the 4xx range below (except 429) is a caller fix, not a retry — retrying the same request reproduces the same error. Correct the request, the character setup, the budget, or the key first.
  • 409 session_exited is terminal for that session — start a new session; do not retry.

Request and authoring errors (4xx — fix the request)

These come from a malformed or out-of-range request body. None are retryable as-is; fix the named field and resend. validation_error is the one you will hit most: it is what every Pydantic field error (missing slug, missing display_name, a voice_samples entry with no text, an example_dialogues entry missing either a user or character turn, a forbidden_style entry that is a string not an object, an out-of-range behavioral_thresholds value, …) is rendered as, with the failing loc: msg quoted into message.

codeHTTPWhen it happensRetryableFallback in your product
validation_error422Any request-body field fails schema validation (wrong type, out of range, missing required field). The failing field path is in message.NoSurface the field path to the developer/operator; fix the body and resend. For agent-authored characters, POST /v1/characters/validate first so this never reaches a user.
invalid_character422POST /v1/characters (or /validate) — the character spec failed the typed authoring contract (e.g. behavioral_thresholds.image_proactivity or profile.drama_intensity out of 0..1).NoFix the named range; re-validate before create.
invalid_update422PATCH /v1/characters/{slug} — the update body failed validation.NoSame as invalid_character; correct the field and resend.
invalid_affordance_schema422affordance_schema is not a known preset (dating/tutor), a { "preset": "…" } pointer, or a valid full schema with unique dimension keys.NoUse a built-in preset or a full inline schema. See Character authoring → affordance schema.
slug_taken409The character slug is already owned by another app. (Reusing your own app’s slug is fine.)NoChoose a different slug.
slug_reserved409The canonical slug belongs to a permanently deleted character tombstone. Deleted slugs cannot be recreated or republished.NoGenerate a new unique slug for this character/test run.
delete_confirmation_mismatch422confirm_slug does not exactly equal the resolved canonical character slug.NoRead the canonical slug, then retry only after explicit destructive confirmation.
no_updates400A PATCH/settings call sent no changeable fields.NoSend at least one field to change.
invalid_app_id422An app_id path/body value is not a UUID.NoUse the app_id from GET /v1/me.
invalid_group_by422GET /v1/usage?group_by= is not a supported dimension.NoUse resource_type (default) or another supported dimension named in message.
invalid_category422A visual-asset upload used an unknown category.NoUse a category from the list in message.
invalid_asset422A visual-asset upload is missing asset_key/label.NoProvide both.
invalid_state422PATCH .../visual-assets/{id} sent an unknown promotion/qa/asset-status enum value.NoUse an allowed enum value.
image_rejected422An uploaded image was not a usable PNG/JPEG/WebP.NoUpload a valid image.
missing_scope_owner422A Story Room run used a scope that needs a scope_owner_id/session_id/thread_id and none was provided.NoProvide the scope owner, or use character_global scope.
invalid_story_room_mode422A Story Room run requested fixture_shadow (a local CLI/test mode, not a BaaS mode).NoUse db_shadow, review, or autocommit.
invalid_story_room_run422A Story Room run had invalid mode/autocommit flags.NoFix the mode and autocommit flags.
import_commit_disabled503POST /v1/imports/{import_id}/commit reached a ready import, but accepted external-memory writes are disabled for the environment.NoTreat the preview as staged-only. Ask the operator to enable import commits before retrying commit.

State conflicts (409 — fix the state, then proceed)

A 409 means the request is well-formed but the current state does not allow it. Retrying the identical call will keep failing until you change the underlying state (upload identity, publish a version, start a new session, …).

codeHTTPWhen it happensRetryableFallback in your product
session_exited409You sent a message / probe / inventory request to a session the character has left.No (terminal for that session)Start a new session and continue there. Treat the session.exited event as the signal to do this proactively.
needs_identity409An image operation (upload of a non-identity asset, selfie probe, inventory generation) ran before the character has an active identity image, or no fetchable identity reference + profile.identity_anchors could be seeded.NoUpload an identity image first (POST /v1/characters/{slug}/visual-assets, category=identity); set profile.identity_anchors as a dict for selfies. Check GET /v1/characters/{slug}/visual-readiness.
no_active_version409GET .../runtime-preview (or similar) for a character with no active published version.NoCreate/republish the character via POST or PATCH /v1/characters.
image_probe_failed409An image probe could not run given the character’s current image setup.No (fix setup)Check the character’s identity + inventory setup and retry once fixed.
candidate_not_activatable409POST .../story-room/candidates/{id}/approve on a candidate that cannot be activated (reason in message).NoReject it or rerun Story Room after fixing the source issue.
candidate_not_rejectable409POST .../story-room/candidates/{id}/reject on already-committed history.NoAppend a compensating event instead of rejecting committed history.
evidence_not_verified409POST .../evidence/{evidence_id}/state-candidate received evidence whose verification_status is not verified.NoVerify or replace the evidence first. A researched lead is not lived canon.
production_activation_blocked409POST /v1/account/apps/{app_id}/production-activation found one or more readiness blockers. The error includes the current readiness object and next_actions.NoFollow detail.readiness.next_actions, clear every blocker, then retry activation for the same app.
character_must_be_archived_before_delete409Character DELETE used the default force=false while the character was still published.NoPrefer POST .../archive, then DELETE with exact confirm_slug; use force=true only in an explicitly approved destructive flow.

Research review and execution result codes

Not every research failure is an HTTP error. POST /v1/characters/{slug}/state-reviews records a review and returns gate outcomes in data.gate_result.issues; a research run can also return a provider result with success: false and error_code. Branch on these codes without treating a successful HTTP response as a successful state commit.

codeWhere it appearsMeaningWhat your agent should do
candidate_not_founddata.gate_result.issues[]A requested candidate id does not belong to this app/character or no longer exists.Remove the id or fetch the character’s current candidate list.
candidate_not_accepted_for_reviewdata.gate_result.issues[]The candidate has not reached accepted_for_review.Complete candidate review first; do not force a commit.
evidence_not_founddata.gate_result.issues[]A requested or candidate-linked evidence id was not found in this app/character scope.Remove the id or submit the missing evidence in the same app/character scope.
evidence_not_verifieddata.gate_result.issues[] (and the 409 above)Evidence exists but is not verified.Verify or replace it before requesting a durable state candidate.
verified_evidence_requireddata.gate_result.issues[]The review has no verified evidence to support its candidates.Attach at least one verified evidence item.
public_eligible_evidence_requireddata.gate_result.issues[]A public_memory_candidate review has no evidence allowed for public use.Attach verified evidence with public_eligible: true, or choose a non-public target.
target_mismatchdata.gate_result.issues[]A candidate’s requested target differs from the state-review target.Split the review by target or choose the candidate’s target.
stale_grounding_queryResearch execution provider result error_codeA provider generated query years older than the run’s freshness policy allows.Replan with current-year constraints; do not admit the stale result as evidence.

Budget (402 — before any spend)

codeHTTPWhen it happensRetryableFallback in your product
budget_exceeded402A metered message send or image generation would exceed the app or per-user credit cap. Rejected pre-flight — nothing was charged and no model ran. message says which cap (per-user vs app).Not until the cap is raised or the period resetsRaise the app budget through the console/account plane or preview provisioning channel, or raise one user’s cap with POST /v1/external-users/{ref}/budget-policy; see GET /v1/rates. In-product, show the end-user a “you’re out of credits / try later” state rather than an error.

Auth (401 / 403 — fix the key)

These are about your key, never a missing resource. The generic invalid/revoked case returns a plain-string detail; known wrong credential prefixes return a structured detail.code so coding agents can fix the right plane.

ConditionHTTPdetail shapeWhen it happensRetryableFallback
Missing key401plain string "api key required" (+ WWW-Authenticate: Bearer)No Authorization: Bearer <key> header.NoSend the key. Keep it server-side.
Invalid / revoked key403plain string "api key invalid or revoked"The key does not resolve, or was revoked, or its app is inactive.NoUse a valid active key; reissue if revoked.
production_not_active403{ code, message, fix, top_up_url, readiness_endpoint, activation_endpoint }A ck_... runtime key belongs to a production app that has not passed readiness/activation yet.NoFollow the Go live (production) steps: top up at top_up_url, set an app cap, call production-readiness, then production-activation to promote your existing app in place. Do not create a new app.
account_token_used_as_runtime_key403{ code, message, fix }A lat_... account token was sent to a runtime /v1/* route.NoUse lat_... only on /v1/account/*, then issue a ck_... runtime key.
clerk_account_api_key_not_loreos_key403{ code, message, fix }A Clerk Account API key (ak_...) was sent to LoreOS.NoDo not use Clerk’s Account/API Keys UI. Issue a LoreOS Runtime API Key from the LoreOS console/account API.
runtime_key_prefix_invalid403{ code, message, fix }The bearer token does not look like a LoreOS runtime key.NoRuntime API keys currently start with ck_...; account tokens start with lat_...; Clerk Account API keys start with ak_....
api_key_expired403{ code, message, fix }A time-limited key (a demo sandbox key, or any key with an expiry) has expired.NoRequest a new sandbox key, or use a persistent account key.
demo_key_surface_disabled403{ code, message, fix }A demo sandbox key tried a surface it is not allowed to use: managed Telegram, webhook delivery, visual-asset uploads, image generation, visual-asset lifecycle, or app-setting changes.NoUse a persistent developer key for that operation. See Staging and evaluation.
demo_quota_exceeded403{ code, message, fix }A demo key hit its character / session / message cap.NoReuse the existing sandbox resource, or request a persistent developer key.

Demo sandbox key issuance (POST /v1/demo/sandbox-key)

These are specific to minting a no-auth demo key. The exact caps are in Authentication. Call GET /v1/demo/sandbox-key/availability first. It returns a normal success envelope and never creates demo state; branch on data.status and data.issuance_available rather than using an issuance error as a health check.

codeHTTPWhen it happensRetryableFallback
demo_sandbox_disabled503Public demo issuance is turned off (operator flag / maintenance / abuse controls). detail is the {code,…} envelope.No — issuance is offUse a persistent runtime key from self-serve account access, the console, or preview invite provisioning.
demo_sandbox_rate_limited429Too many demo keys were issued from this IP / fingerprint inside the issuance window.Yes, after the window (the fix tells you roughly how long)Wait the window and retry, or use a persistent runtime key. Do not re-mint a demo key per eval run — use a persistent staging key instead.
demo_input_too_large413A demo-key message exceeded the input-character cap.NoShorten the message, or use a persistent developer key.
demo_sandbox_challenge_required403The operator enabled a browser challenge and no turnstile_token was sent.No (until you provide a token)Submit a Cloudflare Turnstile token, or use a persistent runtime key.
demo_sandbox_challenge_failed403The submitted challenge token did not verify.NoRetry the challenge, or use a persistent runtime key.
demo_sandbox_challenge_unavailable503Challenge verification was momentarily unreachable.Yes, laterRetry later, or use a persistent runtime key.
demo_sandbox_missing_client_ip400The request origin could not be identified for rate limiting (rare).NoRetry from a normal browser, or use a persistent runtime key.

For a visual POC or a repeated eval loop, do not fight the demo issuance rate limit — mint a persistent staging key through self-serve account access or preview provisioning and reuse it. See Staging and evaluation.

Server / transient (5xx — retry with backoff)

code (or shape)HTTPWhen it happensRetryableFallback
internal_error500An unhandled server error on /v1. The envelope carries a trace_id in fix.Yes (with backoff)Retry; if it persists, contact support with the trace_id from fix.
image_engine_error502A transient failure in the image engine during a probe/inventory run.YesRetry.
(plain string) “Database connection temporarily unavailable…“503The database was momentarily unavailable. detail is a plain string.Yes (with backoff)Retry after a moment.
(plain string) “API connection pool is busy…“503The server’s connection pool was saturated. detail is a plain string.Yes (with backoff)Retry after a moment.
no_trace / unscoped_context / demo_sandbox_rate_limit_secret_missing / demo_sandbox_turnstile_secret_missing500Server misconfiguration (these should not occur in normal operation).No (server-side fix)Contact support; do not loop-retry.

Failures that arrive as events, not HTTP errors

A reply is asynchronous, so a reply that fails after POST /messages was accepted does not surface as an HTTP error — POST /messages already returned 202-style accepted with a cursor. Watch the event log for:

  • message.failed (role: "system") — the reply for a turn failed. Payload is { turn_index, reason, recoverable }. If recoverable is true, the turn is eligible for the platform’s reply-recovery sweep; if false, prompt the user to resend.

So a robust client handles errors in two places: the synchronous error envelope on the call you made, and message.failed on the session event stream.

Client checklist

  • Branch on body.detail.code for the envelope errors; branch on HTTP status for the plain-string auth 401/403 and the infrastructure 503s.
  • Retry only 429 (after its window), 502, 500 internal_error, and the infrastructure 503s — with backoff. Never blind-retry a 4xx request error.
  • Treat 409 session_exited as terminal: open a new session.
  • Treat 402 budget_exceeded as “out of credits,” not “try again now.”
  • Subscribe to message.failed so an async reply failure is not silently lost.