> ## Documentation Index
> Fetch the complete documentation index at: https://docs.viggle.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and recovery

> Handle V1 errors predictably.

All V1 errors use a single envelope, `ErrorBody`, shared by synchronous HTTP failures (wrapped in `{"error": ...}`), asynchronous resource `error` fields, and render SSE events:

```json theme={null}
{
  "error": {
    "code": "INSUFFICIENT_CREDITS",
    "message": "Insufficient credits to start this job",
    "retryable": false,
    "request_id": "req_123abc",
    "details": {},
    "remediation": {
      "action": "add_credits",
      "retry_after_ms": null
    }
  }
}
```

| Field | Type | Description |
| - | - | - |
| `code` | string | `SCREAMING_SNAKE_CASE` value from the closed vocabulary below. Use this for program logic. |
| `message` | string | Human-readable description. Do not parse it. |
| `retryable` | boolean | Whether retrying the same request could succeed. |
| `request_id` | string or null | Correlates the response with Viggle logs and support. `null` only when an asynchronous worker cannot recover the originating request ID. |
| `details` | object | Additional structured context. May be empty. |
| `remediation` | object | `{action, retry_after_ms}` — a machine-readable hint for what to do next. `retry_after_ms` is `null` unless the recommended action is timed. |

Internal implementation codes are never exposed.

## Error codes

