<!-- source: https://docs.redpine.ai/docs/errors -->
# Errors

> Every status code, what causes it, and how to fix it.

## Error response format

All error responses follow this structure:

```json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable description of what went wrong",
    "requestId": "req_a1b2c3d4"
  }
}
```

* `code`: Machine-readable error code. Use this for programmatic error handling.
* `message`: Human-readable description. Safe to display to end users.
* `requestId`: Unique request identifier. Include this when contacting support.

This is the shape for every endpoint documented in this API reference. A separate, per-IP request limiter guards login, invites, and most dashboard/admin endpoints; its 429 responses are a bare `{"error": "Rate limit exceeded: ..."}` string, not this object. See [Rate limits](/docs/rate-limits).

## Error codes

| Status | Code                           | Meaning                                                                                                     |
| ------ | ------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `200`  | None                           | Success                                                                                                     |
| `401`  | UNAUTHORIZED                   | Missing API key                                                                                             |
| `401`  | INVALID\_API\_KEY              | API key is malformed or does not match a live key                                                           |
| `401`  | API\_KEY\_REVOKED              | API key has been revoked                                                                                    |
| `402`  | INSUFFICIENT\_CREDITS          | The organization's balance cannot cover the request                                                         |
| `402`  | BILLING\_SUSPENDED             | Billing is on hold for the organization                                                                     |
| `403`  | NO\_COLLECTION\_ACCESS         | No access to requested collection                                                                           |
| `403`  | SANDBOX\_UNSUPPORTED\_ENDPOINT | Sandbox key used on an endpoint with no sandbox behaviour                                                   |
| `404`  | COLLECTION\_NOT\_FOUND         | Collection not found                                                                                        |
| `409`  | IDEMPOTENCY\_KEY\_IN\_PROGRESS | A preview with this `Idempotency-Key` is still running                                                      |
| `410`  | CACHED\_RESULT\_EXPIRED        | Cached result or preview has expired (7-day window), or the preview was written by an older payload version |
| `422`  | VALIDATION\_ERROR              | Invalid request body                                                                                        |
| `422`  | INVALID\_FILTER                | A search filter cannot be applied: unknown field, unsupported operator combination, or invalid value        |
| `422`  | INVALID\_QUERY                 | The query carries too little to search on, such as only symbols, only stop words, or one repeated character |
| `422`  | IDEMPOTENCY\_KEY\_REUSED       | This `Idempotency-Key` was already used with a different request body                                       |
| `429`  | QUOTA\_EXCEEDED                | Per-minute, daily, or monthly quota exceeded                                                                |
| `429`  | API\_KEY\_BUDGET\_EXCEEDED     | The API key's spending cap is used up                                                                       |
| `429`  | PREVIEW\_RATE\_LIMIT\_EXCEEDED | Preview rate limit exceeded                                                                                 |
| `429`  | SEARCH\_RATE\_LIMITED          | Too many rejected queries in a short time                                                                   |
| `500`  | INTERNAL\_ERROR                | Internal server error                                                                                       |
| `503`  | SERVICE\_UNAVAILABLE           | A dependency the request needs (e.g. the quota service) is unreachable                                      |

## Handling errors in the SDKs

Every non-2xx response becomes a typed error carrying `status`, `code`, `message` and `requestId` from the envelope above, so the `code` column is what you branch on and `requestId` is what you send to support. `429` and `503` are retried automatically before they surface (see [Rate limits](/docs/rate-limits)).

| Status        | Python                | TypeScript            | Go                          |
| ------------- | --------------------- | --------------------- | --------------------------- |
| `401`         | `AuthError`           | `AuthError`           | `*AuthError`                |
| `402`         | `InsufficientCredits` | `InsufficientCredits` | `*InsufficientCreditsError` |
| `403`         | `AccessDenied`        | `AccessDenied`        | `*AccessDeniedError`        |
| `404`         | `NotFound`            | `NotFound`            | `*NotFoundError`            |
| `410`         | `Expired`             | `Expired`             | `*ExpiredError`             |
| `422`         | `ValidationError`     | `ValidationError`     | `*ValidationError`          |
| `429`         | `QuotaExceeded`       | `QuotaExceeded`       | `*QuotaExceededError`       |
| `503`         | `AssistedUnavailable` | `AssistedUnavailable` | `*AssistedUnavailableError` |
| anything else | `RedpineError`        | `RedpineError`        | `*APIError`                 |

