
# Error codes

Reference of Vibe API error codes: the unified response format, the code list, common causes, and how to fix them. Applies to all `/v1/...` endpoints.

## Error response format

Every error response is returned in a unified shape. `error` is an object.

```json
{
  "success": false,
  "error": {
    "code": "ENTITY_NOT_FOUND",
    "message": "Item not found"
  }
}
```

| Field | Type | Req. | Description |
|------|-----|:-----:|----------|
| `success` | boolean | yes | Always `false` on error |
| `error.code` | string | yes | Machine-readable error code. Use it to distinguish error types in client code |
| `error.message` | string | yes | Error description for the developer. The language is not known in advance and depends on the source: the platform's technical messages arrive in English, messages from Bitrix24 arrive in the Bitrix24 account's language, and cloud provider failure messages are localized to the user's language. Do not parse the text or rely on its language — branch on `error.code` |
| `error.hint` | string \| object | no | Developer hint: what to try next, which limits to watch. A text string, or — on infrastructure creation endpoints `POST /v1/infra/servers` — an object with `reason`, `recovery`, and `example` fields, where `example` holds a ready-to-send request body to fix the call |
| `error.userMessage` | string | no | Message for the end user in the user's interface language. Appears in billing, infrastructure, and plan errors |
| `error.warning` | string | no | Appears on repeated identical errors on the same key — a likely sign of a bug in the client code. The counter is heuristic, so the warning does not guarantee a bug |
| `error.retryAfter` | number | no | Seconds until the next attempt. Appears in `429` (rate-limit, and also the portal queue — `QUEUE_OVERFLOW`/`QUEUE_TIMEOUT`) |

Example response with extra fields (`429 RATE_LIMITED`):

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "QUERY_LIMIT_EXCEEDED",
    "hint": "Wait 1-2 seconds and retry. Use POST /v1/batch to combine up to 50 calls in 1 request.",
    "retryAfter": 2
  }
}
```

The `Retry-After` HTTP header mirrors the `error.retryAfter` value for compatibility with the HTTP standard.

## Code summary table

The table covers the core codes that appear on any Vibe API endpoint. Domain codes (`BOT_NOT_FOUND`, `SERVER_NOT_RUNNING`, `AGENT_LIMIT_REACHED`, and similar) are documented on the pages of their respective sections.

### Authorization and keys

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `MISSING_API_KEY` | 401 | Request without the `X-Api-Key` header |
| `INVALID_API_KEY` | 401 | Key not found in the system or its format is unrecognized |
| `INVALID_APP_KEY` | 401 | A `vibe_app_*` key was passed without an accompanying `Authorization: Bearer ...` |
| `WRONG_AUTH_SCHEME` | 401 | An API key was passed in the `Authorization: Bearer` header. An OAuth app key (`vibe_app_*`) goes in `X-Api-Key`, and `Authorization: Bearer` carries the session token (`vibe_session_*`). For a client that can only send Bearer, use a personal key (`vibe_api_*`) — it is accepted in `Authorization: Bearer` |
| `TOKEN_EXPIRED` | 401 | The OAuth token has expired |
| `TOKEN_REFRESH_FAILED` | 401 | Failed to refresh the OAuth token on the Bitrix24 side |
| `WRONG_KEY_TYPE` | 401 | The key type does not match the endpoint: e.g. a management key on an entity route |
| `SESSION_REQUIRED` | 401 | A [placement](./apps/placements.md) operation was called without the `Authorization: Bearer` header carrying a session token, on an account that requires it |

### Application user authorization (OAuth)

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `INVALID_REDIRECT_URI` | 400 | `redirect_uri` is not registered for the application, on entry to `GET /v1/oauth/authorize` |
| `INVALID_STATE` | 400 | `state` was not found or expired after 20 minutes when receiving the Bitrix24 response. `error.details.reason` carries `NOT_FOUND` or `EXPIRED` |
| `INVALID_CODE` | 400 | The authorization code was not found or belongs to another application |
| `CODE_EXPIRED` | 400 | The authorization code has expired — 5 minutes |
| `CODE_ALREADY_USED` | 400 | The authorization code has already been exchanged for a session token |
| `REDIRECT_URI_MISMATCH` | 400 | The `redirect_uri` at code exchange does not match the one passed on entry |
| `DOMAIN_MISMATCH` | 400 | `domain` in `POST /v1/oauth/placement-session` does not match the application's portal |
| `USER_AUTH_REQUIRED` | 401 | The Bitrix24 user token did not confirm authorization on the portal |
| `MISSING_TOKEN` | 400 | `POST /v1/oauth/revoke` without an `Authorization: Bearer` header carrying a session token |

A rejection on the return may arrive not as a code but as an `?error=` parameter appended to the `redirect_uri` — `token_exchange_failed`, `invalid_domain` or `profile_fetch_failed`.

Full description of the flow — [Application user authorization](./keys-auth/oauth.md).

### Permissions and scopes

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `SCOPE_DENIED` | 403 | The key lacks the required scope (e.g. `crm` for deals, `imbot` for bots) |
| `WRITE_BLOCKED_READONLY_KEY` | 403 | The key is in read-only mode, but the call performs a write |
| `INFRA_FORBIDDEN_FOR_COWORK_KEY` | 403 | A Co-work subscription key (`vibe:cowork`) — data-plane only: provision / deploy / exec / upload / server lifecycle and catalog publishing are forbidden. Use a project key with deploy permissions. The response lists your other active deploy-capable keys in `error.details.deployableKeys` (name / prefix / suffix, up to 5) |
| `INFRA_SCOPE_REQUIRED` | 403 | The key lacks the `vibe:infra` scope — infrastructure management is unavailable. Add the scope or use a key with infrastructure permissions |
| `KEY_POLICY_READONLY_REQUIRED` | 403 | The portal policy allows regular users to create read-only keys only — issuing a write-enabled key was rejected |
| `MANAGEMENT_KEY_READ_ONLY` | 403 | A management key without write permissions attempts a `POST/PATCH/DELETE` |
| `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | 403 | A management key accesses an entity endpoint — an APP key with the required scope is needed |
| `BITRIX_ACCESS_DENIED` | 403 | Bitrix24 returned `ACCESS_DENIED`: the user lacks permission for the operation or entity |
| `OAUTH_REQUIRED` | 403 | The endpoint requires a user context — a `vibe_app_*` key + Bearer token is needed |
| `WAITLIST_PENDING` | 403 | The account is awaiting waitlist activation |
| `OAUTH_SCOPE_CHANGE_REQUIRES_REISSUE` | 403 | An attempt to add a Bitrix24 scope to an OAuth-app key (`vibe_app_*`) via `PATCH /v1/keys/:id`, or to an app via `PATCH /v1/apps/:id`. An OAuth app's scopes are fixed at issue time — removals work, but for a new scope create a new authorization key with the scopes you need |
| `OAUTH_APP_REQUIRED` | 400 | A [placement](./apps/placements.md) operation was called with a personal `vibe_api_*` key. Binding, unbinding, and listing bound placements are available only to the application authorization key `vibe_app_*` |
| `PLACEMENT_SCOPE_MISSING` | 403 | The key lacks the `placement` scope — [binding and unbinding placements](./apps/placements.md) are unavailable |
| `SESSION_REQUIRES_ADMIN` | 403 | [Binding a placement](./apps/placements/bind.md) on a self-hosted account is performed under an account without account-administrator rights |
| `APP_NOT_REGISTERED` | 400 | The application has no Bitrix24 application identifier — a [placement](./apps/placements.md) cannot be bound or unbound |
| `BOX_NO_DEVELOPER_KEY` | 400 | The application author has no developer key configured — placement operations on a self-hosted account are unavailable |
| `OAUTH_APP_KEY_CANNOT_RELINK` | 403 | `POST /v1/apps/:id/relink-oauth` was called with the OAuth app's own key (`vibe_app_*`). Such a key cannot relink the app's credentials — use a personal key (`vibe_api_*`) or the Vibecode dashboard |

