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

# Errors

> How Roughy reports failures — the response envelope, the codes you can rely on, and how to correlate a failed request with the server-side logs.

Every Roughy API endpoint reports failures with the same response
shape, regardless of which endpoint failed or which HTTP status
code is returned. Successful responses are the resource directly
(no wrapper); error responses are always the envelope below.

## Envelope

```json theme={null}
{
  "error": {
    "code": "validation_error",
    "message": "Upload-Length must be at least 64 bytes.",
    "fields": [
      { "field": "body.name", "message": "Field required" }
    ]
  }
}
```

| Field     | Type        | When set                                                                     | Use it for                                                                                                             |
| --------- | ----------- | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `code`    | string enum | always                                                                       | Branching client logic. Switch on this, not on `message`.                                                              |
| `message` | string      | always                                                                       | Showing the user a single-line description. Safe to surface verbatim.                                                  |
| `fields`  | array       | only when `code` is `validation_error` and one or more request fields failed | Highlighting bad form fields. `field` is a dotted path rooted at the request location (`body.…`, `query.…`, `path.…`). |

## Error codes

| `code`                   | Typical status | What it means                                                                                                                                                                                                                                                                                    |
| ------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `bad_request`            | 400            | Malformed request that isn't covered by a more specific code.                                                                                                                                                                                                                                    |
| `unauthenticated`        | 401            | Missing or invalid `Authorization: Bearer sk_…`.                                                                                                                                                                                                                                                 |
| `insufficient_credits`   | 402            | Your credit balance is at or below zero, so `POST /v1/assets` rejects the upload before it starts — top up first. An upload that *starts* with a positive balance but can't cover the final per-minute charge instead lands the asset in `pending_payment`, which activates on your next top-up. |
| `forbidden`              | 403            | Authenticated, but not allowed for this resource. Rare — most ownership failures surface as `not_found` so an attacker can't probe existence.                                                                                                                                                    |
| `not_found`              | 404            | Resource doesn't exist, or doesn't belong to you.                                                                                                                                                                                                                                                |
| `conflict`               | 409            | A state-machine violation not covered by one of the more specific 409 codes below.                                                                                                                                                                                                               |
| `asset_processing`       | 409            | `POST /v1/cuts`: the asset is still being prepared and isn't cuttable yet. Transient — back off for the `Retry-After` seconds and retry.                                                                                                                                                         |
| `asset_payment_required` | 409            | `POST /v1/cuts`: the asset is prepared but unpaid (`pending_payment`). Top up, then retry.                                                                                                                                                                                                       |
| `asset_failed`           | 409            | `POST /v1/cuts`: the asset failed preparation and can't be cut. Terminal — re-upload.                                                                                                                                                                                                            |
| `asset_not_ready`        | 409            | `POST /v1/renders`: a **video** output needs the asset `ready` (an audio output instead gates on `cuttable`, like a cut).                                                                                                                                                                        |
| `modifier_not_ready`     | 409            | `POST /v1/renders`: the `cut_id` names a cut of this asset that isn't `completed` yet.                                                                                                                                                                                                           |
| `unsupported_format`     | 409            | A cut/subtitle export `format` that isn't available for this entity.                                                                                                                                                                                                                             |
| `precondition_failed`    | 412            | A required header / preflight condition wasn't met (e.g. `Tus-Resumable`).                                                                                                                                                                                                                       |
| `payload_too_large`      | 413            | Body / `Upload-Length` exceeds the documented cap.                                                                                                                                                                                                                                               |
| `unsupported_media_type` | 415            | Wrong `Content-Type` on the request body.                                                                                                                                                                                                                                                        |
| `validation_error`       | 422            | The request was well-formed but one or more fields failed validation. `fields[]` lists each one.                                                                                                                                                                                                 |
| `internal_error`         | 500            | Something failed server-side. Include the `X-Request-Id` header in a support ticket so we can trace the request.                                                                                                                                                                                 |
| `upstream_error`         | 502            | A downstream service rejected our integration. Retry won't help without changing the request.                                                                                                                                                                                                    |
| `upstream_unavailable`   | 503            | A downstream service is transiently unreachable. Retry after the `Retry-After` header's seconds.                                                                                                                                                                                                 |

Only the transient codes carry a `Retry-After` header: `asset_processing`
(on `POST /v1/cuts`, while the asset is still being prepared) and
`upstream_unavailable`. Back off for the header's seconds and retry the
same request.

The list is closed — Roughy won't introduce a new code without a
backward-compatible documentation update. If you encounter a code
not listed here, treat it as `internal_error`.

## Correlating with logs

Every response (success or error) carries an `X-Request-Id`
response header. Our server-side structured logs bind the same
id to every line emitted while processing the request, so
including the header value in a support ticket lets us trace the
chain.

The id is **header-only** — the error envelope body does not
echo it. When opening a ticket, grab the `X-Request-Id` from the
response (visible in your browser's network panel or `curl -v`)
and paste it alongside the JSON.

## Example — `validation_error` with `fields`

```http theme={null}
POST /v1/projects HTTP/1.1
Authorization: Bearer sk_live_…
Content-Type: application/json

{ "name": "" }
```

```http theme={null}
HTTP/1.1 422 Unprocessable Content
X-Request-Id: 01HZ6P7K9V3QMZB2ACS9YYCP9N
Content-Type: application/json

{
  "error": {
    "code": "validation_error",
    "message": "String should have at least 1 character",
    "fields": [
      { "field": "body.name", "message": "String should have at least 1 character" }
    ]
  }
}
```

## Example — `not_found`

```http theme={null}
GET /v1/projects/00000000-0000-0000-0000-000000000000 HTTP/1.1
Authorization: Bearer sk_live_…
```

```http theme={null}
HTTP/1.1 404 Not Found
X-Request-Id: 01HZ6P7K9V3QMZB2ACS9YYCP9N
Content-Type: application/json

{
  "error": {
    "code": "not_found",
    "message": "Project 00000000-0000-0000-0000-000000000000 not found"
  }
}
```

Note `fields` is absent — it's only present for validation
errors.

## Success responses

For symmetry, the success side: status codes carry the meaning,
the body is the resource directly. No wrapper.

| Status           | Body                                                  |
| ---------------- | ----------------------------------------------------- |
| `200 OK`         | The resource.                                         |
| `201 Created`    | The newly-created resource, plus a `Location` header. |
| `202 Accepted`   | Reference identifiers for async work.                 |
| `204 No Content` | Empty body (per RFC 9110 §15.3.5).                    |

Listings carry pagination metadata — `{ items: [...], total,
limit, offset }`. That's a pagination shape, not a generic
envelope.
