# Errors & retries
> The error envelope, the retryable rule, and the full error code table.

> The table below is auto-generated from Beatra's public runtime error taxonomy.

Every public Beatra error response uses the standard error envelope. The `error.code` field is one of the stable public values below; the `error.retryable` boolean tells the client SDK whether a naive retry could succeed.

Every error body is:

```json
{
  "error": {
    "code": "<machine code>",
    "type": "<error family>",
    "message": "<human description>",
    "retryable": true,
    "param": "/field",
    "details": {},
    "request_id": "req_..."
  }
}
```

## How to branch

| You see | You should |
|---|---|
| `retryable: true` | After a brief backoff, retry the **exact same request** with the same `Idempotency-Key`. Honor `Retry-After` when present. `429` responses include `X-RateLimit-*` headers. |
| `retryable: false` | Retrying as-is produces the same outcome. Change the request, fix credentials, or surface the error to the user. Looping on a non-retryable error is a bug. |
| `error.param` | When present, a JSON Pointer (RFC 6901) to the offending field in the request body. |
| `error.type` / `error.details` / `error.request_id` | Stable family, correction facts, and the trace identifier. |
| `error.message` | Diagnostic prose, not a branchable contract. For a task that reaches terminal failure after a provider response, it preserves the provider's wording after credential, signed-URL, control-character, and length safety handling. |

When present, `error.details.recovery` adds a branchable action without changing the top-level error envelope. `requires_input_change` says whether the submitted content must change. `client_request_id: new` means the corrected or terminal work is a new logical request; otherwise follow `retryable` and the endpoint's idempotency rules.

```json
{
  "details": {
    "recovery": {
      "action": "fix_arguments_and_resubmit",
      "requires_input_change": true,
      "client_request_id": "new"
    }
  }
}
```

## Auth / Account

| code | HTTP | retryable | description |
|---|---|---|---|
| `unauthorized` | 401 | no | Missing or malformed authentication credentials. Send `Authorization: Bearer <API key>`. |
| `invalid_api_key` | 401 | no | API key is invalid, disabled, revoked, or unknown. Create or rotate keys in the console (Developer → API keys) and retry with the new key. |
| `api_key_region_mismatch` | 400 | no | API key region prefix does not match this endpoint. Call the API host matching the key's region, or use a key issued for this region. |

**3 more**

| code | HTTP | retryable | description |
|---|---|---|---|
| `tenant_disabled` | 403 | no | Tenant has been suspended or disabled. Contact support; retrying cannot succeed. |
| `forbidden` | 403 | no | Caller does not have permission for this operation. Use a credential with the required permission; retrying unchanged cannot succeed. |
| `email_send_failed` | 502 | yes | Login-code email could not be delivered (transient send failure). Request a new code after a brief backoff. |

## Billing

| code | HTTP | retryable | description |
|---|---|---|---|
| `insufficient_balance` | 402 | no | Insufficient credits: this request was not started and nothing was charged. Add credits at https://console.beatra.ai/wallet?intent=buy — they take effect immediately; then retry this same request. No reinstall or re-authorization is needed. |
| `billing_failed` | 500 | yes | Charge or refund attempt failed after retries. Retry with the same Idempotency-Key / client_request_id; the identity guarantees no double charge. |
| `payment_provider_unavailable` | 503 | no | Payment checkout provider is not configured for this payment method. Choose another payment method in the console. |

## Capacity and availability

| code | HTTP | retryable | description |
|---|---|---|---|
| `rate_limited` | 429 | yes | Caller-side rate limit exceeded. Retry after the `Retry-After` interval; `X-RateLimit-*` headers report the active budget. |
| `model_unavailable` | 503 | yes | The requested model is not currently available. Retry with backoff, switch to another selectable model (`beatra.models.list` or `GET /v1/catalog/models`), or use `model=auto`. |
| `model_request_failed` | 502 | yes | The model request failed after retryable service errors. Retry with backoff; if it persists, adjust inputs or choose another model. |

**4 more**