### Request validation

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `VALIDATION_ERROR` | 400 | The body or query failed schema validation. `message` contains per-field details |
| `INVALID_JSON_BODY` | 400 | The request body cannot be parsed as JSON. It is returned before schema validation, so `message` carries no field details. This code comes from the entity routes `/v1/<entity>`, as well as `/v1/apps`, `/v1/bots`, `/v1/keys`, `/v1/note`, every `/v1/infra/servers/:id` server operation — `deploy`, `exec`, `upload`, `icon`, `logs`, `lock`, `unstick` — and `/v1/infra/runtimes` |
| `FST_ERR_CTP_INVALID_JSON_BODY` | 400 | The same case — the body cannot be parsed as JSON — on the other routes: [`POST /v1/search`](./search/run.md), [`POST /v1/research`](./search/research.md), [`POST /v1/batch`](./batch.md) and the rest |
| `fst_err_ctp_invalid_json_body` | 400 | The same case on the OpenAI-compatible [AI Router](./ai.md) routes — `/v1/ai/`, `/v1/chat/`, `/v1/models`, `/v1/audio/`. There the codes are lowercased and the response uses the OpenAI envelope: `error.type`, `error.code`, with no `success` field |
| `INVALID_PARAMS` | 400 | Bitrix24 returned `INVALID_PARAMS` or the route handler found an invalid parameter value |
| `INVALID_REQUEST` | 400 | The request structure does not match the required one (e.g. `calls` in `/v1/batch` is an empty array or contains more than 50 elements) |
| `MISSING_PARAMS` | 400 | A required parameter explicitly listed in the endpoint schema was not passed |
| `MISSING_REQUIRED_FILTER` | 400 | A required filter for list endpoints that need context (`timelines`, `task-comments`, and similar) was not passed |
| `MISSING_REQUIRED_PARAMS` | 400 | Required context parameters for a nested search or list were not passed: `files` requires `folderId`, `folders` requires `parentId`, `calendar-events` requires `type` and `ownerId`. `message` lists the missing fields |
| `MISSING_REQUIRED_FIELDS` | 400 | A body field declared required for creating the entity was not passed. `message` names the missing field |
| `EMPTY_CREATE_BODY` | 400 | The create request body is empty — no field was passed |
| `EMPTY_UPDATE_BODY` | 400 | The update request body is empty — no field was passed |
| `INVALID_FILTER_FIELD` | 400 | Field name starts with the Bitrix24-native prefix `@` (IN) or `!@` (NOT IN) — use the `$in` / `$nin` operators instead |
| `UNKNOWN_SORT_FIELD` | 400 | Sort on a non-existent field (for entities whose sort validator is active) |
| `BATCH_LIMIT_EXCEEDED` | 400 | The request contains more than 50 elements in a bulk operation (vipchats, task-comments, and similar) |
| `MESSAGE_REQUIRED` | 400 | `POST /v1/chats/{dialogId}/messages` with no text: the `message` field is empty and there is no `attach` block. Often the text was passed under an unknown field name, for example `text`. The response lists the unrecognized fields |
| `INVALID_EVENT` | 400 | The portal event subscription code does not match the `^[A-Z][A-Z0-9_]+$` format. See [portal event subscriptions](./infra/event-subscriptions.md) |
| `INVALID_APP_PATH` | 400 | The `appPath` delivery path does not start with `/` or contains control characters. See [portal event subscriptions](./infra/event-subscriptions.md) |
| `PLATFORM_HANDLER_UNRESOLVABLE` | 400 | The handler address points at the technical address of the app server, and the platform handler could not be resolved. The placement was not registered. See [Bind a placement](./apps/placements/bind.md) |

