Casola Avatar Interaction API

6. Error reference

Every error is a JSON body with an error field and the matching HTTP status. Some carry a detail with a more specific code, and some carry extra fields (max_chars, retry_after, reset_at).

{ "error": "profile_mismatch", "message": "That profile does not belong to the avatar this session resolved to.", "profile_id": "019fe4a0-…" }

Capacity — POST /api/v1/sessions

Status Error Meaning Retry?
503 fleet_busy Full past the wait horizon, or no box registered No — surface to the user
503 custom_avatar_unavailable No live box can currently serve custom avatars Yes, with backoff
422 custom_avatar_unsupported Every registered box lacks custom-avatar support No — a fleet-level condition
503 no_box_available No box could be leased for this operation Yes
503 signing_key_not_configured Environment misconfiguration No — contact support
402 quota body Cumulative minutes exhausted (anonymous tier); carries reset_at, retry_after After retry_after

202 queued is not an error — see session lifecycle.

A 503 custom_avatar_unavailable means the platform knows of no box that has proven it can materialize custom avatars. It clears on its own once the fleet recovers; stock avatars (persona) usually still work meanwhile, which makes a stock fallback a reasonable degradation path.

Avatar selection

Status Error Meaning
404 avatar_not_found No such avatar, or it is deleted
400 avatar_not_published The avatar has no published version to mint against
400 invalid_avatar_id Missing or non-string
400 avatar_ref_avatar_id_exclusive Sent both — pick one
400 avatar_ref_persona_exclusive Custom avatar and stock persona both named
400 invalid_avatar_version_id Missing or non-string
400 version_not_published The pinned version is not published/sealed
403 avatar_not_in_workspace The avatar belongs to another workspace
404 voice_version_not_found Unknown voice_ref
403 voice_not_in_workspace The voice belongs to another workspace

Prompt, profile and config

Status Error Meaning
400 invalid_profile_id Missing or non-string
400 profile_mismatch The profile does not belong to the resolved avatar (or no avatar was resolved)
400 invalid_context_system_prompt Not a string, or over 40,960 characters
400 invalid_context_backstory Not a string, or over 16,384 characters
400 system_prompt_too_long Profile authoring; carries max_chars
400 backstory_too_long Profile authoring; carries max_chars
400 invalid_config + detail unknown_config_key:*, config_not_boolean:*, config_not_integer:*, config_out_of_range:*, config_too_large
400 invalid_config_json + detail Same vocabulary, profile authoring
400 config_too_large Merged session+profile config over 1,024 bytes
400 invalid_tools_json + detail Profile authoring: not_an_object, unknown_tools_field:*, invalid_tool_name, too_many_tools, tool_name_conflict:*
400 invalid_field_type A profile text field was sent as a non-string
409 profile_is_default Cannot delete the default profile — switch first
403 stock_read_only Profile mutation on a stock avatar
404 not_found Unknown avatar or profile on the profile routes

Credential fences

All 403, all meaning “this credential kind may not send that parameter” — see 1. Authentication.

Error Parameter refused
custom_avatar_not_allowed avatar_ref / voice_ref from a publishable key
avatar_version_pin_not_allowed avatar_version_id
profile_pin_not_allowed profile_id
tools_context_not_allowed tools, extra_tools, context.*, config, extra_*
session_cap_not_allowed session_cap_seconds
extra_tools_not_allowed (400) extra_tools on the first-party /api/sessions route

Generic

Status Error Meaning
400 invalid_json Body did not parse
400 missing_params A required path parameter is absent
401 unauthorized Missing/invalid credential
403 forbidden Authenticated but not permitted
403 workspace_mismatch A workspace-bound key named a different workspace in the path
403 custom_avatars_disabled The workspace lacks the custom_avatars feature (authoring only); carries feature + workspace_id
400 invalid_response_language Not a recognizable BCP-47 tag or language name
400 invalid_observability Not a boolean
400 invalid_session_cap_seconds Not an integer in 60–1200
400 agent_claim_too_large agent object over 1,024 bytes
404 not_found Unknown session on the lifecycle routes
403 invalid_seat_token The seat token does not match that session

Handling patterns

Retry only what is retryable. fleet_busy and custom_avatar_unsupported are terminal for this attempt — retrying in a tight loop makes a capacity problem worse. custom_avatar_unavailable and no_box_available are transient; exponential backoff starting around 2 s is reasonable.

Never retry a 4xx unchanged. Every 400/403 here is a statement about the request, not the system’s mood.

Distinguish “busy” from “broken” in your UI. 202 queued with a position and ETA is a good experience if you show it; a spinner that silently hides a queue is not.

switch (res.status) {
  case 201: return connect(body);
  case 202: return showQueue(body.position, body.eta_seconds), pollAgain(body.queue_ticket);
  case 402: return showQuotaExhausted(body.reset_at);
  case 503:
    return body.error === 'fleet_busy'
      ? showAllLinesBusy()          // terminal for now
      : retryWithBackoff();         // custom_avatar_unavailable, no_box_available
  default:  return reportBug(body.error, body.detail);
}

Back to the index.