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.com Authorization: the X-Api-Key: your_key header vibe_app_… keys additionally require an Authorization: Bearer header with a session token on every entity request. For unattended jobs (cron, schedulers) with no user present, use a vibe_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_FOUNDError codes.

List — `GET /v1/{entity}`

Fetch records with pagination, sorting, and field selection.

Terminal
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}`

Terminal
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}`

Terminal
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}`

Terminal
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_BODYError codes.

Delete — `DELETE /v1/{entity}/{id}`

Terminal
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

Terminal
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.

Terminal
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: count is computed with a single fast call to Bitrix24. For sum/avg/min/max the platform loads the records matching the filter, at most 5000, and computes the aggregates on the Vibecode side. If fewer records are read than data.count promised, the response is flagged meta.truncated: true, and the ceiling is only one of the reasons for that flag, with the size of the gap reported in data.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/max for the UF types integer, double, money. groupBy accepts 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. Reserved entityTypeId values (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, carries meta.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 than data.count promised, and the size of the gap is reported in data.meta.recordsShortfall.
  • Once it is on — the response is 422 AGGREGATION_LIMIT_EXCEEDED and 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:

JSON
{
  "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.

Terminal
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.

Terminal
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.

JSON
{ "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:

JSON
{
  "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/batchresults, errors, summary, totals, and meta live inside data, split by the id of each call.
  • Dedicated routes — chats, mail, Feed, knowledge base, calls, workday — carry their own data shape, 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}/aggregate with the count function returns the count in one call, without fetching any records.
  • You do not need the number — remove it from the response: withTotal=false on the list request, or the totalDefault setting on the API key if that is how the whole integration works. The cases where meta.total still arrives anyway are described later in this section. Important: This reduces load only on a single-page call (limit of 50 or less): there the count really is not requested. On a multi-page call (limit above 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 aggregate with count is 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.totalmeta.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:

JSON
{ "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.

JSON
{ "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:

JSON
{ "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}.

Terminal
# 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.

Terminal
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}/aggregatecount runs as a single fast call; sum/avg/min/max load up to 5000 records and are computed on the Vibecode side
  • The select parameter — 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/batchdeals, tasks, and contacts in one request
Record + related data ?include=company,contactrelated 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