# Errors & Rate Limits

Error envelope, HTTP status codes, error codes by category, rate-limit headers, and retry guidance.

## Errors

### Error Envelope

REST endpoints return errors in a consistent format with a `code`, `message`, and optional `hint`, wrapped in a `detail` object. MCP tools use a different envelope — see below.

```json
{
  "detail": {
    "code": "DOCUMENT_FORMAT_UNSUPPORTED",
    "message": "The uploaded file format is not supported.",
    "hint": "Supported formats: PDF, DOCX, XLSX, TXT, CSV, JSON, HTML, MD"
  }
}
```

| Field | Type | Description |
| --- | --- | --- |
| code | string | Machine-readable error code (e.g., DOCUMENT_FORMAT_UNSUPPORTED) |
| message | string | Human-readable error description |
| hint | string? | Optional hint for resolving the error |

### MCP Tool Errors

Tools called over MCP (for example `connector_execute`) don't carry HTTP status codes. They return a structured envelope with a `retryable` flag in place of `hint`:

```json
{
  "code": "SERVICE_RATE_LIMITED",
  "message": "Upstream service is rate limiting requests.",
  "retryable": true
}
```

When `retryable` is `true`, the call may be retried after a short backoff; when `false`, retrying with the same arguments will fail again.

### HTTP Status Codes

| Code | Description |
| --- | --- |
| 200 | Success |
| 201 | Created — resource successfully created |
| 204 | No Content — successful deletion |
| 400 | Bad Request — invalid parameters or body |
| 401 | Unauthorized — missing or invalid token |
| 403 | Forbidden — token lacks required scope |
| 404 | Not Found — resource does not exist |
| 409 | Conflict — resource already exists (idempotency) |
| 422 | Unprocessable Entity — validation error |
| 429 | Too Many Requests — rate limit exceeded |
| 500 | Internal Server Error |
| 504 | Gateway Timeout — request took too long |

### Error Codes by Category

Common error codes grouped by category. The complete, always-current set is browsable in the [interactive API explorer](/docs/api-explorer).

| Category | Codes |
| --- | --- |
| Authentication | `AUTH_TOKEN_EXPIRED` `AUTH_TOKEN_INVALID` `AUTH_HEADER_MISSING` `AUTH_PERMISSION_DENIED` `AUTH_SCOPE_MISSING` |
| Documents | `DOCUMENT_FORMAT_UNSUPPORTED` `DOCUMENT_SIZE_EXCEEDED` `DOCUMENT_PASSWORD_PROTECTED` `DOCUMENT_OCR_FAILED` `DOCUMENT_NOT_FOUND` |
| LLM | `LLM_PROVIDER_UNAVAILABLE` `LLM_REQUEST_TIMEOUT` `LLM_PROVIDER_ERROR` `LLM_RATE_LIMITED` |
| Tools | `TOOL_VALIDATION_FAILED` `TOOL_NOT_FOUND` `TOOL_TIMEOUT` `TOOL_EXECUTION_FAILED` |
| Data | `DATA_NOT_FOUND` `DATA_CONFLICT` `DATA_INVALID_PARAMETER` `DATA_QUOTA_EXCEEDED` |
| Service | `SERVICE_UNAVAILABLE` `SERVICE_RATE_LIMITED` `SERVICE_CONNECTION_LOST` `SERVICE_CIRCUIT_OPEN` |

### Error Handling Example

Recommended approach to handling API errors in Python.

```python
import requests

response = requests.post(url, headers=headers, json=body)

if response.status_code >= 400:
    error = response.json().get("detail", {})
    code = error.get("code", "UNKNOWN")
    message = error.get("message", "An error occurred")
    hint = error.get("hint")

    if response.status_code == 429:
        # Rate limited — retry after delay
        retry_after = response.headers.get("Retry-After", "60")
        print(f"Rate limited. Retry after {retry_after}s")
    elif response.status_code == 401:
        # Authentication error
        print(f"Auth error: {message}")
    else:
        print(f"Error [{code}]: {message}")
        if hint:
            print(f"Hint: {hint}")
```

## Rate Limits

### Rate Limit Headers

Every API response includes headers with current rate limit information.

```bash
HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1711540800
```

| Header | Description |
| --- | --- |
| X-RateLimit-Limit | Maximum number of requests in the window |
| X-RateLimit-Remaining | Number of requests remaining |
| X-RateLimit-Reset | Unix timestamp when the rate limit window resets |
| Retry-After | Seconds to wait before retrying (only on 429) |

### Plan-Based Limits

Per-minute request limits depend on your subscription plan and are shared across the organization.

| Plan | Requests |
| --- | --- |
| Free | 10 req/min |
| Pro | 30 req/min |
| Max | 60 req/min |

Only the per-minute limit is currently enforced.

### Rate Limit Exceeded Response

When the rate limit is exceeded, the API returns a 429 status with retry timing information.

```json
{
  "detail": {
    "code": "SERVICE_RATE_LIMITED",
    "message": "Rate limit exceeded. Please retry after the reset time.",
    "hint": "Check X-RateLimit-Reset header for the reset timestamp."
  }
}
```

### Retry Logic

Implement exponential backoff when receiving 429 responses.

```python
import time
import requests

def request_with_retry(url, headers, max_retries=3):
    for attempt in range(max_retries):
        response = requests.get(url, headers=headers)

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", "60"))
            print(f"Rate limited. Retrying in {retry_after}s...")
            time.sleep(retry_after)
            continue

        return response

    raise Exception("Max retries exceeded")
```

### Best Practices

- Cache responses that don't change often (e.g., model list)
- Use exponential backoff for retries
- Monitor X-RateLimit-Remaining headers to anticipate limit exhaustion
- For bulk operations, use batch endpoints instead of multiple individual requests
