For AI agents: markdown of this page — /docs-content-en/optimization.md documentation index — /llms.txt

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 | Batch calls | Date-windowed search | Aggregation | Portal queue | Caching | 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.

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:

Terminal
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 updateitems; for list / get / fieldscalls.

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:

Terminal
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.jsonCache-Control: public, max-age=300. The specification is identical for every client, so any cache may store it.
  • /v1/guideCache-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:

Terminal
# 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}/searchlimit 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