| code | HTTP | retryable | description |
|---|---|---|---|
| `model_timeout` | 504 | yes | The model request timed out. Retry with backoff. |
| `model_rate_limited` | 429 | yes | Model capacity was rate-limited. Retry after a backoff. |
| `model_output_invalid` | 502 | yes | The model completed successfully, but the generated artifact could not be downloaded after repeated attempts. The task is failed and no result artifact is available. |
| `internal_error` | 500 | yes | Unclassified server-side error. Retry with the same idempotency identity; if it persists, report the request_id. |

## Other error codes

**Request validation 15 ⌄**

| code | HTTP | retryable | description |
|---|---|---|---|
| `invalid_request` | 422 | no | Request body fails structural validation. `param` points to the first offending field; `details.errors` lists every failure with its `loc` and reason. Fix the named fields and resend. |
| `skill_package_format_invalid` | 422 | no | Skill package identity is malformed. Use a lowercase hyphenated slug of at most 64 characters and a valid SemVer package version. |
| `unsupported_capability` | 400 | no | Capability does not exist or is disabled. `GET /v1/catalog/capabilities` lists the currently available capabilities. |
| `unsupported_model` | 400 | no | Model does not exist or is not selectable for this capability. Discover selectable models with `beatra.models.list` or `GET /v1/catalog/models`, or use `model=auto`. |
| `unsupported_option` | 422 | no | The selected concrete model does not support a requested value or control. `param` identifies the field and `details.candidate_failures[]` carries a specific failure code such as `unsupported_resolution`, `unsupported_duration`, or `unsupported_aspect_ratio`, plus the received and supported values. Change the named field or choose a different model; with `model=auto` this is returned only when no candidate can satisfy the request. |
| `no_eligible_model` | 422 | no | No live model can honor the complete `model=auto` request. `details.candidate_failures[]` lists each provider-neutral rejection; change only the incompatible fields or pick a listed concrete model. |
| `voice_model_incompatible` | 422 | no | The requested voice does not resolve on the requested model. Use `model=auto`, a compatible model, or another voice from the voices list. |
| `unsupported_input_modality` | 422 | no | The selected model does not support an input modality in the request (such as image, audio, or file input). `param` points to the first offending field; remove it or choose a model that supports it. |
| `context_length_exceeded` | 422 | no | The request exceeds the selected model's context window. Shorten the input or choose a model with a larger context window. |
| `safety_rejected` | 400 | no | The request did not pass content safety review. Ask the user to review the prompt and every input resource, then edit or replace content that may be unsafe before submitting a new task with a new client_request_id. Retrying the same content will not help. |
| `content_filtered` | 502 | no | A safety filter blocked the output mid-generation. Rephrase the prompt or switch models; an identical retry is usually blocked again. |
| `not_found` | 404 | no | Requested HTTP resource or endpoint was not found. Check the path and identifier; retrying unchanged cannot succeed. |
| `streaming_not_yet_implemented` | 501 | no | Legacy compatibility code for older deployments where `stream=true` was recognized but not enabled. |
| `invalid_window` | 422 | no | Time range is invalid. |
| `invalid_cursor` | 400 | no | Pagination cursor is malformed. |

**Callbacks 3 ⌄**

| code | HTTP | retryable | description |
|---|---|---|---|
| `callback_url_required` | 422 | no | A callback URL is required when a callback signing key is selected. Add `callback_url` or remove `callback_signing_key_id`. |
| `callback_signing_key_not_found` | 404 | no | Callback signing key does not exist or is not visible to this account. List signing keys in the console and pass a current key id. |
| `callback_signing_key_inactive` | 422 | no | Callback signing key is retired and cannot be selected for a new task. Select an active key, or omit the key id for unsigned delivery. |

**Media inputs 5 ⌄**

| code | HTTP | retryable | description |
|---|---|---|---|
| `invalid_media` | 422 | no | Media input is malformed or cannot be decoded. Re-export or re-encode the file and submit it again; `param` names the offending field. |
| `unsupported_media_type` | 422 | no | Media input type is not supported for this capability. Convert to a supported format (see the capability page or model constraints) and resend. |
| `media_too_large` | 413 | no | Media input exceeds the allowed size. Compress or trim the input, or upload it first and pass an artifact reference. |
| `media_fetch_failed` | 422 | yes | Beatra could not access an input resource. Check that the resource is reachable over HTTPS, or upload it and pass an artifact reference. |
| `upload_stalled` | 408 | yes | An upload stopped sending data for too long, or ran past the upload time limit. Send the same PUT to the same upload URL again; if the upload grant has expired, request a new one with `beatra.assets.upload`. |

