# 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](./keys-auth.md).

## 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](/docs/api-reference) and by the `operations` field in the [`GET /v1/guide`](/docs/keys-auth/guide) response. A request to an operation the entity does not have returns `404 ROUTE_NOT_FOUND` — [Error codes](./errors.md).

### List — `GET /v1/{entity}`

Fetch records with pagination, sorting, and field selection.

```bash
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, catalog SKUs, catalog offers and catalog services, 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](#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}`

```bash
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 adds an `UNKNOWN_SELECT_FIELD` warning to `meta.warnings`. A user field is referenced by the name from the schema — [User fields (UF)](#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}`

```bash
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](./errors.md).

### Update — `PATCH /v1/{entity}/{id}`

```bash
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](./errors.md).

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

```bash
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](./filtering.md)

```bash
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](./filtering.md)) |
| `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](./filtering.md#pagination). |
| `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](#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](/docs/api-reference). For entities without aggregation the record count arrives in `meta.total` of the list response.

```bash
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 return `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` normally comes from one Bitrix24 probe; if the method omits a usable `total`, the platform recovers it by paging. 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` — the ceiling is only one of the reasons for that flag — and the size of the gap is reported in `data.meta.recordsShortfall`. Full details — [Aggregation POST — the 5000-record ceiling](#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](#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` does not load rows for numeric aggregation. Bitrix24 normally supplies an exact `total` in one probe; when a method omits a usable `total`, the platform walks count pages up to 5000 rows.

If that count walk stops on a failed page, a malformed page, or a page longer than 50 rows, the response remains `200`, but `data.truncated` and `data.meta.truncated` are `true`. `data.count` and `data.meta.totalRecords` are then lower bounds from completed pages; if the first page fails, the successful probe rows provide the bound. `data.meta.pageErrorSample` contains only the safe `AGGREGATE_COUNT_PAGE_FAILED` code and an offset. After 100 full pages without proof of an end, `data.meta.countWalkLimit: 5000` appears; the ceiling alone creates no `pageErrorSample`, but a later record-page failure can add one. In both cases `data.meta.warnings` contains `AGGREGATE_TRUNCATED` with count-specific text. These fields tied to an incomplete count are absent after a complete count; `meta.truncated` is always present and then reads `false` unless the separate record fetch was truncated. An ordinary upstream `total` or a `paramWrapper` method does not start this walk.

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 funnel 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` are exact after a complete probe or recovery walk; an interrupted count walk makes both lower bounds.

**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. `data.count` and `meta.totalRecords` are exact when the count probe or recovery walk completes. When that walk is interrupted, both are lower bounds, and `data.truncated` and `meta.truncated` are true. 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. Check `meta.truncated`: without a usable `total`, even a plain `count` can require a bounded page walk.

**Mixed currencies.** A separate `AGGREGATE_MIXED_CURRENCIES` entry in `meta.warnings` appears independently of truncation when `sum`, `avg`, `min` or `max` reads a field stored in the record's own currency: `deals.amount` and `deals.taxValue`, `leads.opportunity` and `leads.amount`, `quotes.amount`, `invoices.opportunity`, `companies.revenue`. Three conditions hold together: the records read span more than one currency, `groupBy` names no currency field of the entity (`currency` for deals and quotes, `currencyId` for the rest), and the function is numeric. The values are not converted, so the resulting number adds different currencies. The Bitrix24 accounting-currency fields (`deals.receivedAmount`, `deals.lostAmount`) never raise it — they are already expressed in one currency.

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 stage-count mode that returns 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` with a scalar `categoryId` or over `stageSemanticId`, 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](./entities/deals/aggregate.md).

### 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 enabled 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](./entities/activities/aggregate.md).

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

```bash
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` | The body has `op`, `field` or `function` at the top level instead of an `aggregate` array. `message` shows an example of the correct array |
| 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 `account+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.

```bash
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`, `sort`, `order`, `limit` and `offset`, 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_*`, regardless of case) means "return every field" — no selection is applied, and an unknown name next to it rejects nothing. Both sort spellings are translated into Bitrix24 field names. When both parameters are sent, `sort` sets the order and `order` is still validated. For `users`, the only supported ordinary sort field is `id`, while `UF_*` is passed as written only with `limit ≤ 50` and `offset = 0`. Multiple recognized `users` fields, as well as a `UF_*` sort with `limit > 50` or `offset > 0`, are replaced in full with ascending `id` order: the sub-call succeeds and its `meta.warnings` carries `SORT_FALLBACK_TO_ID`. A single ordinary field other than `id` returns `UNKNOWN_SORT_FIELD`. An invalid direction rejects the sub-call for any entity, and an unknown field does so for entities whose sort-name validator is active (see `UNKNOWN_SORT_FIELD` in [error codes](./errors.md)); on other entities the name goes to Bitrix24 as written. Entities that do not support sorting in a sub-call reject it too: `departments`, `telephony-lines`, `calendar-resources`, `calendar-events` and `doc-templates`. Only that sub-call is rejected, and its neighbors still run. Document templates are sorted by the single `GET /v1/doc-templates?sort=…`, and calendar events come back ordered by start time.

