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

# Error Codes

> Understanding API error responses and how to handle them

The Qwairy API returns two error shapes depending on where the failure occurs:

* **Resource errors** (validation and not-found errors raised inside an endpoint) return a **nested** object: `{ "error": { "code", "message" } }`. Use the `code` for programmatic handling.
* **Authentication and rate-limit errors** (raised by the API gateway before the endpoint runs — 401 and 429) return a **flat** object: `{ "error", "message" }`. Here `error` is a short status label, not a machine-readable code.

Always branch on the HTTP status first, then read the body in the shape matching that status.

## HTTP Status Codes

| Code | Status                | Description                             |
| ---- | --------------------- | --------------------------------------- |
| 200  | OK                    | Request succeeded                       |
| 400  | Bad Request           | Invalid request parameters              |
| 401  | Unauthorized          | Missing or invalid authentication       |
| 403  | Forbidden             | Valid auth but insufficient permissions |
| 404  | Not Found             | Resource doesn't exist                  |
| 429  | Too Many Requests     | Rate limit exceeded                     |
| 500  | Internal Server Error | Server-side error                       |

## Error Response Format

### Resource errors (nested)

Validation and not-found errors raised inside an endpoint return a structured `error` object:

```json theme={null}
{
  "error": {
    "code": "BRAND_NOT_FOUND",
    "message": "Brand not found or not accessible"
  }
}
```

<ResponseField name="error" type="object">
  <Expandable title="Error object">
    <ResponseField name="code" type="string">
      Machine-readable error code for programmatic handling
    </ResponseField>

    <ResponseField name="message" type="string">
      Human-readable error description
    </ResponseField>
  </Expandable>
</ResponseField>

### Authentication & rate-limit errors (flat)

`401` (authentication) and `429` (rate limit) responses are produced by the API gateway and use a flat shape — `error` is a short status label, not a code:

```json theme={null}
{
  "error": "Unauthorized",
  "message": "Missing or invalid Authorization header. Use: Authorization: Bearer qw-api-xxx"
}
```

<ResponseField name="error" type="string">
  Short status label (e.g. `Unauthorized`, `Too Many Requests`).
</ResponseField>

<ResponseField name="message" type="string">
  Human-readable description of the failure.
</ResponseField>

429 responses additionally include `limit` (`"burst"` or `"daily"`) and `retryAfter` (seconds).

## Error Codes Reference

### Authentication & Authorization

| Code                | HTTP Status | Description                    |
| ------------------- | ----------- | ------------------------------ |
| `UNAUTHORIZED`      | 401         | Authentication required        |
| `INVALID_TOKEN`     | 401         | Invalid or expired API token   |
| `INSUFFICIENT_PLAN` | 403         | Growth plan or higher required |
| `FORBIDDEN`         | 403         | Access denied                  |

### Not Found

| Code                   | HTTP Status | Description                       |
| ---------------------- | ----------- | --------------------------------- |
| `BRAND_NOT_FOUND`      | 404         | Brand not found or not accessible |
| `COMPETITOR_NOT_FOUND` | 404         | Competitor not found              |
| `SOURCE_NOT_FOUND`     | 404         | Source not found                  |
| `PROMPT_NOT_FOUND`     | 404         | Prompt not found                  |
| `ANSWER_NOT_FOUND`     | 404         | Answer not found                  |
| `RESOURCE_NOT_FOUND`   | 404         | Generic resource not found        |
| `ENDPOINT_NOT_FOUND`   | 404         | API endpoint does not exist       |

### Bad Request

| Code                 | HTTP Status | Description                |
| -------------------- | ----------- | -------------------------- |
| `INVALID_REQUEST`    | 400         | Invalid request format     |
| `INVALID_PARAMETER`  | 400         | Invalid parameter value    |
| `MISSING_PARAMETER`  | 400         | Missing required parameter |
| `INVALID_DATE_RANGE` | 400         | Invalid date range         |
| `INVALID_SORT_FIELD` | 400         | Invalid sort field         |

### Rate Limiting

| Code                  | HTTP Status | Description         |
| --------------------- | ----------- | ------------------- |
| `RATE_LIMIT_EXCEEDED` | 429         | Rate limit exceeded |

### Server Errors

| Code             | HTTP Status | Description           |
| ---------------- | ----------- | --------------------- |
| `INTERNAL_ERROR` | 500         | Internal server error |

## Common Errors

### Authentication Errors (401)

Authentication failures use the **flat** shape:

```json theme={null}
// Missing or malformed Authorization header
{
  "error": "Unauthorized",
  "message": "Missing or invalid Authorization header. Use: Authorization: Bearer qw-api-xxx"
}

// Invalid token, revoked token, or plan below Growth
{
  "error": "Unauthorized",
  "message": "Invalid API token or insufficient subscription plan (Growth+ required)"
}
```

