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

# Errors

> The standard error shape and every error code you might see.

Every error response follows the same JSON shape:

```json theme={null}
{
  "success": false,
  "error": "<error_code>",
  "message": "<human_readable>"
}
```

Handle this shape in your client — the HTTP status code tells you the
category, the `error` field tells you the exact reason, and `message`
is safe to surface to end users (no stack traces, no internal info).

## HTTP status codes

| Code  | Category            | Typical                                                                         |
| ----- | ------------------- | ------------------------------------------------------------------------------- |
| `200` | Success             | Sync result returned                                                            |
| `201` | Resource created    | File uploaded, key minted, webhook subscribed                                   |
| `202` | Accepted (async)    | Task queued; poll `/tasks/{id}`                                                 |
| `204` | No content          | Image/key/webhook deleted                                                       |
| `302` | Redirect            | `GET /runs/{id}/download`                                                       |
| `400` | Bad request         | Validation or preflight error                                                   |
| `401` | Unauthorized        | Bad / missing API key                                                           |
| `402` | Payment required    | Out of credits                                                                  |
| `403` | Forbidden           | Key valid but not permitted                                                     |
| `404` | Not found           | Run / file / image doesn't exist or belongs to another org                      |
| `408` | Timeout             | Sync ceiling exceeded; response tells you to retry async                        |
| `409` | Conflict            | Idempotency collision or invalid state                                          |
| `410` | Gone                | Resource existed but was deleted / expired                                      |
| `413` | Payload too large   | Upload exceeds tier limit                                                       |
| `422` | Validation          | Field-level rejection on the request body                                       |
| `429` | Rate limited        | See `Retry-After` header                                                        |
| `500` | Server error        | Our bug. Gets paged.                                                            |
| `501` | Not implemented     | Endpoint exists but feature isn't live yet                                      |
| `502` | Upstream error      | A dependency we rely on (model provider, storage, or rendering pipeline) failed |
| `503` | Service unavailable | Capacity exhausted or maintenance                                               |

## Error codes

### Authentication

| Code              | When                                                    | Remedy                                              |
| ----------------- | ------------------------------------------------------- | --------------------------------------------------- |
| `invalid_api_key` | Missing, malformed, or revoked `sk_live_*`              | Check `Authorization` header; rotate if compromised |
| `forbidden`       | Key OK but lacks permission (suspended org, wrong tier) | Check org status on `GET /verify`                   |

### Billing

| Code                   | When                                   | Remedy                                |
| ---------------------- | -------------------------------------- | ------------------------------------- |
| `insufficient_credits` | Org balance below the format's minimum | Top up via dashboard or contact sales |

### Validation

| Code                | When                                    | Remedy                                           |
| ------------------- | --------------------------------------- | ------------------------------------------------ |
| `invalid_request`   | Missing required field, bad enum value  | Fix payload per error message                    |
| `validation_error`  | Pydantic field-level error              | Error message has JSON pointer to the bad field  |
| `preflight_failed`  | LLM validator rejected (missing assets) | Attach the missing assets or rephrase the prompt |
| `payload_too_large` | File exceeds tier limit                 | Compress or upgrade tier                         |

### Resources

| Code        | When                                                                    | Remedy                                                       |
| ----------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ |
| `not_found` | Run / file / image / webhook ID doesn't exist OR belongs to another org | Check ID spelling; confirm you created it under the same org |
| `gone`      | Resource expired (30-day retention)                                     | Start a new run                                              |
| `conflict`  | Idempotency key reused with different body, or resource in wrong state  | Different key, or wait for terminal state                    |

### Runtime

| Code                         | When                                                                                                | Remedy                                          |
| ---------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `sync_timeout_upgrade_async` | Sync request hit 300s ceiling                                                                       | Resubmit with `async: true`; poll `/tasks/{id}` |
| `rate_limit_exceeded`        | Over per-key tier limit                                                                             | Retry after the `Retry-After` header seconds    |
| `internal_error`             | Unhandled server exception                                                                          | Include `X-Request-Id` in support ticket        |
| `upstream_error`             | LLM provider / storage outage                                                                       | Retry with exponential backoff                  |
| `service_unavailable`        | Pool saturated, scheduled maintenance                                                               | Retry after 30-60s                              |
| `not_implemented`            | Endpoint exists (for shape compatibility) but the feature isn't available on your tier / deployment | Contact [support](mailto:support@overten.ai)    |

## Handling errors in code

<CodeGroup>
  ```python Python theme={null}
  import requests

  resp = requests.post(f"{API}/excel/generate", headers=headers, json=body)
  if not resp.ok:
      err = resp.json()
      code = err.get("error", "unknown")
      msg = err.get("message", "")

      if code == "preflight_failed":
          # Fix the prompt or attach missing assets
          print(f"Rejected: {msg}")
      elif code == "insufficient_credits":
          # Show a billing prompt to the user
          redirect_to_billing()
      elif code == "rate_limit_exceeded":
          # Honor Retry-After
          retry_after = int(resp.headers.get("Retry-After", "30"))
          time.sleep(retry_after)
          # then retry
      elif resp.status_code >= 500:
          # Transient — retry with exponential backoff
          retry_with_backoff()
      else:
          # Permanent — don't retry
          raise RuntimeError(f"{code}: {msg}")
  ```

  ```javascript Node theme={null}
  const resp = await fetch(`${API}/excel/generate`, {
    method: "POST", headers, body: JSON.stringify(body),
  });
  if (!resp.ok) {
    const err = await resp.json();
    const { error: code, message } = err;

    switch (code) {
      case "preflight_failed":
        throw new ValidationError(message);
      case "insufficient_credits":
        return redirectToBilling();
      case "rate_limit_exceeded":
        const retryAfter = Number(resp.headers.get("Retry-After") || "30");
        await sleep(retryAfter * 1000);
        // retry
        break;
      default:
        if (resp.status >= 500) {
          await retryWithBackoff();
        } else {
          throw new Error(`${code}: ${message}`);
        }
    }
  }
  ```
</CodeGroup>

## Debugging failed runs

Capture the `run_id` and `task_id` from any response (successful or not)
and include them on support tickets — they're the fastest way for us to
pull the matching ledger / trace rows on our side.

For terminal `failed` states on async tasks, the `error` field on
`GET /tasks/{task_id}` carries the structured failure:

```json theme={null}
{
  "task_id": "task_...",
  "status": "failed",
  "error": {
    "code": "internal_error",
    "message": "AgentLoopError: max_tool_calls=30 exceeded"
  },
  "credits_used": 0,
  "duration_ms": 45123
}
```

## Rate limits

Every response includes rate-limit headers:

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1713312345
```

And on 429:

```
Retry-After: 42
```

Limits are per API key, per minute. Tiers:

| Tier         | Requests/min | Concurrent runs |
| ------------ | ------------ | --------------- |
| `free`       | 10           | 2               |
| `pro`        | 60           | 10              |
| `enterprise` | 600          | 50              |

Rate limiting kicks in at the gateway, before any agent / pool work.
It's effectively free to hit rate limits (no credits deducted).