### File uploads

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `STORAGE_FORBIDDEN_CONTENT_TYPE` | 415 | For `PUBLIC` objects the types `text/html`, `application/javascript`, `application/x-javascript`, `image/svg+xml` are forbidden — they are prone to cross-site scripting. Upload such a file as `PRIVATE` |

### Portal event subscription preconditions

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `NOT_OAUTH_APP` | 400 | The server is not bound to an OAuth app with an `application_token` — an event subscription cannot be registered |
| `NO_USER_TOKEN` | 400 | The app has no OAuth token — authorize the app on the portal first |

For the full operation reference, see [portal event subscriptions](./infra/event-subscriptions.md).

### Resource not found

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `ENTITY_NOT_FOUND` | 404 | A CRM entity record with the given `id` does not exist. The canonical code for `/v1/deals/:id`, `/v1/contacts/:id`, and similar |
| `NOT_FOUND` | 404 | `GET /:id` only: Bitrix24 returned `success`, but `result` is empty (applies to several smart methods) |

Domain `*_NOT_FOUND` codes (`BOT_NOT_FOUND`, `SERVER_NOT_FOUND`, `AGENT_NOT_FOUND`, `APP_NOT_FOUND`, `PORTAL_NOT_FOUND`, `USER_NOT_FOUND`, `FILE_NOT_FOUND`, `SUBSCRIPTION_NOT_FOUND`) are documented on the pages of their respective sections.

### State conflicts

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `CONFLICT` | 409 | The current resource state is incompatible with the request |
| `ALREADY_EXISTS` | 409 | A record with these key fields already exists |
| `EVENT_BOUND_ELSEWHERE` | 409 | A portal event is already bound to another server of the same OAuth app. See [portal event subscriptions](./infra/event-subscriptions.md) |
| `OAUTH_CLIENT_ID_IN_USE` | 409 | `POST /v1/apps/:id/relink-oauth`: the given `bitrixClientId` is already linked to another application. One `client_id` — one application |

### Billing and plans

Occur on endpoints that create and wake infrastructure (servers, agents, managed bots). The response includes a localized `userMessage` for display in the client interface.

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `BILLING_EXHAUSTED` | 402 | The balance dropped into the red zone and the account is frozen. A top-up is required |
| `ACCOUNT_FROZEN` | 402 | The billing account is frozen for other reasons |
| `COMMERCIAL_PLAN_REQUIRED` | 402 | Free Bitrix24 plan, the trial period is unavailable or already used |
| `INT_TARIFF_REQUIRED` | 402 | The portal is on a free Bitrix24 plan — creating servers, deploying, and waking require a commercial plan (a demo plan grants a limited trial) |
| `TRIAL_EXPIRED` | 402 | The trial period has ended |
| `TRIAL_PORTAL_LIMIT` | 402 | The overall per-portal server limit was exceeded during the trial period |
| `TRIAL_USER_LIMIT` | 402 | The per-user server limit was exceeded during the trial period |
| `PLAN_NOT_ALLOWED_ON_TRIAL` | 402 | The requested server/agent plan is unavailable during the trial period |
| `SERVER_WAKE_BLOCKED` | 403 | Server wake-up is blocked for a non-billing reason |
| `INT_TARIFF_REQUIRED` | 403 | The account gets access through its Bitrix24 plan, and [binding a placement](./apps/placements/bind.md) requires a commercial plan |