**Tasks and resources 11 ⌄**

| code | HTTP | retryable | description |
|---|---|---|---|
| `task_not_found` | 404 | no | task_id does not exist or does not belong to this account. Use the task_id returned at creation; list recent tasks with `GET /v1/tasks` or `beatra.tasks.list`. |
| `conflict` | 409 | no | The operation conflicts with the resource's current state — for task cancellation this means execution continues. Follow `details.action` (usually: keep polling the same task); do not resubmit the work. |
| `result_undeliverable` | 502 | no | The completed result could not be delivered because it is too large, too deeply nested, or not valid JSON. Do not resubmit the same lookup. |
| `artifact_not_found` | 404 | no | artifact_id does not exist or does not belong to this account. Upload the media again and use the newly returned artifact id. |
| `voice_not_found` | 404 | no | voice_id does not exist or does not belong to this account. List available voices with `GET /v1/voices` or `beatra.voices.list`. |
| `voice_activation_required` | 422 | no | This voice is a free preview and can't be used for speech until it is activated. Confirm the activation price with the user, then call beatra.voices.activate. |
| `price_confirmation_required` | 422 | no | The activation quote has expired or changed. Show the new quote to the user, get their consent, then call beatra.voices.activate again with the new confirmation_token and confirm_credits. |
| `voice_preview_expired` | 422 | no | This preview voice has expired and can no longer be activated or used. Clone the voice again. |
| `voice_sample_rejected` | 422 | no | The voice sample was rejected. See details.reason, fix the sample, and clone again with a new client_request_id. |
| `voice_limit_reached` | 409 | no | This account already has the maximum number of cloned voices waiting for activation. Activate one, or delete one in the desktop app or with `DELETE /v1/voices/{voice_id}`; waiting voices also expire after 7 days. |
| `voice_activation_daily_limit` | 429 | no | This account has activated 3 voices today, the daily limit. Nothing was charged. To activate more voices today, contact Beatra support; the limit resets at 00:00 UTC (08:00 Beijing time). |

**Idempotency and response mode 2 ⌄**

| code | HTTP | retryable | description |
|---|---|---|---|
| `idempotency_conflict` | 409 | no | Same `Idempotency-Key` / `client_request_id` was used with a different request body. Submit changed work with a NEW identity; to retry identical work unchanged, reuse the original. Inspect the existing task before choosing. |
| `request_in_progress` | 409 | yes | Same idempotency identity matches an in-flight request. Retry later with the same identity; the original response will be returned. |

## Asynchronous task failures

A generation can be admitted successfully and later end with `status: failed`. REST task reads, MCP `beatra.tasks.get`, and callbacks return the same `Task.error` object with `code`, `type`, `message`, `retryable`, optional `param`, optional `details`, and `request_id`. A failed task read is the task's final state, not an MCP protocol error.

`model_output_invalid` means the model completed successfully but Beatra could not download the generated artifact after repeated attempts. No result artifact is available for that failed task.

## MCP tool errors

A failed MCP tool call returns a tool result with `isError: true` and `structuredContent.error` carrying the **same vocabulary** as the REST envelope — `code`, `type`, `message`, `retryable`, optional `param` (JSON Pointer into the tool arguments), optional `details` — plus the JSON-RPC `jsonrpc_code`. Branch on `code` and `retryable` exactly as with REST.

One MCP-only code exists: `insufficient_scope` (the OAuth grant lacks a required scope; `missing_scopes` names them — re-run the Beatra device authorization; retrying without re-authorizing cannot succeed). Unexpected server failures surface as `internal_error` tool results, never as bare HTTP errors.

---

Generated from 52 public runtime error codes. 25 internal, admin, or provider-ingress codes are intentionally omitted from public docs.
