Para agentes de IA: markdown desta página — /docs-content-en/optimization.md índice da documentação — /llms.txt
Os artigos da documentação estão disponíveis atualmente em inglês.
Limits and optimization
Vibecode automatically merges calls and paginates result sets server-side. 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 | Paging and counts | Exporting a large collection | Batch calls | Date-windowed search | Aggregation | Portal queue | Client-side timeout | 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.hasMore field (and meta.total, if a count was requested — see below). 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.
This mode is built for result sets of up to 5000 records. A collection of tens of thousands of records is read with a cursor — Exporting a large collection.
Paging and record counts
These are two different tasks, solved by different fields. Conflating them is the most expensive mistake you can make on lists.
Page by meta.hasMore. The signal is derived from page fullness: a full page means there may be more, a short page means the collection has ended. When the collection size is an exact multiple of limit, the last step returns an empty list — that is the normal end-of-list signal. The field describes the state of one response, not an immutable snapshot of the collection.
When a method response includes meta.nextAfterId, use the following approach for large scans: sort strictly by id ascending, disable the exact count with withTotal=false, and use select to request only the required fields together with id. The meta.nextAfterId field carries the identifier of the last returned record. Pass it back as filter[>id] and the next page starts after it. Such a scan does not depend on an offset and does not get more expensive towards the end of the collection:
GET /v1/deals?order[id]=asc&limit=50&withTotal=false&select=id,title
GET /v1/deals?order[id]=asc&limit=50&withTotal=false&select=id,title&filter[>id]=<meta.nextAfterId of the previous response>
The cursor is available for deals, leads, contacts, companies, quotes, and smart process items. Other entities carry no meta.nextAfterId in the response: page them with offset and narrow the result set with a filter. A ready-made pagination loop in JavaScript — Pagination.
Internally, this mode follows the recommended Bitrix24 pattern: start=-1, order=ID ASC, and an ID filter greater than the last received identifier. The client does not pass start to the Vibecode API. Vibecode applies it only to methods verified to support an id filter together with start=-1. For other methods, the service preserves request correctness and may not apply the no-count mode.
This scan assumes that records are not deleted and access rights do not change before it finishes. If either condition is violated, some records may be skipped. The Vibecode API does not guarantee a complete traversal as a client invariant.
Take the count from an available aggregation operation. Counting a collection is disproportionately expensive for Bitrix24 — markedly more so than returning a page. If you need an exact number, first read the entity's operations.search.paginationStability.counting advice in GET /v1/guide. When the advice contains an aggregation path, ask for the number in a single aggregation call with the count function. When the advice exists but contains no path, there is no cheap exact count: read meta.total only when the field is present, and bound the scan by meta.hasMore. If the entire counting block is absent together with the generic search operation, do not guess an aggregation path — follow the pointer to the entity or domain documentation in the same guide and use only an explicitly documented count operation. If you do not need the number, turn the count off with withTotal=false. On POST /v1/{entity}/search it is the body field of the same name. In the supported mode, meta.total is absent and no separate COUNT runs. Important: As an optimization, withTotal=false only works at a limit of at most 50 — at that size the count really is not requested. Above 50 the platform needs the count to plan the walk, so the parameter removes the number and not the load, and on top of that it discards the exact count a short first page would have handed over for free.
If withTotal is not passed, the value comes from the totalDefault setting on the API key, and failing that from the platform default. The value currently in effect is shown in the totalDefault block of GET /v1/me.
A disabled count does not always mean no number: on a call with offset = 0 where the page came back shorter than the requested limit, the exact count is known from the page itself and arrives for free. An explicit withTotal=false in the request removes that too — for the full table of when the field is present, see Paging and record counts.
The converse holds as well: the presence of meta.total does not mean it was paid for with a count. On a multi-page call (limit above 50) the count runs only when it cannot be avoided — the shape of the response is unchanged, and meta.total arrives exactly as it always has. Nothing is required of you here.
Do not emulate a counter by walking the collection. Paging through five thousand records just to learn there are 4863 of them is a hundred calls instead of one, and for a Bitrix24 account it is the worst load you can generate. When aggregation is available, one aggregate with count gives the same number in a single call.
meta.total is an informational field. It may lag behind the current state of the collection by up to a minute, so the number of returned rows can occasionally exceed it. Completeness of the result does not depend on meta.total.
Exporting a large collection
A wide result set in a single call is built for collections that fit within 5000 records. Once tens of thousands of records match the filter, the task changes shape: such a collection is read with a cursor rather than with one wide limit.
The cursor is described above, in Paging and record counts: sort strictly by id ascending, set withTotal=false, list the fields you need in select together with the mandatory id, and pass meta.nextAfterId from the previous response back as filter[>id]. Every call reads one short page, so the walk does not get more expensive towards the end of the collection, and an interrupted walk resumes from the last cursor instead of starting over. A ready-made JavaScript loop — Pagination.
The sign that a result set did not fit into the time allowed for it is a 429 with the code OPERATION_TIME_LIMIT or RATE_LIMITED. Both mean a pause for the calling key, and repeating the same wide request leads to the same outcome — switch to the cursor.
Filtering by a custom field
Filtering by the value of a custom field is done on the Bitrix24 side: pass the condition in filter under the same name the field carries in the entity schema — the spelling is covered in Filtering. This is the first choice, because you then read the matches rather than the whole collection.
If such a request runs into the time limit on a large collection, put the field in select and pick the records you need on your side. A custom field is passed in select alongside the standard ones, and the cursor keeps working.
GET /v1/companies?order[id]=asc&limit=50&withTotal=false&select=id,title,ufCrm_1698325419&filter[>id]=<meta.nextAfterId of the previous response>
The response carries the field value for every record, and the filtering then happens without another call to Bitrix24. This path reads the whole collection, so it is the fallback. The custom field names on your Bitrix24 account are listed by GET /v1/{entity}/fields.
Batch calls across multiple entities
POST /v1/batch merges up to 50 operations across 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:
{
"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:
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, pass items. For list / get / fields, pass 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:
Fetch the root list in a single call — Vibecode paginates server-side automatically:
GET /v1/deals?limit=5000&select=id,title,companyId,assignedByIdCollect the
idvalues of the related entities and load them in batches of 50 viaPOST /v1/batch:{ "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 carries up to 50 related records. The loop repeats for the next batch of
idvalues.
In this scenario the root fetch takes one network request (Vibecode pages through the records in steps of 50 internally), 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:
{
"filter": { "createdAt": { "$gte": "2026-01-01", "$lte": "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, the remainder is left out of 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 — use it when you hit network timeouts or unstable results. The full list of search parameters is in each entity's documentation.
Incomplete result
A result can also be incomplete when no window failed at all. The response then carries a meta.warnings array with an entry { "code": "WINDOW_TRUNCATED", "field": "...", "message": "..." }. There are two causes, and the message text names the one that fired: either a single window held more records than one window fetch returns, or the window fetches together hit the 5000-record ceiling and the remaining windows were never sent. Both causes share one code — you can branch on it without parsing the text. The warning arrives for entities whose list method Bitrix24 serves page by page.
The field value names the range field by its original Bitrix24 name, not by the one you sent: for entities that carry their own field names these are different strings. The mapping between the two is in the entity's field reference, for example GET /v1/deals/fields. Do not compare field against your own filter keys directly.
Below is the meta block of a search over deals across two years. The data array in such a response is populated and holds the records that were collected:
{
"total": 5000,
"hasMore": false,
"autoWindowed": true,
"windowCount": 105,
"batchWaves": 2,
"durationMs": 41230,
"warnings": [
{
"code": "WINDOW_TRUNCATED",
"field": "createdTime",
"message": "This result is incomplete: the range filter on \"createdTime\" reached the 5000-row ceiling of a windowed search, so the remaining time windows were never requested. Narrow the date range or add filters — paging is not available on a windowed search."
}
]
}
Check meta.warnings before treating a result as complete. Neither meta.hasMore nor meta.total serves that purpose: in the example above both say the result set has ended — they describe what was collected, not what was left beyond the boundary. Paging does not help either: while date windowing is active, an offset above zero is rejected with 400 UNSTABLE_OFFSET_PAGINATION, so you cannot read the remainder with a next-page request. Narrow the date range or add filters so the search stops hitting the ceiling. A search over a narrow range that is not split into windows is unaffected by this warning.
Aggregation instead of fetching records
When you need sums, minimums, maximums, or averages, first make sure GET /v1/guide exposes operations.aggregate for the entity. Then POST /v1/{entity}/aggregate returns the result without a separate record fetch:
{
"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 the result comes back in a single call regardless of result-set size. The sum / avg / min / max functions load records page by page up to 5000. The response carries meta.truncated: true when fewer records were read than data.count promised — including when more than 5000 matched the filter — or when the slice was cut short by a sub-page error. The size of the gap is reported in data.meta.recordsShortfall, the interrupted slice in data.meta.pageErrorSample. The ceiling is not the only reason for this marker, so testing its value is safer than comparing data.count against 5000.
The count function follows the separate paging and record counts rules. Use it only at the path explicitly named by operations.search.paginationStability.counting. The presence of operations.aggregate for other functions does not prove that an exact count is available, or vice versa. The example below applies only to an explicitly named path. Do not walk the collection to calculate a count:
{
"aggregate": [ { "field": "*", "function": "count" } ],
"filter": { "stageId": "NEW" }
}
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 80 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 large result sets —
GET /v1/{entity}?limit=...(Vibecode paginates server-side) orPOST /v1/{entity}/search(date windowing). - When you need aggregates, not records, and
GET /v1/guideexposesoperations.aggregate—POST /v1/{entity}/aggregate. For an exact count, use only the path fromoperations.search.paginationStability.counting.
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). TheRetry-Afterheader carries the recommended pause in seconds.429 QUEUE_TIMEOUT— the request waited for a slot longer than 80 seconds. The request was not sent to Bitrix24 — safe to retry. Theerror.retryAfterbody field carries the recommended pause.
Analytics widgets that fire a burst of /search calls must retry with exponential backoff and random 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.
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))
}
}
Pauses on a single method. Besides the queue, two more pauses can reject a request. Both answer 429 with a Retry-After header and lift automatically, so the retry loop above covers them too. The error.scope field says who the pause applies to:
429 OPERATION_TIME_LIMIT,scope: "apiKey"— Bitrix24 has paused this method for your key for about 5 minutes: the method used up its operating-time budget on the account. Other methods and other keys of the account keep working.429 TIMEOUT_QUARANTINE,scope: "portal"— the method failed to respond within the call timeout several times in a row, and the "portal + method" pair is paused on the Vibecode side. The pause applies to every key of the account. Do not shorten the retry interval: once every 5 minutes one call is let through as a recovery probe, and frequent retries take that slot — the method stays closed longer. The mechanism is rolling out: until it is enabled on the account, this code does not arrive.
These pauses are measured in minutes, not in the seconds a queue refusal takes. Do not wait them out inside a user request — move the retry to a background job.
Both codes are described in Errors.
Client-side timeout
A response does not arrive instantly: the request first waits for a slot in the portal queue, then runs in Bitrix24. Both phases have an upper limit, as does the time the platform holds the connection, and the client-side timeout has to exceed the sum of the first two.
| Phase | Limit | What arrives when it expires |
|---|---|---|
| Waiting for a slot in the portal queue | 80 seconds | 429 QUEUE_TIMEOUT — the request never reached Bitrix24, a retry is safe |
| A single call to Bitrix24 | 60 seconds | 503 BITRIX_TIMEOUT |
| The platform holding the connection | 660 seconds | the connection is closed |
Hence the working values, and they differ for a single page and for a large result set. A request with limit up to 50 makes one call to Bitrix24 and is covered by a timeout of at least 150 seconds — the sum of the first two phases with headroom. A request with limit > 50 reads the records in several sequential calls of 50, each bounded by its own 60 seconds, so the only ceiling above it is how long the connection is held: set the timeout above 660 seconds, at least 690, or page through 50 records at a time. The request waits in the queue only once — for the request as a whole, not once per page.
What helps instead of one long request:
- Read pages of 50 records and walk the cursor. Every call is short, and an interrupted walk resumes from the last
meta.nextAfterIdinstead of starting over. - Set the timeouts separately — one for establishing the connection, one for waiting on data. In Python:
requests.get(url, headers=headers, timeout=(10, 150))— 10 seconds to connect, 150 to wait for data from the server. For a result set withlimit > 50raise the second value to 690. - For result sets over a wide date range —
POST /v1/{entity}/search: the range is split into windows that run in parallel waves.
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:
{
"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 do not add load to 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`, `/v1/statuses`, and `/v1/{entity}/fields`
Responses from GET /v1/users, GET /v1/statuses, and GET /v1/{entity}/fields are cached server-side. Users are cached for 60 seconds. CRM directories and field schemas are cached for 5 minutes. This speeds up dashboards that request these endpoints on every load.
The GET /v1/users cache is bound to the personal key vibe_api_.... The GET /v1/statuses cache is bound to the Bitrix24 account: personal keys from the same account use one cache entry because CRM stages and directories are shared across the account.
The GET /v1/{entity}/fields cache is bound to the Bitrix24 account, authorization key, entity, path parameters, request parameters, and response language. Complete responses are stored for 5 minutes. Responses with the fields_partial warning are not stored, so the next request can fetch the complete field schema.
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 clears the matching cache immediately. POST /v1/users, PATCH /v1/users/:id, and similar operations clear the users and directory caches. POST / PATCH / DELETE on /v1/userfields/:entity and /v1/items/:entityTypeId/userfields clear the GET /v1/{entity}/fields cache for that entity or smart process. An edit made directly in the Bitrix24 interface is invisible to the cache, so such changes may not appear until the cache entry expires: up to 60 seconds for users and up to 5 minutes for directories and field schemas.
To bypass the cache and get guaranteed-fresh data, add the Cache-Control: no-cache header. For GET /v1/{entity}/fields, the refresh=true parameter also works:
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, the Cache-Control: no-cache header, or refresh=true for /fields. 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 has not changed, the endpoint answers 304 Not Modified with an empty body instead of re-sending the whole document:
# 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, Vibecode auto-paginates |
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. A date range greater than 14 days is split into 7-day windows |
POST /v1/chats/messages/bulk — dialogs |
up to 50 in one request |
| Portal queue | a limited number of concurrent requests, a wait of up to 80 seconds |
Response cache for /v1/users / /v1/statuses / /v1/{entity}/fields |
60 seconds / 5 minutes / 5 minutes, bypass — Cache-Control: no-cache header, for /fields also refresh=true |