### Rate limiting

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `RATE_LIMITED` | 429 | Bitrix24 throttled the request rate or an internal limit was exceeded. The response includes `retryAfter` (seconds) and the `Retry-After` header |
| `ERROR_LOOP_DETECTED` | 429 | A Vibecode-side block: the same request repeats with the same error. A signal of a bug in the client code. Every Nth request is forwarded onward to check for recovery |

### Backend and third-party services

| Code | HTTP | When it occurs |
|-----|------|-----------------|
| `BITRIX_ERROR` | 422 | Bitrix24 returned a business error that does not fall under narrower categories (`ACCESS_DENIED`, `NOT_FOUND`, `INVALID_PARAMS`) |
| `BITRIX_UNAVAILABLE` | 502 | Bitrix24 returned 5xx or did not respond in time |
| `BIND_FAILED` | 502 | Bitrix24 rejected the event registration (`event.bind`) — for example, the portal is not on a commercial plan. See [portal event subscriptions](./infra/event-subscriptions.md) |
| `WINDOWED_SEARCH_FAILED` | — | Retired: when auto-windowing fails completely, `/search` returns the real Bitrix24 code — `UNKNOWN_FILTER_FIELD` / `INVALID_PARAMS` / `BITRIX_ACCESS_DENIED` / `RATE_LIMITED` / `BITRIX_UNAVAILABLE` / `BITRIX_TIMEOUT` (503), same as for a narrow range |
| `QUEUE_OVERFLOW` | 429 | The portal queue is overflowing: too many concurrent Bitrix24 calls. Rejected instantly, `Retry-After` in the header |
| `QUEUE_TIMEOUT` | 429 | The portal queue is saturated: more than 30 seconds of waiting on the Vibecode side. The request was NOT sent to Bitrix24 — safe to retry |
| `BITRIX_TIMEOUT` | 503 | Bitrix24 accepted the request but did not respond within 15 seconds — the outcome is unknown. For writes: re-read the entity first, the change may have applied |
| `POOL_EXHAUSTED` | 503 | The service is temporarily overloaded — the database connection pool is exhausted. The response carries `retryAfter` and a `Retry-After` header, retry after a few seconds |
| `INTERNAL_ERROR` | 500 | An unexpected Vibecode backend error |
| `NETWORK_DEVKEY_REQUIRED` | 503 | The developer key for the application author has not been issued yet — [binding a placement](./apps/placements/bind.md) is temporarily unavailable |

## Detailed reference

### `MISSING_API_KEY` (401)

The request does not contain the `X-Api-Key` header.

```json
{
  "success": false,
  "error": {
    "code": "MISSING_API_KEY",
    "message": "API key required. Pass via X-Api-Key header."
  }
}
```

**Causes:**
- The `X-Api-Key` or `Authorization` header was not passed.
- The header was passed with an empty value.

**Fix:**
- Add the `X-Api-Key: vibe_api_...` or `X-Api-Key: vibe_app_...` header.
- Verify that the environment variable holding the key is set correctly (for CLI tools and SDKs).

---

### `INVALID_API_KEY` (401)

The passed key does not exist or its format is unrecognized.

```json
{
  "success": false,
  "error": {
    "code": "INVALID_API_KEY",
    "message": "Invalid API key"
  }
}
```

**Causes:**
- A typo or extra whitespace in the key.
- The key was deleted by its owner or an administrator.
- The key is from a different environment (staging/production).
- The prefix is not among the supported ones: `vibe_api_`, `vibe_app_`, `vibe_live_`, `vibe_mgmt_`.

**Fix:**
- Check the key in your account on the `/keys` page.
- Create a new key if the old one was deleted.

---

### `SCOPE_DENIED` (403)

The key lacks the required scope for the requested operation.

```json
{
  "success": false,
  "error": {
    "code": "SCOPE_DENIED",
    "message": "This endpoint requires 'crm' scope"
  }
}
```

**Causes:**
- CRM entities require the `crm` scope, tasks require `task`, the bot platform requires `imbot`, the AI Router requires `vibe:ai`, infrastructure requires `vibe:infra`.
- The key's scope was narrowed at creation time.

**Fix:**
- Open the key page in your account and issue a new key with a broader scope set.
- The full list of scopes and their purpose is on the [Keys and authorization](./keys-auth.md) page.

---

### `BITRIX_ACCESS_DENIED` (403)