| `code` | HTTP | Typical cause | Recovery |
| - | - | - | - |
| `UNAUTHENTICATED` | 401 | No credential supplied. | Add `Authorization: Bearer YOUR_API_KEY`. |
| `INVALID_CREDENTIAL` | 401 | API key or OAuth token is invalid or expired. | Replace the credential. |
| `FORBIDDEN` | 403 | Credential does not grant access to this resource. | Use a credential scoped to the right project/principal. |
| `INVALID_REQUEST` | 400 | Missing input, malformed field, or unsupported parameter combination. | Correct the request from the endpoint's parameter table. |
| `INVALID_CHARACTER` / `INVALID_MOTION` | 400 | Supplied character/motion source failed validation. | Check the source file, URL, or asset ID. |
| `UNSUPPORTED_MEDIA` | 400 / 422 | File type or encoding not accepted. | Convert to a supported format. |
| `CONTENT_POLICY_VIOLATION` | 422 | Media rejected by content policy. | Do not retry unchanged; use different source media. |
| `NO_HUMANS_DETECTED` | 422 | No person detected in the source image/video. | Use source media with a clearly visible person. |
| `VIDEO_TOO_LONG` / `VIDEO_TOO_LARGE` / `VIDEO_RESOLUTION_TOO_HIGH` / `VIDEO_DIMENSIONS_MUST_BE_EVEN` / `VIDEO_UNREADABLE` / `VIDEO_INACCESSIBLE` | 400 / 422 | Motion source video fails a specific constraint. | Trim, re-encode, resize, or re-host the video and retry. |
| `IMAGE_REQUIRED` / `MOTION_VIDEO_REQUIRED` | 400 | Required source omitted. | Supply the missing `image`/`image_url` or `motion_video`/`motion_video_url`. |
| `INVALID_BACKGROUND_MODE` | 400 | `background_mode`/`aspect_ratio` combination not supported. | Use one of the documented enum values; note only `aspect_ratio: source` is served today. |
| `UPLOAD_REQUIRED` | 400 / 409 | `POST /v1/renders` (JSON) called before all declared uploads completed. | Finish every `PUT` from `renders/prepare`, then include a matching `upload_completions` entry. |
| `UPLOAD_MISMATCH` | 400 | An `upload_completions` entry doesn't match a declared upload slot. | Re-check `upload_handle` values against the `prepare` response. |
| `UPLOAD_EXPIRED` | 409 | A direct-upload URL's `expires_at` passed before the `PUT`. | Call `renders/prepare` again for a fresh upload plan. |
| `DRAFT_NOT_FOUND` | 404 | `draft_id` doesn't exist or already expired. | Call `renders/prepare` again. |
| `DRAFT_ALREADY_CONSUMED` | 409 | The draft was already used to create a render. | Use the render returned by that earlier call; don't resubmit the same draft. |
| `IDEMPOTENCY_KEY_REUSED` | 409 | `Idempotency-Key` reused with different normalized input. | Use a new key for a genuinely new render, or resend with the original input to fetch the original render. |
| `CHARACTER_NOT_FOUND` / `MOTION_NOT_FOUND` / `RENDER_NOT_FOUND` / `MOTION_TEMPLATE_NOT_FOUND` / `NOT_FOUND` | 404 | The resource doesn't exist, or belongs to another account — the two cases are deliberately indistinguishable. | Verify the ID and that it belongs to the calling principal. |
| `MOTION_NOT_READY` | 400 / 409 | A reusable Motion is still processing. | Poll the Motion until `ready`, then submit the Render. |
| `MOTION_MODEL_MISMATCH` | 400 | Requested model/skeleton incompatible with how the Motion was created. | Re-check the `model`/`download_type` value against the Motion's `type`. |
| `ID_ALREADY_EXISTS` | 409 | A client-supplied `task_id` is already in use. | Use a new `task_id`, or treat the existing task as the result. |
| `RENDER_NOT_CANCELLABLE` | 409 | Reserved — Render cancellation is not a published client operation. | Not applicable to normal integrations. |
| `EXTRACTION_UNAVAILABLE` | 502 | The 3D extraction backend is temporarily unavailable. | Retry with backoff. |
| `EXTRACTION_FAILED` | 502 | The 3D extraction itself failed. | Inspect `vsplat.error`/`glb.error`; retry only if `retryable` is `true`. |
| `INSUFFICIENT_CREDITS` | 402 | Not enough balance to start the operation. | Add credits in the [Dashboard](https://portal.viggle.ai/dashboard). |
| `RATE_LIMITED` | 429 | Too many requests. | Back off using `remediation.retry_after_ms` or the `Retry-After` header. |
| `SERVICE_BUSY` / `ALL_WORKERS_BUSY` / `WORKER_STOPPED` | 503 | Temporary processing capacity issue. | Retry with exponential backoff. |
| `INTERNAL_ERROR` / `PROCESSING_FAILED` / `TASK_FAILED` | 500 | Unexpected server-side failure. | Retry only if `retryable` is `true`; contact support with `request_id` if persistent. |
| `RESULT_EXPIRED` | 400 | Requested output's signed URL window has passed. | Re-request the export/download endpoint for a freshly signed URL. |
| `AVATAR_NOT_FOUND` / `ANIMATION_NOT_FOUND` | 404 | Reserved for requests still routed through the legacy migration proxy. New integrations never trigger these — use Character/Motion `export` instead. | Migrate off `/v1/avatars` and `/v1/animations`; see [Migrate from Legacy](/v1/production/migrate-from-legacy). |

## Retry policy

Use `retryable` and `remediation` on the error itself rather than hardcoding a policy per HTTP status — they're specific to what actually happened. As a baseline: retry temporary service failures (`RATE_LIMITED`, `SERVICE_BUSY`, `ALL_WORKERS_BUSY`, `WORKER_STOPPED`, and `retryable: true` `INTERNAL_ERROR`s) with exponential backoff, honoring `retry_after_ms` or `Retry-After` when present. Don't retry a 400-, 401-, 402-, 403-, 404-, 409-, or 422-level response unchanged.

The JSON draft form of `POST /v1/renders` requires `Idempotency-Key`, so it's safe to retry after a network timeout with the same key and body — you'll get the original render back rather than a duplicate. The `multipart/form-data` form of `POST /v1/renders`, and Character/Motion creation, have no such key: if a create response was received, use its resource ID; otherwise resolve the outcome with your request logs and `X-Request-Id` before submitting again.

## Failed async jobs

An HTTP `200` response from a status endpoint can still describe a failed job — the resource's own `error` field carries the same `ErrorBody`:

```json theme={null}
{
  "id": "render_123abc",
  "status": "failed",
  "error": {
    "code": "TASK_FAILED",
    "message": "The render could not be completed.",
    "retryable": false,
    "request_id": "req_123abc",
    "details": {},
    "remediation": { "action": "contact_support", "retry_after_ms": null }
  }
}
```

Treat `failed` as terminal. Keep the resource ID and `request_id` for support.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.