<CodeBlockTabs defaultValue="Python" groupId="lang">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Go">
      Go
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Python">
    ```python  
    from redpine import AccessDenied, QuotaExceeded, RedpineError

    try:
        r = client.search("your search query", collection="my-collection")
    except AccessDenied as e:
        print("key cannot reach this collection:", e.code)      # NO_COLLECTION_ACCESS
    except QuotaExceeded as e:
        print("back off for", e.retry_after, "seconds")
    except RedpineError as e:
        print(e.status, e.code, e.message, e.request_id)
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```typescript  
    import { AccessDenied, QuotaExceeded, RedpineError } from "@redpine-ai/sdk";

    try {
      await client.search("your search query", { collection: "my-collection" });
    } catch (e) {
      if (e instanceof AccessDenied) console.log("key cannot reach this collection:", e.code);
      else if (e instanceof QuotaExceeded) console.log("back off for", e.retryAfter, "seconds");
      else if (e instanceof RedpineError) console.log(e.status, e.code, e.message, e.requestId);
      else throw e;
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Go">
    ```go  
    _, err := client.Search(ctx, "your search query", redpine.SearchOptions{Collection: "my-collection"})

    var denied *redpine.AccessDeniedError
    var quota *redpine.QuotaExceededError
    var apiErr *redpine.APIError
    switch {
    case errors.As(err, &denied):
        fmt.Println("key cannot reach this collection:", denied.Code)
    case errors.As(err, &quota):
        if quota.RetryAfter != nil {
            fmt.Println("back off for", *quota.RetryAfter, "seconds")
        }
    case errors.As(err, &apiErr):
        fmt.Println(apiErr.Status, apiErr.Code, apiErr.Message, apiErr.RequestID)
    case err != nil:
        return err
    }
    ```
  </CodeBlockTab>
</CodeBlockTabs>

A missing key never reaches the network: the Python and TypeScript constructors raise `AuthError` and Go's `New` returns an error when no explicit key is given and neither `REDPINE_API_KEY` nor the legacy `CONNECT_API_KEY` is set.

## Troubleshooting

* **401 UNAUTHORIZED**: The Authorization header is missing entirely. Include `Authorization: Bearer sk_live_...` (or `sk_test_...` for sandbox).
* **401 INVALID\_API\_KEY**: The header is present but the key doesn't match a live key. Check for typos or a stale/rotated key.
* **401 API\_KEY\_REVOKED**: The key was revoked. Generate a new one. Retrying with a revoked key will keep failing.
* **402 INSUFFICIENT\_CREDITS**: The balance cannot cover the request. Nothing is charged; add credits or a payment method and retry the same request.
* **402 BILLING\_SUSPENDED**: Billing is on hold for the organization, or the balance check is briefly unavailable. Retry once; if it persists, contact support.
* **403 NO\_COLLECTION\_ACCESS**: The API key doesn't have access to this collection. Use an org-wide key or add the collection to the key's scope.
* **403 SANDBOX\_UNSUPPORTED\_ENDPOINT**: The key is a sandbox key (`sk_test_`) and this endpoint has no sandbox behaviour. Use a live key, or one of the endpoints listed in [Sandbox](/docs/sandbox). This is deliberate: an endpoint that quietly behaved normally would leave you unable to tell a fixture from a real result.
* **404 COLLECTION\_NOT\_FOUND**: Verify the collection name exists. Names are case-sensitive.
* **410 CACHED\_RESULT\_EXPIRED**: The `queryId` you passed to `GET /api/v1/search/results/{queryId}` or `POST /api/v1/search/unlock` is older than the 7-day window, or the preview it points to was written by an older payload version. Run the search or preview again to get a fresh `queryId`.
* **422 VALIDATION\_ERROR**: Check required fields (query is required; exactly one of collection/collections). Ensure query is under 1000 characters (16,000 on `POST /api/v1/search/assisted`) and limit is between 1-30. On `POST /api/v1/search/unlock`, this also covers a `resultId` that isn't part of the preview and any unrecognized field in the request body. Both preview and unlock reject unknown keys rather than ignoring them.
* **422 INVALID\_FILTER**: The `message` says what to change and names the field where there is one (also in `details.field`). Common causes: a field name typo (the message lists the indexed fields), `doc_id`, a `journal_metric.*` threshold outside the [metric bounds](/docs/filtering#metric-bounds) or matching more than 35,000 ISSNs, two operator families in one condition, a field the collection has no index for, or an invalid date or `article_type`. Retrying the same filter fails the same way. See [Filtering](/docs/filtering#rejected-filters).
* **422 INVALID\_QUERY**: The query has too little to search on: only whitespace or symbols, too few letters or digits, one character repeated (`aaaaaaa`), the same word over and over, or only stop words (`what is the`). Short identifiers such as `p53`, `BRCA1` or `COVID-19` are fine. Rephrase the query; retrying it unchanged fails the same way.
* **409 IDEMPOTENCY\_KEY\_IN\_PROGRESS** and **422 IDEMPOTENCY\_KEY\_REUSED**: See [Retrying a preview safely](/docs/preview-unlock#retrying-a-preview-safely). Retry the first after a moment; for the second, send a new key.
* **429 QUOTA\_EXCEEDED**: You've hit the per-minute, daily, or monthly quota. Back off and retry. See [Rate limits](/docs/rate-limits) for how long each window takes to reset.
* **429 API\_KEY\_BUDGET\_EXCEEDED**: The key has reached the spending cap set on it in the dashboard. The cap applies however the key is used, including through MCP sign-in and paid integration tools. Ask an organization admin to raise the cap, or use another key.
* **429 PREVIEW\_RATE\_LIMIT\_EXCEEDED**: This organization's preview rate limit was exceeded. Wait for the Retry-After period. See [Rate limits](/docs/rate-limits) for the current per-hour default.
* **429 SEARCH\_RATE\_LIMITED**: More than 30 queries were rejected as `INVALID_QUERY` within a minute. Wait for the `Retry-After` period and check what is generating the queries.
* **500 INTERNAL\_ERROR**: An unexpected error occurred. Retry the request. If it persists, contact support with the requestId.
* **503 SERVICE\_UNAVAILABLE**: A backing service the request depends on (e.g. the quota check `POST /api/v1/search/unlock` performs) is temporarily unreachable. On unlock, this happens before the charge commits: nothing is charged and no result is unlocked, so it's safe to retry with backoff.

***

Usage is tracked per API key. View statistics on your Usage page in the dashboard.