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

# Errors

> Stable machine-readable codes you can branch on.

Every error returns the same shape:

```json theme={null}
{
  "error": {
    "code": "insufficient_credit",
    "message": "Balance is $0.00. Top up to continue.",
    "balance_usd": 0,
    "required_usd": 0.15
  }
}
```

**Branch on `code`, never on `message`.** Codes are stable and will not change meaning. Messages are written for humans and may be reworded.

## Codes

| Code                  | Status | Meaning                                                  | What to do                                       |
| --------------------- | ------ | -------------------------------------------------------- | ------------------------------------------------ |
| `unauthorized`        | 401    | Missing or invalid key                                   | Check the `Authorization` header                 |
| `forbidden`           | 403    | API access not enabled for this account                  | Contact us; do not retry                         |
| `insufficient_credit` | 402    | Balance is empty                                         | Top up. Carries `balance_usd` and `required_usd` |
| `spend_cap_reached`   | 402    | This month's cap is reached                              | Raise the cap. Carries `cap_usd` and `spent_usd` |
| `rate_limited`        | 429    | Too many requests                                        | Back off and retry                               |
| `invalid_request`     | 400    | Malformed or missing parameter                           | Fix the request; retrying will not help          |
| `not_found`           | 404    | The video could not be found or has no downloadable file | Check the URL                                    |
| `upstream_error`      | 502    | A provider or model failed                               | Retry with backoff                               |
| `internal_error`      | 500    | Our fault                                                | Retry with backoff; tell us if it persists       |

## Which to retry

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'ui-sans-serif, system-ui, -apple-system, sans-serif','fontSize':'14px','primaryColor':'#F4F4F5','primaryTextColor':'#18181B','primaryBorderColor':'#D4D4D8','lineColor':'#A1A1AA','tertiaryColor':'#FAFAFA','clusterBkg':'#FAFAFA','clusterBorder':'#E4E4E7'}}}%%
flowchart TD
    E(["Error response"]) --> C{"error.code"}

    C -->|"rate_limited<br/>upstream_error<br/>internal_error"| W["Back off, then retry<br/>with the <b>same</b><br/>Idempotency-Key"]
    C -->|"insufficient_credit"| T["Top up the balance"]
    C -->|"spend_cap_reached"| S["Raise the cap<br/>in Settings"]
    C -->|"invalid_request<br/>not_found"| F["Fix the request.<br/>Retrying will not help"]
    C -->|"unauthorized<br/>forbidden"| K["Check the key,<br/>or contact us"]

    W --> R(["Retry"])

    classDef billed fill:#FFF1E8,stroke:#FF5500,stroke-width:1.5px,color:#7A2900
    classDef free fill:#F4F4F5,stroke:#D4D4D8,stroke-width:1.5px,color:#3F3F46
    classDef stop fill:#FEF2F2,stroke:#DC2626,stroke-width:1.5px,color:#7F1D1D
    classDef good fill:#F0FDF4,stroke:#16A34A,stroke-width:1.5px,color:#14532D
    classDef action fill:#EFF6FF,stroke:#2563EB,stroke-width:1.5px,color:#1E3A8A
    class W,R good
    class F,K stop
    class T,S action
    class E,C free
```

<CodeGroup>
  ```typescript TypeScript theme={null}
  const RETRYABLE = new Set(['rate_limited', 'upstream_error', 'internal_error'])

  async function call(path: string, body: unknown, attempt = 0): Promise<Response> {
    const response = await fetch(`https://nouvelhq.com/api/v1/${path}`, {
      method: 'POST',
      headers: {
        authorization: `Bearer ${process.env.NOUVEL_API_KEY}`,
        'content-type': 'application/json',
        // Same key across retries, so a call that actually succeeded
        // server-side is never charged twice.
        'idempotency-key': idempotencyKeyFor(body),
      },
      body: JSON.stringify(body),
    })
    if (response.ok || attempt >= 3) return response

    const { error } = await response.clone().json()
    if (!RETRYABLE.has(error?.code)) return response

    await new Promise((r) => setTimeout(r, 2 ** attempt * 1000))
    return call(path, body, attempt + 1)
  }
  ```

  ```python Python theme={null}
  RETRYABLE = {"rate_limited", "upstream_error", "internal_error"}

  def call(path, body, idempotency_key, attempt=0):
      response = requests.post(
          f"https://nouvelhq.com/api/v1/{path}",
          headers={
              "Authorization": f"Bearer {API_KEY}",
              # Reused across retries so a success we never saw is not billed twice.
              "Idempotency-Key": idempotency_key,
          },
          json=body,
          timeout=300,
      )
      if response.ok or attempt >= 3:
          return response
      if response.json().get("error", {}).get("code") not in RETRYABLE:
          return response
      time.sleep(2 ** attempt)
      return call(path, body, idempotency_key, attempt + 1)
  ```
</CodeGroup>

<Warning>
  Always reuse the **same** `Idempotency-Key` across retries of one logical request. A fresh key per attempt defeats the protection and you will be charged for every attempt that quietly succeeded.
</Warning>

## Errors cost nothing

No error response is ever billed. You are only charged for work delivered.