Bitrix24 returned `ACCESS_DENIED`: the user or app lacks permission for the entity or operation.

```json
{
  "success": false,
  "error": {
    "code": "BITRIX_ACCESS_DENIED",
    "message": "ACCESS_DENIED"
  }
}
```

**Causes:**
- The user lacks permission for the entity in CRM (e.g. another user's deal with restricted visibility).
- The Bitrix24 app does not have the required scope in its permissions on the Bitrix24 account.
- The requested module is disabled in the Bitrix24 account (no CRM, no bot platform, and so on).

**Fix:**
- Check the user's permissions in the Bitrix24 entity card.
- Open the app settings in the Bitrix24 account and broaden the permission set.

---

### `WRITE_BLOCKED_READONLY_KEY` (403)

The key is in read-only mode (`accessMode: "READONLY"`), but the request performs a write. The full description of the mode, switching, and portal policy is in [Access mode](./keys-auth/access-mode.md).

```json
{
  "success": false,
  "error": {
    "code": "WRITE_BLOCKED_READONLY_KEY",
    "message": "Key is in read-only mode. Switch to read+write in /keys to enable writes.",
    "details": {
      "method": "crm.item.add",
      "keyName": "MCP key",
      "currentMode": "READONLY",
      "switchUrl": "/keys"
    }
  }
}
```

**`details` fields:**

| Field | When returned | Description |
|------|-------------------|----------|
| `method` | Only when proxying to Bitrix24 | The Bitrix24 method name that would have been called on a successful write (e.g. `crm.item.add`). Not returned for management keys — the block is based on the request's HTTP method |
| `keyName` | Always | The key name from your account. If the key has no name — `"unnamed"` is returned |
| `currentMode` | Always | The key's effective mode — always `"READONLY"` for this error |
| `switchUrl` | Always | The path to the account page where the mode is switched — `"/keys"` |

**Causes:**

- **An API key or authorization key (`vibe_api_`, `vibe_app_`)** in `READONLY` mode made a call that proxies to Bitrix24 as a write operation: create, update, delete, an action on an entity.
- **A management key (`vibe_live_`)** in `READONLY` mode made a request with the `POST`, `PATCH`, `PUT`, or `DELETE` HTTP method — e.g. an attempt to create a key via `POST /v1/keys` or delete a feedback record.

**Fix:**

- The key owner should open [API Keys](/keys), select "Read and write" under the **Access mode** block in the relevant key's card, and save. The mode applies to the next request, no reissue needed.
- If the toggle in the card is unavailable — the Bitrix24 account administrator restricted the mode. Ask the administrator to lift the restriction for this key.
- When working through an AI agent — the effective mode is returned by `GET /v1/me` in the `data.accessMode` field. If writes are needed permanently, issue a separate key in "read and write" mode.

---

### `VALIDATION_ERROR` (400)

The body or query parameters failed schema validation. Most V1 endpoints validate via Zod, so the message lists the specific problematic fields.

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "fields.title: Required; fields.stageId: Expected string, received number"
  }
}
```

**Causes:**
- Required fields are missing.
- A value's type does not match the schema (string instead of number, wrong date format).
- The `Content-Type: application/json` header is missing. A body that cannot be parsed as JSON is rejected with separate codes — `INVALID_JSON_BODY`, `FST_ERR_CTP_INVALID_JSON_BODY`, or `fst_err_ctp_invalid_json_body` on the AI Router routes. Which one you get depends on the route — see the table above.

**Fix:**
- Check the body against the endpoint schema in the [entity reference](./entity-api.md) or on the specific endpoint page.
- Pass numbers without quotes, dates in ISO 8601 format (`2026-04-29T10:00:00`).
- Add the `Content-Type: application/json` header.

---

### `INVALID_PARAMS` (400)

Bitrix24 or the route handler found an invalid parameter value. Bitrix24 errors of the form `INVALID_PARAMS: ...` arrive under this same code.

```json
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Path parameter :id must be a positive integer"
  }
}
```

**Causes:**
- An invalid path parameter value (e.g. a non-number where a number is expected).
- Bitrix24 rejected a request parameter (e.g. an unsupported enum field value).

**Fix:**
- Check the endpoint page: which values are valid for each parameter.
- For `filter`, use the field list from `GET /v1/<entity>/fields`.

---

### `MISSING_REQUIRED_FILTER` (400)

A required filter was not passed on a list endpoint that needs context.

```json
{
  "success": false,
  "error": {
    "code": "MISSING_REQUIRED_FILTER",
    "message": "filter[entityType] and filter[entityId] are required for /v1/timelines"
  }
}
```

**Causes:**
- The list of timeline records, comments, and other "nested" data requires a pair of parent identifiers.

**Fix:**
- Add the required filter parameters listed in `message` or on the endpoint page.

---

### `BATCH_LIMIT_EXCEEDED` (400)

The request exceeds the element limit in a bulk operation. At the `/v1/batch` level the limit is checked via `INVALID_REQUEST` referencing `Array must contain at most 50 element(s)`. On domain bulk endpoints (e.g. `chats`, `task-comments`) — a dedicated `BATCH_LIMIT_EXCEEDED` code.

```json
{
  "success": false,
  "error": {
    "code": "BATCH_LIMIT_EXCEEDED",
    "message": "Maximum 50 dialogs per bulk request (Bitrix24 batch limit)."
  }
}
```

**Causes:**
- The array has more than 50 elements.

**Fix:**
- Split the operation into several requests of 50 elements each.
- Use [batch requests](./batch.md) for sequential calls from a single key.

---

### `ENTITY_NOT_FOUND` (404)

A CRM entity record with the given `id` does not exist or was deleted.

```json
{
  "success": false,
  "error": {
    "code": "ENTITY_NOT_FOUND",
    "message": "Item not found"
  }
}
```

**Causes:**
- A record with this `id` genuinely does not exist.
- The record was deleted by a concurrent process.
- The wrong entity: the request goes to `/v1/deals/:id`, but the ID is from a lead.

**Fix:**
- Check that the record exists via the entity's list endpoint.
- Restore it from the Bitrix24 recycle bin if it was deleted recently (via the Bitrix24 account interface).

---

### `RATE_LIMITED` (429)

Bitrix24 throttled the request rate or an internal Vibecode limit was triggered.

```json
{
  "success": false,
  "error": {
    "code": "RATE_LIMITED",
    "message": "QUERY_LIMIT_EXCEEDED",
    "hint": "Wait 1-2 seconds and retry. Use POST /v1/batch to combine up to 50 calls in 1 request.",
    "retryAfter": 2
  }
}
```

**Response headers:**

```
Retry-After: 2
```

**Causes:**
- Too many concurrent requests piled up from a single key: the general platform limit is counted per API key. The exceptions are [`POST /v1/search`](./search/run.md), [`POST /v1/research`](./search/research.md) and [`POST /v1/batch`](./batch.md), where the limit is counted per portal and all of its keys share one counter.
- The requests-per-second limit on the Bitrix24 side was exceeded. It is counted per portal.

**Fix:**
- Wait the number of seconds given in `error.retryAfter` or the `Retry-After` header.
- Implement retries with exponential backoff.
- Combine up to 50 calls via [`POST /v1/batch`](./batch.md).
- Cache rarely changing reference data — fields, statuses, currencies.

---

### `ERROR_LOOP_DETECTED` (429)

Vibecode detects a series of identical errors on a single key for a single Bitrix24 method — and temporarily blocks the request on the Vibecode side to avoid inflating Bitrix24 counters. Every Nth request is forwarded onward: if the backend has recovered, the block is lifted automatically.

```json
{
  "success": false,
  "error": {
    "code": "ERROR_LOOP_DETECTED",
    "message": "Vibe-side block (not a Bitrix24 limit). 12 failures on crm.deal.list in the last hour.",
    "hint": "Every 50th request will probe for recovery — keep retrying with backoff. If you suspect a platform-side issue (e.g. 5xx during an outage), platform admin can clear the block via POST /api/platform/analytics/circuit-breaker/<apiKeyId>/clear.",
    "retryAfter": 60
  }
}
```

**Causes:**
- A bug in the client code: a request with identical parameters repeats and consistently fails.
- A platform outage on the Bitrix24 side during the burst of requests.

**Fix:**
- Read `message` — it names the specific method with the error series.
- Find the request's source in the code, fix the parameters or logic.
- During a platform outage — the platform administrator can clear the block via `POST /api/platform/analytics/circuit-breaker/<apiKeyId>/clear`.

---

### `BILLING_EXHAUSTED` (402)

The billing account dropped into the red zone: the balance is negative, the grace period is exhausted. Requests to create and wake infrastructure are blocked until a top-up.

```json
{
  "success": false,
  "error": {
    "code": "BILLING_EXHAUSTED",
    "message": "Account is frozen due to negative balance.",
    "userMessage": "The billing account is frozen due to a negative balance. Top up the account to continue working with servers and agents.",
    "hint": "Top up the account at /billing/topup, then call POST /v1/portals/:id/refresh-tariff."
  }
}
```

**Causes:**
- There are no funds on the balance to cover the hourly cost of servers and agents.

**Fix:**
- Top up the balance on the `/billing/topup` page.
- After topping up, call `POST /v1/portals/:id/refresh-tariff` to update the status.

---

### `COMMERCIAL_PLAN_REQUIRED` (402)

Creating servers and agents is unavailable on the free Bitrix24 plan after the trial period ends.

```json
{
  "success": false,
  "error": {
    "code": "COMMERCIAL_PLAN_REQUIRED",
    "message": "Commercial Bitrix24 plan required for infrastructure operations.",
    "userMessage": "Infrastructure creation is available on commercial Bitrix24 plans. The trial period has already been used.",
    "hint": "Upgrade plan at https://www.bitrix24.com/prices/, then call POST /v1/portals/:id/refresh-tariff."
  }
}
```

**Fix:**
- Upgrade the Bitrix24 plan to a commercial one.
- After upgrading, call `POST /v1/portals/:id/refresh-tariff` or `GET /v1/me?refresh=tariff`.

---

### `TRIAL_EXPIRED` (402)

The trial period has ended and the plan is still free.

```json
{
  "success": false,
  "error": {
    "code": "TRIAL_EXPIRED",
    "message": "Trial period has ended.",
    "userMessage": "The trial period has ended. Switch to a commercial Bitrix24 plan to keep using servers and agents."
  }
}
```

**Fix:**
- Switch to a commercial Bitrix24 plan.
- Update the status via `POST /v1/portals/:id/refresh-tariff`.

---

### `BITRIX_ERROR` (422)

Bitrix24 returned a business error that does not fall under narrower categories (`ACCESS_DENIED`, `NOT_FOUND`, `INVALID_PARAMS`, `RATE_LIMITED`).

```json
{
  "success": false,
  "error": {
    "code": "BITRIX_ERROR",
    "message": "Invalid filter: field 'NON_EXISTENT_FIELD' is not allowed in filter",
    "hint": "Known Bitrix24 limitation: …"
  }
}
```

**Causes:**
- Bitrix24 rejected the operation for a business reason: incompatible state, unsupported value, business rule.
- The request runs in a Bitrix24 account where the corresponding module is disabled.

**Fix:**
- Read `message` — it holds the original error text from Bitrix24.
- If `hint` is present — use it as the first diagnostic step.

---

### `BITRIX_UNAVAILABLE` (502)

Bitrix24 returned 5xx or did not respond within the allotted time.

```json
{
  "success": false,
  "error": {
    "code": "BITRIX_UNAVAILABLE",
    "message": "Bitrix24 returned 503 Service Unavailable"
  }
}
```

**Causes:**
- Maintenance or overload on the Bitrix24 side.
- Network problems between Vibecode and the Bitrix24 portal.

**Fix:**
- Retry the request after a few minutes, implementing retries with exponential backoff.
- ⚠️ **For write operations (POST/PATCH) — first check whether the original request applied.** A slow portal may process the write AFTER the API already returned a timeout — a blind retry creates a duplicate (task, epic, comment). Before retrying, `GET` the list/record (e.g. search by the title you just sent) and retry only if the record is absent.
- Check the portal status at `/bitrix/admin/site_checker.php` (for the Bitrix24 account administrator).

---

### `QUEUE_OVERFLOW` (429)

The request queue to Bitrix24 for a specific portal is overflowing: too many calls are already pending (more than 100 by default). The response returns instantly, within milliseconds, with the HTTP header `Retry-After: N` (seconds).

```json
{
  "success": false,
  "error": {
    "code": "QUEUE_OVERFLOW",
    "message": "Portal queue overloaded — 100 Bitrix24 calls already pending",
    "userMessage": "The portal queue is overflowing. Too many concurrent requests to Bitrix24.",
    "hint": "Honor the Retry-After header. Use exponential backoff with jitter for repeated failures.",
    "retryAfter": 10
  }
}
```

**Fix:**
- The portal queue is overflowing — retry after `Retry-After` seconds, using backoff with jitter (a random addition to the delay) so retries from different clients don't arrive as one wave.
- Reduce concurrency on the client side.
- Combine calls via `POST /v1/batch`.

---

### `QUEUE_TIMEOUT` (429)

The request queue to Bitrix24 for a specific portal is saturated: more than 30 seconds of waiting.

```json
{
  "success": false,
  "error": {
    "code": "QUEUE_TIMEOUT",
    "message": "Portal queue saturated — too many concurrent Bitrix24 calls",
    "userMessage": "Requests to Bitrix24 have been queued for over 30 seconds. The portal likely has many concurrent operations.",
    "hint": "If this is a /search request with a wide date range, try adding \"autoWindow\": false OR narrow the date range to <14 days. See /v1/guide for optimization tips.",
    "retryAfter": 10
  }
}
```

The request was NOT sent to Bitrix24 — safe to retry.

**Fix:**
- Retry after `retryAfter` seconds, using backoff with jitter.
- Reduce concurrency.
- For `/search` endpoints — narrow the date range or pass `autoWindow: false`.
- Combine calls via `POST /v1/batch`.

---

### `BITRIX_TIMEOUT` (503)

Bitrix24 accepted the request but did not respond within 15 seconds — the outcome is unknown: the request MAY have applied on the portal side.

```json
{
  "success": false,
  "error": {
    "code": "BITRIX_TIMEOUT",
    "message": "Bitrix24 accepted the request but did not respond within 15s",
    "hint": "For WRITE operations, verify whether the change was applied (re-read the entity) before retrying. Reads are safe to retry.",
    "retryAfter": 10
  }
}
```

**Fix:**
- For reads (GET/`/search`) — safe to retry, after a longer backoff than for `429`.
- ⚠️ **For write operations (POST/PATCH) — re-read the entity first.** The change may have already applied on the Bitrix24 side even though no response arrived. A blind retry creates a duplicate (task, epic, comment). Check whether the record exists (e.g. search by the title you just sent) and retry the write only if it's absent.

---

### `INTERNAL_ERROR` (500)

An unexpected error on the Vibe API side.

```json
{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Internal server error"
  }
}
```

**Fix:**
- Retry the request.
- If the error reproduces consistently — submit a ticket via `POST /v1/feedback` with the request time. The `X-Request-Id` header from the response speeds up diagnosis.

## Retries and backoff

Summary of codes that signal a temporary problem and imply a retry:

| Code | HTTP | Retry strategy |
|-----|------|--------------------|
| `RATE_LIMITED`, `QUEUE_OVERFLOW`, `QUEUE_TIMEOUT` | 429 | Retry after `Retry-After` + backoff with jitter (a random addition to the delay) |
| `BITRIX_TIMEOUT` | 503 | Longer backoff than for 429. For write operations — verify-before-retry first: re-read the entity, the change may have already applied |
| `BITRIX_UNAVAILABLE` | 502 | A retry won't help without changing the request — this is a 5xx from Bitrix24 itself or a network problem between Vibecode and the Bitrix24 portal, not a temporary queue overload |

## Error handling in code

### JavaScript

```javascript
async function vibeRequest(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      'X-Api-Key': process.env.VIBE_API_KEY,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  const data = await response.json();

  if (!data.success) {
    const { code, message, retryAfter } = data.error;

    switch (code) {
      case 'RATE_LIMITED':
      case 'QUEUE_TIMEOUT': {
        const wait = retryAfter ?? Number(response.headers.get('Retry-After')) ?? 1;
        await new Promise(r => setTimeout(r, wait * 1000));
        return vibeRequest(url, options);
      }

      case 'BITRIX_UNAVAILABLE':
        await new Promise(r => setTimeout(r, 5000));
        return vibeRequest(url, options);

      case 'MISSING_API_KEY':
      case 'INVALID_API_KEY':
        throw new Error('Check the API key');

      default:
        throw new Error(`${code}: ${message}`);
    }
  }

  return data;
}
```

### Python

```python
import os
import time
import requests