A sub-call `offset` counts records: `{ "limit": 50, "offset": 50 }` returns the second fifty. The sub-call result carries `hasMore` next to `total` — end an `offset += limit` loop on that flag rather than on the page length. If the window comes out empty while records remain (some Bitrix24 methods filter after paginating), an `OFFSET_BEYOND_FETCHED_PAGE` warning arrives in the `meta` of that sub-call. For the few entities whose Bitrix24 method cannot honor an offset, the sub-call answers with an `UNSUPPORTED_OFFSET` error, and its neighbors still run.

To work with **different entities** in one request, use the [Batch API](./batch.md).

## Legacy GET aggregate

**Coverage:** this GET is mounted only for entities that do not pass the type in the path, and `{entity}` below means such an entity (for example `deals`). Entities with a path parameter — smart processes (`items`) and categories (`categories`) — cannot receive `entityTypeId` through a query string, so the GET route is not registered for them at all: a request like `GET /v1/items/aggregate?op=sum&field=amount` never reaches the aggregation handler and returns none of the response described below. The only aggregation address for smart processes is `POST /v1/items/:entityTypeId/aggregate`.

Legacy GET /v1/{entity}/aggregate?op=sum&field=amount is kept for compatibility; use POST for new callers. On GET, data.meta always contains recordsProcessed and truncated. A partial answer also carries data.truncated, groups[].truncated and total.truncated where those objects exist, plus meta.warnings with AGGREGATE_TRUNCATED. Known totalRecords and positive recordsShortfall are included; pageErrorSample contains only sanitized code/message when a page failed. Unknown totalRecords and recordsShortfall are omitted: an explicit paginator truncation signal remains true without either a measurable gap or a page error, even if a reported total equals the fetched count. GET count uses fetched rows and can be partial; POST normally obtains a complete total, but its missing-total recovery can also be marked incomplete. The previous GET limit of 10000 records still applies: exceeding it returns 422 AGGREGATION_LIMIT_EXCEEDED.

Example data for two fetched rows with an unknown overall count:

<!-- legacy-get-aggregate-example -->
```json
{
  "op": "sum",
  "field": "amount",
  "value": 100,
  "count": 2,
  "truncated": true,
  "meta": {
    "recordsProcessed": 2,
    "truncated": true,
    "warnings": [
      {
        "code": "AGGREGATE_TRUNCATED",
        "message": "This aggregate was computed over only the records in data.meta.recordsProcessed. Values and group counts describe a partial sample, not the full matching collection. Narrow the filter before treating the result as complete."
      }
    ]
  }
}
```

## 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](#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](#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: on list, on search, on reading a record by `id`, on a field-schema request, on an entity's product rows, on aggregation, and on a single-record write (`POST`, `PATCH`). 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](./optimization.md#incomplete-result). `OFFSET_BEYOND_FETCHED_PAGE` — a page came back empty during a paged walk, covered in [Page through manually](./filtering.md#page-through-manually). `AGGREGATE_TRUNCATED` — a numeric aggregation used only some records, or a count-page walk stopped and `count` became a lower bound, covered in [the 5000-record ceiling](#aggregation-post-the-5000-record-ceiling). `UNRECOGNIZED_WRITE_FIELD` — on a single-record `POST` or `PATCH` a scalar body key was recognized neither by the entity description nor by your account's field list, so Bitrix24 most likely did not store it — the hint does not compare against the written record; the record was still created or updated, and `field` names the key. Exception: when the schema does not declare `fields` and the route wraps the field map itself, an object under `fields` is rejected with `400 NESTED_FIELDS_ENVELOPE` before writing; pass fields at the JSON root. This also rejects `fields: {}` and a body that combines `fields` with flat fields. Unknown scalar fields keep their existing behavior. At most ten hints per response. The usual cause is a typo in a field name. The hint appears only when the account's field list is available — the platform never warns by guesswork. `REREAD_FAILED` — on `PATCH`: Bitrix24 accepted the update, but reading the record back failed; the response is `200`, `data` holds only `id`, and the cause is in `cause`. Read the record yourself before sending anything again: repeating the same `PATCH` may apply it twice. `LEAD_AUTO_CONVERTED` — on `POST /v1/leads`: the Bitrix24 account runs CRM in simple mode (no leads), and Bitrix24 converted the lead right on creation — it came back `CONVERTED` and closed, with a contact and a deal created from it that the request never asked for; the warning carries their ids in `contactId` and `dealId` (`null` when the record was not created or not found; a contact the client bound itself is reused by Bitrix24 and does not count as created). The lead is created and the response is `201`. It does not appear when the client asked for `CONVERTED` itself |

