Para agentes de IA: markdown desta página — /docs-content-en/entity-api.md índice da documentação — /llms.txt
Os artigos da documentação estão disponíveis atualmente em inglês.
API overview
Vibecode provides a single REST interface for working with Bitrix24 entities: CRM, tasks, users, calendar, Drive, catalog, document management, and others. The request, response, and error format is the same for every entity.
Base URL:
https://vibecode.bitrix24.comAuthorization: theX-Api-Key: your_keyheadervibe_app_…keys additionally require anAuthorization: Bearerheader with a session token on every entity request. For unattended jobs (cron, schedulers) with no user present, use avibe_api_…key. More details — Creating and using a key.
Available operations
The standard set of operations is described below. The path follows the /v1/{entity} template.
Each entity has its own set of operations: for some entities certain operations are not available. Which operations an entity supports is shown on its page in the API reference and by the operations field in the GET /v1/guide response. A request to an operation the entity does not have returns 404 ROUTE_NOT_FOUND — Error codes.
List — `GET /v1/{entity}`
Fetch records with pagination, sorting, and field selection.
curl -H "X-Api-Key: $KEY" \
"https://vibecode.bitrix24.com/v1/deals?limit=100&offset=0"
Parameters:
| Parameter | Type | Description |
|---|---|---|
limit |
number | Number of records (default 50, maximum 5000). A value of 0 is not a page size: the parameter is dropped, the default page size applies, and the response carries a LIMIT_ZERO_IGNORED warning in meta.warnings |
offset |
number | Skip N records |
select |
string[] | Field selection: ?select=id,title,amount. Accepts canonical names from GET /v1/{entity}/fields, the original Bitrix24 names of declared fields, and the interchangeable date names updatedAt / updatedTime, createdAt / createdTime. An unknown name is rejected with a 400 UNKNOWN_SELECT_FIELD error on the entities whose field set has been verified against Bitrix24 — deals, contacts, companies, leads, quotes, activities, addresses, smart processes, products and product sections, catalogs with their products and sections, orders and order statuses, statuses, currencies, calendar events and calendar sections, files, folders, storages, departments, workgroups, document templates. On the others it produces an UNKNOWN_SELECT_FIELD warning in meta.warnings. User fields (UF_*, ufCrm*) are never rejected, and product properties are each accepted on THEIR OWN entity: PROPERTY_295 on products, property295 on catalog products, where their numbers are assigned by the Bitrix24 account; the method does not recognize the other spelling, and its gate rejects it. On the list and search operations for requisites and bank details, the name is passed on to Bitrix24, where an account that has no such field rejects the whole call with 422 BITRIX_ERROR. The value * (and UF_*, regardless of case) means "return every field" — no selection is applied, and an unknown name next to it produces a warning instead of an error |
order |
object | Sorting: ?order[createdAt]=desc |
withTotal |
string | Whether to return the count: true or false, exactly these two values. false is the only way to guarantee meta.total is absent from the response. With a limit of 50 or less it also cancels the count; above 50 it removes the number, not the count. Without the parameter the value comes from the API key setting, then from the platform default, and in that case a short page still carries the exact count even without an explicit request — see Paging and record counts |
Auto-pagination: when limit > 50, Vibecode automatically requests several pages from Bitrix24 and returns all records in a single response.
Get by ID — `GET /v1/{entity}/{id}`
curl -H "X-Api-Key: $KEY" \
"https://vibecode.bitrix24.com/v1/deals/123"
Response: { success: true, data: { id: 123, title: "...", ... } }
The select parameter also works when fetching a record by id — the response contains only the listed fields. Three forms are supported: a comma-separated list ?select=id,title,stageId, a repeated key ?select[]=id&select[]=title, and an indexed form ?select[0]=id&select[1]=title. The id field is always returned. An unknown field name returns a 400 UNKNOWN_SELECT_FIELD error on the entities with a verified field set (the list is in the parameter table above), and on the others it carries an UNKNOWN_SELECT_FIELD warning in meta.warnings. A user field is referenced by the name from the schema — User fields (UF). The value * (and UF_*) is the familiar Bitrix24 way to say "return every field": no selection is applied and the full record comes back. The parameter is compatible with include — related data is resolved before the fields are selected.
Create — `POST /v1/{entity}`
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
"https://vibecode.bitrix24.com/v1/deals" \
-d '{"title": "New deal", "stageId": "NEW", "amount": 150000}'
Response: { success: true, data: { id: 456, ... } } (HTTP 201)
A body with no fields returns 400 EMPTY_CREATE_BODY; for entities that have mandatory creation fields it returns 400 MISSING_REQUIRED_FIELDS, naming the first missing field. Both codes — Error codes.
Update — `PATCH /v1/{entity}/{id}`
curl -X PATCH -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
"https://vibecode.bitrix24.com/v1/deals/123" \
-d '{"stageId": "WON", "amount": 200000}'
A body with no fields returns 400 EMPTY_UPDATE_BODY — Error codes.
Delete — `DELETE /v1/{entity}/{id}`
curl -X DELETE -H "X-Api-Key: $KEY" \
"https://vibecode.bitrix24.com/v1/deals/123"
Search — `POST /v1/{entity}/search`
Search with filtering. More on filter syntax: Filtering
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
"https://vibecode.bitrix24.com/v1/deals/search" \
-d '{"filter": {"stageId": "NEW", "amount": {"$gte": 100000}}, "sort": {"createdAt": "desc"}, "limit": 200}'
Parameters:
| Parameter | Type | Description |
|---|---|---|
filter |
object | Filtering conditions (three syntaxes) |
sort |
string | object | array | Sorting. Supported: "id" / "-amount" / "id,-createdAt" (string), { id: "asc", amount: "desc" } or { id: 1, amount: -1 } (object), ["id", "-amount"] (array). Invalid type → 400 INVALID_SORT_TYPE. More on pagination — see filtering.md. |
limit |
number | Number of records (default 50, maximum 5000, auto-pagination when > 50). A value of 0 is dropped — see the LIMIT_ZERO_IGNORED warning. A value of the wrong type — neither a number nor a numeric string — is refused before Bitrix24 is called: 400 INVALID_LIMIT |
offset |
number | Skip N records |
select |
string | string[] | Field selection — the same rules as ?select= in the list above. A value of the wrong type: 400 INVALID_SELECT_TYPE (neither a string nor an array of strings — for example 999, true, {}, or [1,2]); an unknown field name is a separate case — UNKNOWN_SELECT_FIELD |
autoWindow |
boolean | false — disable date windowing for large result sets |
withTotal |
boolean | Whether to return the count. false is the only way to guarantee meta.total is absent from the response. With a limit of 50 or less it also cancels the count; above 50 it removes the number, not the count. Without the field the value comes from the API key setting, then from the platform default, and in that case a short page still carries the exact count even without an explicit request — see Paging and record counts |
Windowed search: for large datasets, search automatically splits the request into time windows. If this causes timeouts, disable it with autoWindow: false.
Aggregation — `POST /v1/{entity}/aggregate`
Counts and numeric aggregations (sum, avg, min, max) with filtering. The operations each entity supports are listed on its page in the API reference. For entities without aggregation the record count arrives in meta.total of the list response.
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"aggregate": [
{ "field": "amount", "function": "sum" },
{ "field": "amount", "function": "avg" }
],
"filter": { "stageId": "WON" }
}' \
"https://vibecode.bitrix24.com/v1/deals/aggregate"
Parameters (body):
| Parameter | Type | Description |
|---|---|---|
aggregate |
array | Array of aggregations: { "field": "amount", "function": "sum" }. Functions: count, sum, avg, min, max. Without an array — only count |
filter |
object | Filtering by entity fields |
groupBy |
string | string[] | Field or array of fields to group by (maximum 5). Allowed values come from the entity's aggregatable list. A value that is neither a string nor an array of strings (a number, a boolean, an object, or a non-string array element) is refused with 400 INVALID_PARAMS — such a request used to answer 200 with no groups key. For entities without that list (for example statuses, currencies, deal-categories), grouping is not supported, but count with a filter is available |
How it works:
countis computed with a single fast call to Bitrix24. Forsum/avg/min/maxthe platform loads the records matching the filter, at most 5000, and computes the aggregates on the Vibecode side. If fewer records are read thandata.countpromised, the response is flaggedmeta.truncated: true, and the ceiling is only one of the reasons for that flag, with the size of the gap reported indata.meta.recordsShortfall. Full details — Aggregation POST — the 5000-record ceiling. For an invalid field the response includes the list of available ones.
User fields (UF) are supported in
sum/avg/min/maxfor the UF typesinteger,double,money.groupByaccepts UF fields of any type. Details — in the Aggregation POST — UF fields section below.
Smart processes (
items) pass the type in the path:POST /v1/items/:entityTypeId/aggregate. ReservedentityTypeIdvalues (1, 2, 3, 4, 7, 31) are served by the dedicated APIs for deals, leads, contacts, companies, quotes, and invoices.
Aggregation POST — the 5000-record ceiling
The ceiling applies to any request that cannot be answered without loading the records themselves: numeric functions and grouping. A plain count without groupBy is not affected — it is computed on the Bitrix24 side and works on a result set of any size.
Behavior beyond 5000 records is currently being rolled out account by account, so there are two cases:
- While the capability is off for the account — the response is
200, carriesmeta.truncated: true, and the result is computed over the first 5000 records. A shortfall below the ceiling raises the same marker: fewer records were read thandata.countpromised, and the size of the gap is reported indata.meta.recordsShortfall. - Once it is on — the response is
422 AGGREGATION_LIMIT_EXCEEDEDand not a single record is loaded. The error text lists what to do next. The reason for the change: on a large result set, loading the first 5000 records did not finish in time, the request timed out, and the truncated response usually never reached the caller at all.
The action is the same in both cases: narrow the filter or use count without grouping. A client that branches on meta.truncated today will start receiving 422 once the capability is enabled for its account — handle both branches.
meta.truncated is now also raised in one more case — and this applies to ALL entities, not only activities. If fewer records were processed than the answer promised, it sets truncated: true and adds meta.recordsShortfall — how many records are missing. The promise is data.count, and under the stage-count mode (meta.aggregatePath: "fanout") the larger of data.count and the sum of the stage counts: within the tolerance the stage probes may add up to more than the pipeline total, and the gap is then measured against them. So the predicate "incomplete if recordsProcessed < totalRecords" does not always hold — read truncated itself. Previously such a response came back with truncated: false, meaning the groups and the numeric aggregations were computed over only some of the records, and the response did not say so. count and meta.totalRecords stay complete.
The truncation marker travels with the number itself. While it lived in meta alone, a client reading data.aggregates.amount.sum and nothing else received a confident number with no hint that it was incomplete. So on a truncated answer every field object in data.aggregates and in groups[].aggregates additionally carries truncated: true:
{
"data": {
"count": 20000,
"aggregates": {
"amount": { "sum": 1234567, "truncated": true }
},
"meta": {
"totalRecords": 20000,
"recordsProcessed": 5000,
"truncated": true,
"warnings": [{ "code": "AGGREGATE_TRUNCATED", "message": "…" }]
}
}
}
The sum / avg / min / max values stay numbers — nothing wraps them into an object. The truncated key inside a field object appears ONLY on a truncated answer: on a complete one it is absent altogether, not false. That covers the conditional markers — data.aggregates.<field>.truncated, the same key inside groups[].aggregates, and groups[].truncated. It does NOT cover data.meta.truncated: that one is always present and simply reads false on a complete answer, so test its value there, never its presence.
The marker sits in the object the number is read from: on a truncated answer the group object itself carries groups[].truncated next to its count. On a count-only grouping that is the only marker next to a number — a count expression produces no field object at all, and both aggregates objects come back empty.
The marker says the answer is a sample. Whether the group counter is a sample too depends on the path, and meta.aggregatePath names it: on the ordinary walk (the field is absent) groups[].count is the size of the slice that was read, not of the whole group, and must not be used as the group total; under the stage-count mode (meta.aggregatePath: "fanout") the counter comes from a separate probe and is exact — only the aggregates beside it remain a sample. Exact numbers never carry it: data.count and meta.totalRecords come from a separate fast probe and are correct at any size. The same condition adds an AGGREGATE_TRUNCATED entry to meta.warnings — a second channel for a client that checks meta.warnings before treating a result as complete.
What to do about it — there are exactly two ways: narrow the filter until the result set fits under the ceiling, or request count alone, with no groupBy and no numeric functions. count is exact at any size because no records are loaded for it at all.
Adding groupBy to a numeric aggregation does not lift the ceiling on any account: grouping makes the request load records, so such an aggregation is computed over the same truncated page and comes back exactly as incomplete. Repeating the same sum with groupBy appended is the one thing to avoid: it is precisely the expensive call the ceiling exists to bound.
Deals have a separate per-stage counting mode that answers exact numbers without reading rows — but it is enabled per account, and a truncated reply does not tell you whether yours has it. Do not guess — probe it with one request: { "aggregate": [{ "field": "*", "function": "count" }], "groupBy": "stageId" } with a scalar categoryId in the filter. If the mode answered, the reply carries meta.aggregatePath: "fanout" and meta.recordsProcessed: 0 and the counts are exact; if it did not, you get the ordinary truncated reply and lose nothing but that one call.
A truncated sum is a sample over some of the records, not a total, and must not be used as one.
Deals have a separate stage-count mode that answers without loading records, but it is not a general way around the ceiling and it is off by default: a platform administrator enables it per account, and it applies only to a groupBy over stageId or stageSemanticId with a scalar categoryId, and only without numeric functions. While it is off — which is the default state — such a request takes the ordinary path and comes back truncated above the ceiling. Full conditions — Aggregate deals.
Aggregation POST — the narrowing filter on activities
Activities carry a different restriction, and it applies even to a plain count: Bitrix24 cannot count all the activities of an account within the time allowed for one call. So an activities aggregate requires one narrowing filter — the ownerTypeId + ownerId pair, or responsibleId, or a date bound on createdAt / updatedAt / deadline. Without one you get 400 MISSING_REQUIRED_FILTER listing the accepted narrowing filters, and while the requirement is not yet on for the account, you get 422 AGGREGATION_LIMIT_EXCEEDED at the moment Bitrix24 genuinely fails to answer. Retrying either one is pointless. Details — Aggregate activities.
Aggregation POST — UF fields
The canonical POST variant of aggregation accepts an array of expressions and supports user fields. The field name is the one from the entity schema: any other spelling returns 400 INVALID_PARAMS with the list of available fields.
curl -X POST https://vibecode.bitrix24.com/v1/deals/aggregate \
-H "X-Api-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"aggregate": [
{ "function": "sum", "field": "amount" },
{ "function": "sum", "field": "ufCrmBudget" },
{ "function": "avg", "field": "ufCrmScore" }
],
"groupBy": "ufCrmPriority"
}'
UF support rules:
| Function | Accepts UF types |
|---|---|
sum, avg, min, max |
only integer, double, money |
groupBy |
any UF type (string, enumeration, date, integer, …) |
money fields are stored in Bitrix24 as the string "amount|currency" (for example "1500.50|USD"). The aggregate extracts the numeric part before | — all arithmetic operations are correct.
Errors:
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_PARAMS |
UF type not in the integer/double/money list for sum/avg/min/max — the actual type is given in message |
| 400 | INVALID_PARAMS |
Field not found in the schema or the UF cache — the available standard and UF fields are listed in message |
| 400 | INVALID_PARAMS |
groupBy is neither a string nor an array of strings — message gives the actual type of the value, or the index of the non-string array element |
UF field cache: one call to crm.{entity}.fields per portal+entity combination (plus entityTypeId for items). TTL 5 minutes — repeat aggregates within the window trigger no extra requests.
Field schema — `GET /v1/{entity}/fields`
Get the description of all entity fields with their types.
curl -H "X-Api-Key: $KEY" \
"https://vibecode.bitrix24.com/v1/deals/fields"
Each field's metadata in the response carries the display name label and a description of the field's purpose, where those are defined. You do not need to look up field names in a separate reference — they arrive with the schema.
Labels and descriptions come in English. Request headers do not switch the language. Names of fields the platform takes straight from the Bitrix24 account, user fields included, come in the account's language.
Batch operations — `POST /v1/{entity}/batch`
Bulk create, update, or delete records of a single entity.
{ "action": "create", "items": [{ "title": "Deal 1" }, { "title": "Deal 2" }] }
The list action takes filter, select and limit, as the single list does, and the selection is applied to the response: only the listed fields plus id stay in the records. An unknown name arrives as an UNKNOWN_SELECT_FIELD warning in the meta of that sub-call — neighboring sub-calls get no meta of their own. On entities whose field set is verified against Bitrix24, such a name rejects the sub-call with an UNKNOWN_SELECT_FIELD error instead of producing a warning: only that sub-call fails, its neighbors in the batch still run. The value * (and UF_*, in any case) means "return every field" — no selection is applied, and an unknown name next to it rejects nothing. Important: the list action does NOT read order — the platform sets the ordering itself, and an order passed in the sub-call is not applied. If you need sorting, use the global POST /v1/batch and pass it as sort: that surface does not translate order into Bitrix24 field names either.
To work with different entities in one request, use the Batch API.
Response format
All endpoints return the same format:
{
"success": true,
"data": [ ... ],
"meta": { "total": 150, "hasMore": true }
}
| Field | Description |
|---|---|
success |
true on successful execution |
data |
Array of records (list/search) or an object (get/create) |
meta.total |
Total number of records matching the filter (for list/search). An optional field: if no count was requested, it is absent from the response — see Paging and record counts |
meta.hasMore |
Whether there are more records to load |
meta.nextAfterId |
The identifier of the last returned record, as a string. Arrives for a list or a search when the sort is strictly id ascending, while meta.hasMore is true. Pass it back as filter[>id]. The entities that support the cursor are listed in Paging and record counts |
meta.pageErrorSample |
Arrives for a list or a search when some pages failed to load: the code and message of the first error. The response then holds a contiguous prefix of the result set, not every record. code is either a Bitrix24 error code or one of Vibecode's own, of which there are three. KEYSET_DISCONTINUITY means Vibecode itself stopped the walk after detecting a gap in the page sequence, and returned a contiguous prefix instead of possible duplicates. PAGE2_COUNT_FAILED — the record count failed (a timeout, a request limit, a Bitrix24 account error). LAZY_COUNT_NO_PROGRESS — the method returned the same records instead of the next ones. meta.total is absent only for the last two: there the count never happened. On every other interruption the number is already counted, and meta.total arrives alongside |
meta.windowErrorSample |
The same for a windowed search: the code and message of the first failed window. Arrives together with meta.windowErrors, the number of failed windows. On a total failure the search returns an error rather than a partial response |
meta.warnings |
An array of warnings about the result set: on list, on search, on reading a record by id, on a field-schema request, on an entity's product rows, and on aggregation. Every entry carries code and message, while field is present only when the warning is tied to a specific field. The LIMIT_ZERO_IGNORED and UNKNOWN_SELECT_FIELD codes are covered in the parameters above. WINDOW_TRUNCATED — a windowed search did not return everything, covered in Incomplete result. OFFSET_BEYOND_FETCHED_PAGE — a page came back empty during a paged walk, covered in Page through manually. AGGREGATE_TRUNCATED — a numeric aggregation was computed over some of the records, covered in the 5000-record ceiling |
The shape above covers the entities of this reference. Other route families place the envelope fields differently:
- The global POST /v1/batch —
results,errors,summary,totals, andmetalive insidedata, split by theidof each call. - Dedicated routes — chats, mail, Feed, knowledge base, calls, workday — carry their own
datashape, described on the pages of those sections.
Paging and record counts
Page by meta.hasMore, not by arithmetic over meta.total. The "there is more" signal is derived from page fullness: a full page means there may be more, a short page means the list has ended. A "read while hasMore" loop always reaches the end. 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, not an error.
When the sort is strictly id ascending, the response also carries meta.nextAfterId — the identifier of the last returned record. Pass it back as filter[>id] and the next page starts right after it. This kind of paging does not depend on an offset and does not get more expensive towards the end of a collection, so for walks of tens of thousands of records it is preferable to a growing offset. A ready-made walk loop — Pagination.
The cursor is available for deals, leads, contacts, companies, quotes, and smart process items. Other entities carry no meta.nextAfterId in the response: there you page with offset and shrink the result set with the filter.
The record count is a separate question, and it is disproportionately expensive on the Bitrix24 side: counting a collection costs markedly more than returning a page from it. So:
- You need the number — ask for it directly:
POST /v1/{entity}/aggregatewith thecountfunction returns the count in one call, without fetching any records. - You do not need the number — remove it from the response:
withTotal=falseon the list request, or thetotalDefaultsetting on the API key if that is how the whole integration works. The cases wheremeta.totalstill arrives anyway are described later in this section. Important: This reduces load only on a single-page call (limitof 50 or less): there the count really is not requested. On a multi-page call (limitabove 50) the platform needs the count to plan the walk, so the parameter removes the number, not the cost. There is no point in passing it there to save anything: the call does not get cheaper, and the exact number a short first page hands over for free is discarded. - Do not emulate a counter by walking. Paging through a collection just to count its rows is dozens or hundreds of calls instead of one, and the most expensive way to learn a single number. That is what
aggregatewithcountis for.
When no count was requested, meta.total is usually absent from the response — but not always. If the page came back shorter than the requested limit, the count is known from the page itself, and the exact number still arrives. The full picture:
| What the request looked like | meta.total in the response |
|---|---|
The count was requested — by default or with withTotal=true |
arrives |
The page is empty, offset is above zero |
absent — there is nothing to vouch for a count |
withTotal=false was passed |
never arrives |
The count is off via the key setting or the platform default, offset = 0, page shorter than limit |
arrives, exact number — including 0 |
The count is off via the key setting or the platform default, and the page is full or offset is above zero |
absent |
The table describes a call on which the count can be skipped. Where it cannot be skipped, withTotal=false is simply ignored and meta.total arrives as before. So check whether the field is present in a given response instead of deriving it from your settings.
In short: when no count was requested, total arrives only on a call with offset = 0, and only when the page came back shorter than requested. When a count was requested — by default or with an explicit withTotal=true — it always arrives. Note the difference between the parameter and the setting: an explicit withTotal=false in the request also removes the exact number derived from a short page, while the totalDefault setting on the key does not. That is why two identical requests from two different keys can return responses of different shape.
There are two exceptions to "a count was requested, so it always arrives". The first is an empty page at an offset above zero: nothing there vouches for a count — no returned row stands behind it — and on such a page some Bitrix24 methods put their own cursor in that field, a number that grows with the offset. So on an empty page past offset zero meta.total does not arrive; meta.hasMore is false there, and the paging loop ends as usual. The second is a partial response in which the count itself failed. If the response carries meta.pageErrorSample with the code PAGE2_COUNT_FAILED or LAZY_COUNT_NO_PROGRESS, meta.total is absent: the count never happened, and the number of returned rows is not a substitute for it. On every other interruption — KEYSET_DISCONTINUITY and any Bitrix24 code — the count happened before the walk broke off, and meta.total arrives next to meta.pageErrorSample. Either response arrives with a 200, holds a contiguous prefix of the result set, and has meta.hasMore equal to true.
The count itself only happens when it cannot be avoided. That does not change the shape of the response: on a multi-page call (limit above 50) meta.total arrives exactly as it used to — sometimes the number simply turns out to be free.
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 data does not depend on meta.total — meta.hasMore and meta.nextAfterId are what guarantee it.
The meta.total default in force for your key is shown by the totalDefault block in GET /v1/me: key is the key setting, platform is the platform default, and effective is what applies when a request passes no withTotal.
Field transformation
Vibecode automatically transforms field names when sending a request. Always use camelCase in requests:
{ "title": "Deal", "stageId": "NEW", "assignedById": 1 }
In responses the declared fields of an entity arrive in camelCase — id, title, stageId, responsibleId.
User fields of a Bitrix24 account are a separate case. Which fields exist depends on the account, they are not described in advance, and some fields carry a name generated by Bitrix24 itself. The GET /v1/{entity}/fields schema gives the name under which such a field is accepted in a request and returned in a response — the spelling differs between entities. Names and value formats are collected in User fields (UF).
It is not only the letter case that differs — for some fields the name itself differs. For example, the deal amount is called amount in Vibecode, while the original name of this field in Bitrix24 is opportunity. The Vibecode name is shown in the Field column of the GET /v1/{entity}/fields schema, and the original Bitrix24 name is in the Bitrix24 column. In requests and when reading the response, use the name from the Field column — the field is returned under it, and the original Bitrix24 name is not returned. The mapping for each entity is on its fields page, for example Deal fields.
Time zone on writes
A datetime written without a zone (2026-07-15T13:00:00) is read by Bitrix24 in the time zone of the account the platform writes through — not in yours. A client in Berlin who sent 13:00 ended up with 13:00 UTC stored instead of 11:00: two hours off in summer, one in winter.
To avoid that, declare your own zone in a request header:
X-Vibe-Timezone: Europe/Berlin
The value is an IANA zone name. In a browser it comes from Intl.DateTimeFormat().resolvedOptions().timeZone; a server-side integration supplies the zone in which it keeps its data.
The header is optional and breaks nothing. Without it, with an unknown zone name, or with garbage instead of a name, behavior stays as before — the value goes to the account as is. There is no rejection: the header was added so that no existing call would silently change behavior.
Only a YYYY-MM-DDTHH:MM value that states no zone receives the offset, and only in the fields the header applies to. Seconds and fractional seconds are optional and do not change that; the list of fields is below. Every other datetime form reaches the account unchanged, and the header has no effect on it:
| What you send | What reaches the account |
|---|---|
2026-09-15T13:00:00 |
2026-09-15T13:00:00+02:00 — offset applied |
2026-09-15T13:00, 2026-09-15T13:00:00.500 |
offset applied the same way — with no seconds and with fractional seconds |
2026-09-15T13:00:00Z or with an offset |
unchanged — you declared the zone yourself, the platform does not rewrite it |
2026-09-15 13:00:00 with a space |
unchanged — read in the account's time zone |
15.09.2026 13:00 in local form |
unchanged — read in the account's time zone |
The last two forms cause no rejection: the response is successful while the stored time differs by the gap between your zone and the account's. With the Europe/Berlin header and an account in UTC, a write of 2026-09-15T13:00:00 is stored as 2026-09-15T11:00:00Z, and the same 13:00 with a space as 2026-09-15T13:00:00Z.
Daylight saving is handled per value. The offset applied is the one in force in that zone on the date inside the value, not at the moment of the request. One integration writing a July date and a January date in the same request gets both right.
The zone is not applied to every field, only to verified ones. Bitrix24 declares some fields as datetime while storing a plain date in them — an applied offset would shift the day on such a field. So the list grows as each field is verified live. Today it holds the task fields deadline, startDatePlan and endDatePlan, plus the service fields createdDate, changedDate and closedDate — there a value like 2019-05-15T13:47:00 gets the offset of your zone instead of being read in the key owner's zone. Where the header takes effect is visible in the machine-readable API description — it is declared among the operation's parameters.
Calendar events stand apart: they carry the zone in their own request parameters (timezoneFrom / timezoneTo), so the header is ignored for them — otherwise the event would shift twice.
The header affects writes only. It does not apply to filters — filter values are read in the account's time zone regardless of the header. What that means for date searches, and how to convert the boundaries — Time zone in a filter value.
Multi-value fields (email, phone, web)
The contact fields email, phone, and web hold multiple values, each with a type. The set depends on the entity — email and phone exist on contacts, companies, and leads, web only on companies. The format differs on input and on output.
On input — an array of objects [{ "value": "contact@example.com", "typeId": "WORK" }]. A single string "contact@example.com" and an array of strings are also accepted. The typeId depends on the field — WORK / HOME / MOBILE / OTHER for phone, WORK / HOME / MAILING / OTHER for email. Default WORK.
{ "email": [{ "value": "contact@example.com", "typeId": "WORK" }] }
The upper-case form with VALUE and VALUE_TYPE keys is not accepted — 400 INVALID_MULTIFIELD_SHAPE. Use camelCase value and typeId.
On output — the primary value comes as a string ("email": "contact@example.com"), and the full list of values with types is in the fm array. Per-type values are available in dedicated fields (emailWork, phoneMobile).
The exact set of typeId values and output fields for each entity is on its page, for example Contacts and Companies.
User fields (UF)
User fields of a Bitrix24 account are read and written alongside the declared fields of an entity — there is no separate endpoint for their values. Creating, updating and deleting the fields themselves is covered in User fields.
Field name
There is one working name — the one the field is listed under in the GET /v1/{entity}/fields schema. The field is accepted under that name in the request body, in filter and in select, and it comes back under it in the response. The spelling differs between entities and cannot be derived from the name given at creation time:
| Entity | Example names in the schema |
|---|---|
| Deals, leads, contacts, companies, quotes | ufCrmProjectCode, ufCrm_1729594209 |
| Smart process items | ufCrm3_1628508847 |
| Requisites | UF_CRM_1698325419 |
| Employees | UF_USR_1619099890455, UF_PHONE_INNER |
The list of field definitions and the entity schema call the same field differently. A field created on deals as fieldName: "PROJECT_CODE" is listed in the GET /v1/userfields/deals response as UF_CRM_PROJECT_CODE, while the deal schema and every request use ufCrmProjectCode.
A name that is missing from the schema does not raise an error on write. On deals, leads, contacts, companies, quotes and smart process items a value sent under UF_CRM_PROJECT_CODE instead of ufCrmProjectCode is not stored: the response arrives with code 200 or 201 while the field stays empty. In select such a name adds no field to the response, and in aggregation it returns 400 INVALID_PARAMS with the list of available fields. Filter behavior is covered in An unknown filter field name.
Value format by type
The field type is set at creation time through the userTypeId parameter and comes back in the entity schema in the type field.
The examples below were captured on a Bitrix24 account in the UTC time zone.
| Type | What to send | What comes back |
|---|---|---|
string |
"Contract No. 17" |
The same string |
integer |
42 or "15" |
A number. A fractional value is truncated to an integer — 3.7 is stored as 3 |
double |
3.14 or "7.25" |
A number rounded to the number of decimal places set by this field's PRECISION setting. With PRECISION equal to 2 the value 3.14159 comes back as 3.14. A field created without settings gets PRECISION equal to 0 and stores integers: 3.14 comes back as 3, and 3.99 as 4 |
boolean |
true, false, "Y", "N". The values 1, 0, "1" and "0" are stored as the negative value — false on deals, "N" elsewhere |
On deals — true or false. On contacts, leads, companies, quotes and smart process items — the strings "Y" and "N" |
enumeration |
The identifier of the option — "3821". The options of the field are listed in the items array of the entity schema |
The identifier as a number — 3821. The option text sent instead of the identifier is stored as 0 |
datetime |
"2026-08-10T12:30:00" — the time is read in the account's time zone. The offset can be stated explicitly |
On deals — the moment in UTC, "2026-08-10T12:30:00.000Z". On contacts, leads, companies, quotes and smart process items — the same moment with the account offset, "2026-08-10T12:30:00+00:00" |
date |
"2026-08-10" |
A full moment in time with the account offset — "2026-08-10T00:00:00+00:00" |
money |
"100|USD" — the amount and the currency code separated by a vertical bar. A value without a currency code is stored in the account's base currency |
"100|USD". A submitted 100 comes back as "100|USD", and "250.50|USD" as "250.5|USD" |
url |
"https://example.com/contract" |
The same string |
address |
"1 Market St, San Francisco|37.7936;-122.3965" — a vertical bar separates the address from the coordinates, and a ; separates latitude from longitude |
The same string. Latitude and longitude separated by a vertical bar instead of ; are not stored |
employee |
1 — the employee identifier, list: GET /v1/users |
1 |
crm |
"L_1000739" — the letter prefix of the record type followed by its identifier: L_ lead, C_ contact |
The same string |
crm_status |
"NEW" — the statusId value, list: GET /v1/statuses |
The same string |
file |
["contract.pdf", "BASE64_CONTENT"] — the file name and its Base64 content |
For a single-value field — an object with the fields id, url, urlMachine, for a multiple one — an array of such objects. Details — Files in CRM |
The PRECISION setting is specified when the field is created — "settings": { "PRECISION": 2 }, see Create a field.
The X-Vibe-Timezone header from Time zone on writes has no effect on user fields: a value without an offset is read in the account's time zone.
Values of the employee, crm and crm_status types are stored without any check against the account's reference data: the identifier of a non-existent employee or a value outside the reference list is accepted and returned as is. Validate them on your side.
Several values in one field
Whether a field holds several values is reported by the list of field definitions — the multiple field with the value Y or N. For deals, leads, contacts, companies, quotes and requisites that list is GET /v1/userfields/:entity, for smart processes — GET /v1/items/:entityTypeId/userfields. For a user field the GET /v1/{entity}/fields schema does not carry this flag.
The value of a multiple field is sent as an array and replaces the whole previous set:
{ "ufCrmProjectTags": ["first", "second"] }
A single value instead of an array is not stored. An empty array and null leave the previous values in place — a multiple field cannot be cleared with an empty array or null.
Clearing a value
A single-value field is cleared with null or an empty string, and a single-value file field — with an empty array. A cleared field comes back as null in the response. None of these clears a multiple field.
Special entities
Smart Processes (Items)
Smart processes use entityTypeId in the URL: GET /v1/items/{entityTypeId}.
# List of smart process records with entityTypeId = 1058
curl -H "X-Api-Key: $KEY" \
"https://vibecode.bitrix24.com/v1/items/1058?limit=10"
List of available smart processes: GET /v1/smart-processes.
Calendar Events
Require two parameters: type, with the value user, group, or company, and ownerId.
curl -H "X-Api-Key: $KEY" \
"https://vibecode.bitrix24.com/v1/calendar-events?type=user&ownerId=1"
Files
Require folderId. Obtain it from GET /v1/storages → the rootFolderId field.
Limits
10 requests/second — the Bitrix24 account-level limit, shared across all keys.
How to stay within the limit:
- Batch API — 50 calls per 1 limit unit
POST /v1/{entity}/batch— up to 500 CRUD records for 10 units (not 500)POST /v1/{entity}/aggregate—countruns as a single fast call;sum/avg/min/maxload up to 5000 records and are computed on the Vibecode side- The
selectparameter — loading only the needed fields reduces the response size
More: Optimization — patterns for dashboards, bulk operations, scanning large data volumes
Writing with a read-only key
A key in read-only access mode performs reads and receives 403 WRITE_BLOCKED_READONLY_KEY on any write call — create, update, delete, or an action on an entity. The full description of the code and the details fields — Error codes. How to switch the mode and how the Bitrix24 account policy works — Access mode.
Usage patterns
| Task | Approach |
|---|---|
| Dashboard / analytics | POST /v1/{entity}/aggregate — counters and sums by filter |
| Bulk update | POST /v1/{entity}/batch with action=update |
| Data from several entities | POST /v1/batch — deals, tasks, and contacts in one request |
| Record + related data | ?include=company,contact — related data in one response |
| Scanning tens of thousands of records | An id cursor — order[id]=asc plus filter[>id] from meta.nextAfterId, see Paging and record counts. Where there is no cursor — POST /v1/{entity}/search with limit up to 5000 and a narrower filter |
See also
- Related data (include) — loading related entities in one request
- Filtering — three filter syntaxes, dates, NOT filters
- Batch API — up to 50 calls in one request
- API reference — full list of entities with links
- Optimization — rate limits, performance patterns
- Error codes — platform error reference