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

# Errors & rate limits

> Every error the Connie API can return — JSON shape, HTTP status codes, error codes, and how rate limits are enforced.

## Error JSON shape

Every error response — regardless of status code — uses the same envelope:

```json theme={null}
{
  "error": "Human-readable message.",
  "code": "MACHINE_READABLE_CODE"
}
```

Some errors include an optional `details` field with structured context (typically validation errors):

```json theme={null}
{
  "error": "Validation failed.",
  "code": "VALIDATION_ERROR",
  "details": {
    "email": "is required",
    "role": "must be one of: view, edit"
  }
}
```

<Tip>
  **Branch on `code`, not `error`.** The `code` is stable; the `error` string is for humans and may be reworded over time.
</Tip>

***

## HTTP status codes

| Status | Meaning | Typical `code` values |
| - | - | - |
| **400** | Bad Request — malformed payload, missing field | `VALIDATION_ERROR` |
| **401** | Unauthorized — no session, expired session, invalid token | `UNAUTHORIZED` |
| **402** | Payment Required — workspace out of credits, payment failed | `BUDGET_EXCEEDED`, `STRIPE_PAYMENT_FAILED`, `STRIPE_CARD_DECLINED` |
| **403** | Forbidden — authenticated, but not allowed on this resource | `FORBIDDEN` |
| **404** | Not Found — resource doesn't exist or you can't see it | `NOT_FOUND` |
| **409** | Conflict — duplicate, version mismatch, in-progress lock | `CONFLICT`, `SUBSCRIPTION_EXISTS` |
| **422** | Unprocessable Entity — semantically invalid input | `VALIDATION_ERROR` |
| **429** | Too Many Requests — rate limit or budget exceeded | `RATE_LIMIT_EXCEEDED`, `BUDGET_EXCEEDED` |
| **5xx** | Server Error — Connie's fault | `INTERNAL_ERROR` |

<Note>
  **404 is intentional for hidden resources.** If you query a resource that exists but your workspace can't see, you get `404` (not `403`) so the response doesn't leak whether the resource exists.
</Note>

***

## Common error codes

Codes worth special handling in client logic:

### `UNAUTHORIZED` (401)

Your session cookie or bearer token is missing or expired. Re-authenticate the user (or, for the extension, request a new token).

### `FORBIDDEN` (403)

Authenticated, but the user doesn't have the right workspace role or doesn't own the resource. Common on admin-only endpoints (billing changes, member management, share updates).

### `BUDGET_EXCEEDED` (402)

Workspace has run out of credits. The user needs to upgrade or wait for the next billing cycle. The [Billing overview](/billing/overview) explains the credit model.

### `RATE_LIMIT_EXCEEDED` (429)

You're hitting an endpoint too often. The response includes `Retry-After` and `X-RateLimit-*` headers — see the section below.

### `VALIDATION_ERROR` (400 / 422)

Request body or query failed schema validation. The `details` field tells you exactly which fields are wrong. Surface this to the user — don't retry blindly.

### `CONFLICT` (409)

Something already exists (duplicate invite, subscription already in this state) or you're racing another writer. Re-fetch the current state before retrying.

### `STRIPE_CARD_DECLINED` / `STRIPE_PAYMENT_FAILED` (402)

Stripe sanitized error codes for billing operations. Don't expose raw Stripe error messages — surface a friendly retry / "update card" CTA.

### `INTERNAL_ERROR` (500)

Connie failed. Retry with exponential backoff up to a few attempts; if it persists, [contact support](mailto:support@connie.ai).

***

## Rate limits

Connie rate-limits most endpoints using a **Redis-backed sliding window**. Limits are tiered by plan and applied at multiple scopes (per-user, per-workspace, or per-IP depending on the endpoint).

### Default tiers

| Plan | Brain (AI) | Inbox | Billing |
| - | - | - | - |
| Free | 10 / min | 20 / min | 5 / min |
| Starter | 30 / min | 60 / min | 10 / min |
| Scale | 100 / min | 120 / min | 10 / min |
| Enterprise | Custom | Custom | Custom |

These are starting points — individual endpoints may be tighter (e.g. checkout, password reset). Inspect the response headers for the actual limit applied.

### Rate-limit response headers

Every rate-limited response — both successes and 429s — includes:

| Header | Value |
| - | - |
| `X-RateLimit-Limit` | The current window's cap |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets |
| `Retry-After` | Seconds to wait before retrying (set on `429` only) |

### Recommended client behaviour

<Steps>
  <Step title="Check Remaining proactively">
    On every response, read `X-RateLimit-Remaining`. If it's low, slow down — don't wait for a `429`.
  </Step>

  <Step title="Respect Retry-After">
    On a `429`, sleep for the number of seconds in `Retry-After` before the next request. Don't retry sooner — you'll just get another `429`.
  </Step>

  <Step title="Backoff for 5xx">
    For `5xx`, use exponential backoff with jitter — start at 1 second, double up to \~30 seconds. Cap retries at 4–5 attempts.
  </Step>

  <Step title="Don't retry 4xx (except 429)">
    A `400`, `403`, `404`, or `422` won't change on retry — fix the request first.
  </Step>
</Steps>

***

## Worked example

A typical client-side handler:

```ts theme={null}
async function callConnie(path: string, init?: RequestInit) {
  const res = await fetch(`https://connie.ai/api${path}`, {
    credentials: 'include',
    ...init,
  });

  if (res.ok) return res.json();

  const body = await res.json().catch(() => ({}));

  switch (body.code) {
    case 'UNAUTHORIZED':
      // Re-authenticate the user
      window.location.href = '/login';
      return;
    case 'BUDGET_EXCEEDED':
      // Prompt the user to upgrade
      showUpgradeModal();
      return;
    case 'RATE_LIMIT_EXCEEDED':
      const retryAfter = Number(res.headers.get('Retry-After') ?? '5');
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      return callConnie(path, init);
    case 'VALIDATION_ERROR':
      throw new ValidationError(body.error, body.details);
    default:
      throw new ApiError(body.error ?? 'Unknown error', res.status);
  }
}
```

Branching on `body.code` keeps your error handling stable across API versions — the codes are part of the contract, the human-readable strings are not.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.