# Errors

The error envelope, every stable error code, and what your agent should do for each.

## The envelope

Every `/api/v1` error body has at least these keys (`validation_error` 422s add
`errors`, the raw validation list):

```json
{
  "detail": {
    "error_code": "insufficient_credits",
    "message": "Insufficient credits: ... Top up at https://www.cliphi.com/pricing.",
    "billed": false
  }
}
```

`message` is written to be relayed to a human verbatim. `billed` says whether
money moved (always `false` on request errors).

Renders can also fail asynchronously: `render_status: "failed"` arrives
inside a successful poll. Branch on fields, never on HTTP codes alone.

## Error codes

| Code | HTTP | Meaning / agent action |
| --- | --- | --- |
| `missing_api_key` | 401 | No key sent. Message says where to create one. |
| `invalid_api_key` | 401 | Wrong or revoked key. |
| `invalid_url` | 400 | The URL is well-formed but not a usable video. Fix the input, do not retry unchanged. (A string that is not a URL at all fails earlier as 422 `validation_error`.) |
| `insufficient_credits` | 402 | Message includes balance, required amount, top-up link. |
| `video_too_long` | 422 | Over the length limit (up to 3 hours by default; the message states the current limit). With fast-ack this often surfaces as a failed poll instead. |
| `throttled` | 429 | Too many jobs; retry after the stated wait. |
| `maintenance` | 503 | Temporary; retry later. |
| `upstream_error` | varies | Source site refused the video; the upstream status is preserved. Often transient. |
| `job_not_found` | 404 | Unknown job id. (Another account's job is 403 `access_denied`, never 404.) |
| `access_denied` | 403 | The resource belongs to another account. |
| `moment_not_found` | 404 | Unknown moment id for this job. |
| `moment_not_ready` | 409 | Job still processing; poll first. |
| `source_expired` | 409 | Source files are kept 30 days; this one aged out. Resubmit the video. |
| `nothing_to_render` | 409 | 16:9 with captions, title, description all off and no crop would equal the free preview. |
| `clip_too_long` | 409 | Moment exceeds the render length cap. |
| `render_in_progress` | 409 | Same render already running; poll instead. |
| `demo_unavailable` | 404 | Demo being refreshed; try later. |
| `not_found` / `method_not_allowed` | 404/405 | Wrong path or verb. |
| `http_error` | varies | Generic enveloped framework error. |
| `validation_error` | 422 | Request shape invalid; `errors` lists fields. |
| `internal_error` | 500 | Our side. Retry in a minute; nothing billed by the request itself. |

## Async render failures

`render_error_code` on a failed moment is one of:

| Code | Agent action |
| --- | --- |
| `insufficient_credits` | Top up, then render again. |
| `source_expired` | Resubmit the video. |
| `render_interrupted` | Safe to POST the render again. |
| `render_failed` | Relay `render_error`; do not claim anything about billing. |