**Solution**: Ensure you're using a valid token with the correct `Bearer` prefix, on a Growth plan or higher.

### Permission Errors (403)

```json theme={null}
// Team subscription doesn't include API access
{
  "error": {
    "code": "INSUFFICIENT_PLAN",
    "message": "Growth plan or higher required"
  }
}
```

**Solution**: Verify you have the correct permissions and plan level.

### Not Found Errors (404)

```json theme={null}
// Brand doesn't exist
{
  "error": {
    "code": "BRAND_NOT_FOUND",
    "message": "Brand not found or not accessible"
  }
}

// Competitor doesn't exist
{
  "error": {
    "code": "COMPETITOR_NOT_FOUND",
    "message": "Competitor not found"
  }
}
```

**Solution**: Check that the resource ID is correct and belongs to your team.

### Rate Limit Errors (429)

Rate-limit responses use the **flat** shape and add `limit` and `retryAfter`:

```json theme={null}
{
  "error": "Too Many Requests",
  "limit": "burst",
  "message": "Per-minute rate limit exceeded. Please try again in 45 seconds.",
  "retryAfter": 45
}
```

`limit` is `"burst"` (per-minute) or `"daily"`.

**Solution**: Wait `retryAfter` seconds (or until the `X-RateLimit-Reset` / `X-RateLimit-Daily-Reset` timestamp), then retry. See [Rate Limits](/api-reference/rate-limits).

### Server Errors (500)

```json theme={null}
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred"
  }
}
```

**Solution**: Retry after a few seconds. If the error persists, contact support.

## Handling Errors

### JavaScript/TypeScript Example

```typescript theme={null}
async function callQwairyAPI(endpoint: string) {
  const response = await fetch(`https://www.qwairy.co/api/v1${endpoint}`, {
    headers: {
      'Authorization': `Bearer ${process.env.QWAIRY_API_TOKEN}`,
    },
  });

  if (!response.ok) {
    const data = await response.json().catch(() => ({}));

    // Auth and rate-limit errors use the flat shape: { error, message }
    if (response.status === 401) {
      throw new Error(`Authentication failed: ${data.message ?? data.error}`);
    }
    if (response.status === 429) {
      const retryAfter = data.retryAfter ?? response.headers.get('Retry-After');
      throw new Error(`Rate limited. Retry after: ${retryAfter}s`);
    }

    // Resource errors use the nested shape: { error: { code, message } }
    const code = data.error?.code ?? 'UNKNOWN';
    const message = data.error?.message ?? data.message ?? `HTTP ${response.status}`;

    switch (code) {
      case 'INSUFFICIENT_PLAN':
      case 'FORBIDDEN':
        throw new Error(`Permission denied: ${message}`);
      case 'BRAND_NOT_FOUND':
      case 'COMPETITOR_NOT_FOUND':
      case 'SOURCE_NOT_FOUND':
      case 'PROMPT_NOT_FOUND':
      case 'ANSWER_NOT_FOUND':
        throw new Error(`Resource not found: ${message}`);
      default:
        throw new Error(`API error [${code}]: ${message}`);
    }
  }

  return response.json();
}
```

### Python Example

```python theme={null}
import requests
import os

def call_qwairy_api(endpoint):
    response = requests.get(
        f"https://www.qwairy.co/api/v1{endpoint}",
        headers={"Authorization": f"Bearer {os.environ['QWAIRY_API_TOKEN']}"}
    )

    if not response.ok:
        body = response.json() if response.content else {}

        # Auth and rate-limit errors use the flat shape: { error, message }
        if response.status_code == 401:
            raise Exception(f"Authentication failed: {body.get('message', body.get('error'))}")
        if response.status_code == 429:
            retry_after = body.get('retryAfter', response.headers.get('Retry-After'))
            raise Exception(f"Rate limited. Retry after: {retry_after}s")

        # Resource errors use the nested shape: { error: { code, message } }
        error = body.get('error', {}) if isinstance(body.get('error'), dict) else {}
        code = error.get('code', 'UNKNOWN')
        message = error.get('message', body.get('message', 'Unknown error'))

        if code in ['INSUFFICIENT_PLAN', 'FORBIDDEN']:
            raise Exception(f"Permission denied: {message}")
        elif code.endswith('_NOT_FOUND'):
            raise Exception(f"Not found: {message}")
        else:
            raise Exception(f"API error [{code}]: {message}")

    return response.json()
```

## Need Help?

If you encounter persistent errors or unexpected behavior, contact us at [team@qwairy.co](mailto:team@qwairy.co) with:

* The endpoint you're calling
* The full error response
* Your request headers (without the token)
* The timestamp of the request