The shape above covers the entities of this reference. Other route families place the envelope fields differently:

- **The global [POST /v1/batch](./batch.md)** — `results`, `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](./filtering.md#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 normally returns a count from one probe; without a usable `total`, a bounded count walk may follow. Check `meta.truncated`.
- **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` 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 list on the client just to count rows is usually costlier than `aggregate` with `count`. If the Bitrix24 method omits `total`, aggregation itself may make up to 100 count-page calls; then check `meta.truncated`.

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 at an offset above 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](./keys-auth/me.md): `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)](#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](./entities/deals/fields.md).

## 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](./entities/calendar-events.md) 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.** Filters preserve an explicit `Z` or offset and fractional seconds; naive values retain Bitrix24 local-time semantics regardless of the header. Pass the zoned datetime from the response as a boundary, without manual compensation. Details — [Time zone in a filter value](./filtering.md#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. The default is `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`. The raw `fm` is passed only as an array: a string, a number, or an object is rejected with `400 INVALID_MULTIFIELD_SHAPE`, and an object keyed by row ids with `400 MULTIFIELD_ID_NOT_SUPPORTED`.

**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](./entities/contacts.md) and [Companies](./entities/companies.md).

## 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](./userfields.md).

### 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 name 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](./filtering.md#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 a false 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 as a string or a number — `"3821"` or `3821`. This is the `ID` value from the field's `items` array in the entity schema. The same identifier arrives in the `list` array of the `GET /v1/userfields/:entity/:id` response, and for smart processes as the `id` field in the `enum` array of the `GET /v1/items/:entityTypeId/userfields/:id` response. The option label `VALUE` is for display only — do not send it in the request. For a multiple field — an array of identifiers, `["3821", "3823"]` | The identifier as a number — `3821`. On deals, leads, contacts, companies, quotes, requisites and smart process items, an option label sent instead of the identifier is stored as `0`, and in the array of a multiple field it is stored as `0` or dropped from the set. A number that is not among the field's options is stored as is. Neither the label nor a non-existent identifier raises an error: the response arrives with code `200` or `201` and no warning |
| `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](./recipes/crm-files.md) |

The `PRECISION` setting is specified when the field is created — `"settings": { "PRECISION": 2 }`, see [Create field](./userfields/crm/create.md).

The `X-Vibe-Timezone` header from [Time zone on writes](#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

In the `GET /v1/{entity}/fields` schema, `multiple: true` on a field means the field accepts several values. Without the flag, the field may still hold several values: the platform does not set it on declared fields, or on fields for which the Bitrix24 account does not report multiplicity. The list of field definitions gives the definitive answer — 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 employees — `GET /v1/userfields/users`.

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

```bash
# 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`.

```bash
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](./batch.md) — 50 calls per 1 limit unit
- `POST /v1/{entity}/batch` — up to 500 CRUD records for 10 units (not 500)
- `POST /v1/{entity}/aggregate` — `count` normally uses one probe, but may walk up to 5000 count rows without a usable `total`; `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](./optimization.md) — 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](./errors.md). How to switch the mode and how the Bitrix24 account policy works — [Access mode](./keys-auth/access-mode.md).

## 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](./includes.md) 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](#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)](./includes.md) — loading related entities in one request
- [Filtering](./filtering.md) — three filter syntaxes, dates, NOT filters
- [Batch API](./batch.md) — up to 50 calls in one request
- [API reference](./api-reference.md) — full list of entities with links
- [Optimization](./optimization.md) — rate limits, performance patterns
- [Error codes](./errors.md) — platform error reference
