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

# fetchbean API errors: types, codes, and error handling

> fetchbean errors share one JSON envelope with a stable type and code field, so a single error-handling path covers every provider and endpoint you call.

Whenever a fetchbean API call fails, the response body contains a unified error envelope with a matching HTTP status code. Every error — whether it originates from a bad request, an auth failure, or an upstream provider — uses the same structure, so you can write one error-handling path that works across all providers and endpoints.

## Error envelope

Every non-2xx response from fetchbean looks like this:

```json theme={null}
{
  "error": {
    "type": "auth",
    "code": "invalid_key",
    "message": "invalid API key",
    "retryable": false,
    "billable": false
  }
}
```

Each field serves a distinct purpose:

| Field       | Type    | Description                                                                                             |
| ----------- | ------- | ------------------------------------------------------------------------------------------------------- |
| `type`      | string  | The error category. Use this to drive top-level branching in your error handler.                        |
| `code`      | string  | A stable, specific code within the category. Use this when you need fine-grained handling.              |
| `message`   | string  | A human-readable description of what went wrong. Safe to surface to logs or users.                      |
| `retryable` | boolean | Whether sending the same request again might succeed. Use this to decide whether to back off and retry. |
| `billable`  | boolean | Whether the call consumed any credits. Provider failures and timeouts are always `false`.               |

## Error types

| Type                   | HTTP | Meaning                                                                                                                 |
| ---------------------- | ---- | ----------------------------------------------------------------------------------------------------------------------- |
| `validation`           | 400  | The request body is missing or malformed. Fix the request before retrying.                                              |
| `auth`                 | 401  | Missing, invalid, or unauthorized credential. Check your `X-API-Key` header.                                            |
| `insufficient_credits` | 402  | Your balance can't cover the call's reserved maximum. Top up and retry.                                                 |
| `rate_limited`         | 429  | Too many requests, or a same-key request is still in flight. Back off and retry.                                        |
| `provider`             | 502  | The upstream provider failed. Billed at zero.                                                                           |
| `timeout`              | 504  | The provider didn't respond in time. Billed at zero.                                                                    |
| `normalization`        | 502  | A curated tool couldn't map the provider response to a stable shape. **Billable** — the provider call itself succeeded. |

## Codes worth handling

A few `code` values inside those types come up often enough to branch on directly:

| Code                             | Type           | What to do                                                                                                                                          |
| -------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `credential_required`            | `auth`         | The tool runs on your own account and isn't connected yet. Connect the provider in the dashboard — see [Connections](/connections). Billed at zero. |
| `beta_unavailable`               | `auth`         | The operation is in beta and not enabled for your org. Email `hi@fetchbean.com` to request access.                                                  |
| `request_in_progress`            | `rate_limited` | A request with the same `Idempotency-Key` is still running. Wait and retry the same key.                                                            |
| `idempotency_replay_unavailable` | `validation`   | That key was already used for a request that never completed. Use a new key.                                                                        |
| `spend_cap_exceeded`             | `rate_limited` | Your org's monthly spend cap is reached. Not retryable until the cap resets or is raised.                                                           |

## Handling errors

Branch on `type` for the main decision logic, and use `code` when you need to handle a specific failure scenario. Use the `retryable` flag to determine whether a retry is appropriate:

```javascript theme={null}
const res = await fetch('https://api.fetchbean.com/v1/search', { /* ... */ });
if (!res.ok) {
  const { error } = await res.json();
  if (error.retryable) {
    // back off and retry
  } else if (error.type === 'insufficient_credits') {
    // prompt a top-up
  } else {
    // surface error.message
  }
}
```

<Note>
  You are never charged for `provider` or `timeout` errors — the provider never delivered, so `billable` is `false`. `normalization` is the exception: the provider call succeeded and cost real money, and only fetchbean's mapping to a stable shape failed, so it is billable. Fall back to [`POST /v1/run`](/guides/raw-run) to get that provider's raw response.
</Note>

Read `billable` rather than inferring from the status code — it is authoritative for every error. Your spend can never exceed your balance regardless, because insufficient-credit calls are rejected before any provider request is made.