def vibe_request(url, method="GET", json_data=None):
    headers = {
        "X-Api-Key": os.environ["VIBE_API_KEY"],
        "Content-Type": "application/json",
    }

    response = requests.request(method, url, headers=headers, json=json_data)
    data = response.json()

    if not data.get("success"):
        err = data.get("error", {})
        code = err.get("code")
        message = err.get("message")
        retry_after = err.get("retryAfter") or int(response.headers.get("Retry-After", 1))

        if code in ("RATE_LIMITED", "QUEUE_TIMEOUT"):
            time.sleep(retry_after)
            return vibe_request(url, method, json_data)

        if code == "BITRIX_UNAVAILABLE":
            time.sleep(5)
            return vibe_request(url, method, json_data)

        raise Exception(f"{code}: {message}")

    return data
```

### PHP

```php
function vibeRequest(string $url, string $method = 'GET', ?array $data = null): array {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => [
            'X-Api-Key: ' . getenv('VIBE_API_KEY'),
            'Content-Type: application/json',
        ],
    ]);
    if ($data !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    }
    $body = json_decode(curl_exec($ch), true);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === null) {
        throw new Exception("HTTP error: $httpCode");
    }

    if (empty($body['success'])) {
        $errCode = $body['error']['code'] ?? 'UNKNOWN';
        $errMsg = $body['error']['message'] ?? 'Unknown error';
        $retryAfter = $body['error']['retryAfter'] ?? 1;

        if (in_array($errCode, ['RATE_LIMITED', 'QUEUE_TIMEOUT'], true)) {
            sleep((int) $retryAfter);
            return vibeRequest($url, $method, $data);
        }

        if ($errCode === 'BITRIX_UNAVAILABLE') {
            sleep(5);
            return vibeRequest($url, $method, $data);
        }

        throw new Exception("$errCode: $errMsg");
    }

    return $body;
}
```

## See also

- [Quick Start](./quickstart.md) — your first request in 2 minutes
- [Keys and authorization](./keys-auth.md) — key types, format, scopes
- [Optimization and batch](./optimization.md) — reducing call volume and avoiding `RATE_LIMITED`
- [Batch requests](./batch.md) — combining up to 50 calls into one request
- [CLI and cURL](./cli.md) — working via the command line
- [MCP for AI](./mcp.md) — integration with AI tools
