
# Limits and optimization

Vibecode merges calls and paginates result sets server-side on its own. This article describes the built-in mechanisms and helps you pick the right endpoint for the job.

**Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** `X-Api-Key`

[Auto-pagination](#auto-pagination-in-list) | [Batch calls](#batch-calls-across-multiple-entities) | [Date-windowed search](#search-with-date-windowing) | [Aggregation](#aggregation-instead-of-fetching-records) | [Portal queue](#portal-queue) | [Caching](#caching) | [Summary limits](#summary-limits)

## Auto-pagination in `list`

The `limit` parameter in `GET /v1/{entity}` accepts values up to 5000. When `limit > 50`, Vibecode splits the result set into internal pages of 50 records and assembles them into a single response:

```
GET /v1/deals?limit=500&filter[stageId]=NEW
```

Returns up to 500 records plus the `meta.total` and `meta.hasMore` meta fields. If more than 5000 records match the filter, the first 5000 are included and `meta.hasMore` comes back `true` — for the remainder, either narrow the filter or use `POST /v1/{entity}/search`.

## Batch calls across multiple entities

`POST /v1/batch` merges up to 50 operations over different entities into a single HTTP request. Each call is identified by its own `id`; a failure in one does not cancel the others.

Suited to dashboards and summary pages where a single load needs data from different places:

```json
{
  "calls": [
    { "id": "deals", "entity": "deals", "action": "list", "params": { "filter": { "stageId": "NEW" }, "limit": 50 } },
    { "id": "tasks", "entity": "tasks", "action": "list", "params": { "filter": { "responsibleId": 1 }, "limit": 20 } },
    { "id": "user", "entity": "users", "action": "get", "entityId": 1 }
  ]
}
```

Full specification — [Batch calls](/docs/batch).

## Batch operations on a single entity

`POST /v1/{entity}/batch` bulk-creates, updates, or deletes up to 500 records of a single entity. Internally Vibecode splits the request into batches of 50 items:

```bash
curl -X POST https://vibecode.bitrix24.com/v1/deals/batch \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "update",
    "items": [
      { "id": 575, "stageId": "WON" },
      { "id": 741, "stageId": "WON" }
    ]
  }'
```

Actions: `create`, `update`, `delete`, `list`, `get`, `fields`. For `delete` pass an `ids` array; for `create` and `update` — `items`; for `list` / `get` / `fields` — `calls`.

## Bulk scanning with on-demand loading of related data

When you need to read thousands of records of one entity and pull in related data from other entities, the flow looks like this:

1. Fetch the root list in a single call — Vibecode paginates server-side on its own:

   ```
   GET /v1/deals?limit=5000&select=id,title,companyId,assignedById
   ```

2. Collect the `id` values of the related entities and load them in batches of 50 via `POST /v1/batch`:

   ```json
   {
     "calls": [
       { "id": "company-15", "entity": "companies", "action": "get", "entityId": 15 },
       { "id": "company-22", "entity": "companies", "action": "get", "entityId": 22 },
       { "id": "user-1",     "entity": "users",     "action": "get", "entityId": 1 }
     ]
   }
   ```

   One HTTP request — up to 50 related records; the loop repeats for the next batch of `id` values.

In this scenario the root fetch takes one network request (Vibecode pages through the records in steps of 50 on its own), and the associations are loaded at a pace of one HTTP request per 50 items instead of a request per item.

## Search with date windowing

`POST /v1/{entity}/search` is designed for large result sets. If the filter contains a date condition with a range greater than 14 days, Vibecode automatically splits the request into 7-day windows and processes them in parallel:

```json
{
  "filter": { "$gte": { "createdAt": "2026-01-01" }, "$lte": { "createdAt": "2026-04-30" } },
  "select": ["id", "title", "stageId"],
  "limit": 5000
}
```

On a partial window failure — when some windows returned data and the response comes back with HTTP 200 — the following fields appear in `meta`:

| Field | Description |
|------|---------|
| `meta.autoWindowed` | `true` when the request was split into date windows. |
| `meta.windowCount` | Number of windows the request was split into. |
| `meta.windowErrors` | Number of windows for which Bitrix24 returned an error. The remaining windows return their data. |
| `meta.windowErrorSample` | An object `{ code, message }` — the code and text of the first failed window, so you can see why data was lost. |
| `meta.batchWaves` | Number of parallel window-dispatch waves. Appears when batched window dispatch was used. |
| `meta.hasMore` | Whether there are records beyond `limit`. When more than 5000 records match, part of them stays outside the result set — narrow the filter or the date range. |

If **all** windows fail, this meta block is absent — the real Bitrix24 error code is returned, as for a narrow range: `UNKNOWN_FILTER_FIELD`, `INVALID_PARAMS`, `BITRIX_ACCESS_DENIED`, `RATE_LIMITED`, `BITRIX_UNAVAILABLE`, or `BITRIX_TIMEOUT` (503). The separate `WINDOWED_SEARCH_FAILED` code is no longer returned.

Windowing is disabled with the `"autoWindow": false` flag in the request body — appropriate during network timeouts or unstable results. The full list of `search` parameters is in each entity's documentation.

## Aggregation instead of fetching records

When you only need counts, sums, minimums, maximums, or averages — `POST /v1/{entity}/aggregate` returns the result in a single call, without fetching the records themselves:

```json
{
  "aggregate": [
    { "field": "amount", "function": "sum" },
    { "field": "amount", "function": "avg" }
  ],
  "filter": { "stageId": "WON" },
  "groupBy": "assignedById"
}
```

The response contains `data.count`, `data.aggregates` and (when `groupBy` is present) a `data.groups` array broken down by the grouping field. Without `groupBy` the response is limited to a single object of summary values. For the `count` function Bitrix24 returns the result in a single call regardless of result-set size; `sum` / `avg` / `min` / `max` load records page by page up to 5000 — at a larger volume `meta.truncated: true` comes back.

## Portal queue

Each Bitrix24 account has its own queue to the Vibecode API: a limited number of requests run concurrently, the rest wait for a slot. If a request hangs in the queue longer than 30 seconds, `429 QUEUE_TIMEOUT` is returned with a `userMessage` and a `hint`.

What reduces the load on the queue:

- Merge disparate calls via `POST /v1/batch` — one HTTP request instead of several.
- For bulk CRUD on a single entity — `POST /v1/{entity}/batch` (up to 500 records per request).
- For wide result sets — `GET /v1/{entity}?limit=...` (Vibecode paginates server-side) or `POST /v1/{entity}/search` (date windowing).
- When you need numbers, not records — `POST /v1/{entity}/aggregate`.

**Retry on queue overload.** Under load the queue returns two different codes, and both mean "retry later":

- `429 QUEUE_OVERFLOW` — the queue is full, the request is rejected immediately (within milliseconds). The `Retry-After` header carries the recommended pause in seconds.
- `429 QUEUE_TIMEOUT` — the request waited for a slot longer than 30 seconds. The request was NOT sent to Bitrix24 — safe to retry. The `error.retryAfter` body field carries the recommended pause.

Analytics widgets that fire a burst of `/search` calls in a row must retry with exponential backoff and random jitter (backoff with jitter), respecting `Retry-After`, rather than retrying instantly in a loop — that makes the overload worse. Lower the concurrency: run the requests sequentially or combine them into `POST /v1/batch`.

```javascript
async function callWithBackoff(url, options, maxRetries = 4) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, options)
    if (res.status !== 429) return res
    if (attempt >= maxRetries) return res

    // Both codes (QUEUE_OVERFLOW, QUEUE_TIMEOUT) carry the pause in the Retry-After header;
    // error.retryAfter in the body duplicates the same value
    const headerWait = Number(res.headers.get('Retry-After'))
    const bodyWait = Number((await res.clone().json())?.error?.retryAfter) || 0
    const baseSec = headerWait || bodyWait || Math.min(2 ** attempt, 30)
    const jitterMs = Math.floor(Math.random() * 1000)
    await new Promise(r => setTimeout(r, baseSec * 1000 + jitterMs))
  }
}
```

## Loading messages from multiple dialogs

`POST /v1/chats/messages/bulk` returns messages from no more than 50 dialogs in a single response and accepts `lastId` / `firstId` cursors and a `limit` per dialog:

```json
{
  "dialogs": [
    { "dialogId": "chat253", "limit": 20 },
    { "dialogId": "chat741", "lastId": 9357, "limit": 50 }
  ]
}
```

Scope: `im`. Response format — `{ results, errors, summary }`, the same as `/v1/batch`.

## Caching

Some responses are served from a cache, so repeat reads don't load Bitrix24 or the portal queue. The cache is transparent — the response body is identical to the uncached one, and the `X-Cache` header shows where the response came from.

### Response cache for `/v1/users` and `/v1/statuses`

Responses of `GET /v1/users` and `GET /v1/statuses` are cached server-side: users for 60 seconds, CRM directories for 5 minutes. This speeds up dashboards that request these endpoints on every load.

The cache is bound to the personal key `vibe_api_...`. For an OAuth-app authorization key `vibe_app_...` the cache is not used — different users have different access to account data.

A write through the API — `POST /v1/users`, `PATCH /v1/users/:id` and similar — clears the cache immediately. An edit made directly in the Bitrix24 interface is invisible to the cache, so such changes may appear with a delay up to the end of the cache lifetime: up to 60 seconds for users and up to 5 minutes for directories.

To get guaranteed-fresh data bypassing the cache, add the `Cache-Control: no-cache` header:

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  -H "Cache-Control: no-cache" \
  https://vibecode.bitrix24.com/v1/users
```

The `X-Cache` header in the response shows how the request was handled:

| Value | Meaning |
|----------|--------------|
| `HIT` | Response served from cache |
| `MISS` | Response fetched from Bitrix24 and stored in the cache |
| `COALESCED` | The request joined an in-flight fetch for the same data |
| `BYPASS` | The cache was not used — an OAuth-app authorization key or the `Cache-Control: no-cache` header. The reason is given in the `X-Cache-Bypass-Reason` header |

### HTTP cache for discovery endpoints

`GET /v1/openapi.json` and `GET /v1/guide` return large documents that a client need not re-download on every startup. Both responses carry standard HTTP cache headers:

- `/v1/openapi.json` — `Cache-Control: public, max-age=300`. The specification is identical for every client, so any cache may store it.
- `/v1/guide` — `Cache-Control: private, max-age=300`. The response content depends on the key's scopes, so a shared cache must not store it.
- `ETag` — a content fingerprint that changes only when the document or the key's scope set changes.

Keep the `ETag` from the first response and send it in the `If-None-Match` header on repeat requests. If the content hasn't changed, the endpoint answers `304 Not Modified` with an empty body instead of re-sending the whole document:

```bash
# First call — full body and ETag (openapi.json needs no authorization)
curl -i https://vibecode.bitrix24.com/v1/openapi.json
# ... ETag: "a1b2c3d4e5f6a7b8"

# Repeat call — 304 Not Modified, no body transferred
curl -i -H 'If-None-Match: "a1b2c3d4e5f6a7b8"' \
  https://vibecode.bitrix24.com/v1/openapi.json
```

The `max-age=300` header also lets the client and any intermediate cache reuse the response for 5 minutes without contacting the server.

## Summary limits

| Scenario | Limit |
|----------|-------------|
| `GET /v1/{entity}` — `limit` | up to 5000 records; when `limit > 50`, auto-pagination on the Vibecode side |
| `POST /v1/batch` — number of calls | up to 50 in one request |
| `POST /v1/{entity}/batch` — bulk CRUD | up to 500 records in one request |
| `POST /v1/{entity}/batch` — read actions (`list` / `get` / `fields`) | up to 50 calls in the `calls` array |
| `POST /v1/{entity}/search` — `limit` | up to 5000 records; for a date range > 14 days — 7-day windows |
| `POST /v1/chats/messages/bulk` — dialogs | up to 50 in one request |
| Portal queue | a limited number of concurrent requests, wait up to 30 seconds |
| Response cache for `/v1/users` / `/v1/statuses` | 60 seconds / 5 minutes, bypass — `Cache-Control: no-cache` header |

## See also

- [Batch calls](/docs/batch)
- [Filtering syntax](/docs/filtering)
- [Entity API](/docs/entity-api)
- [Error codes](/docs/errors)
- [Quickstart](/docs/quickstart)
