# Обзор API

Вайбкод предоставляет единый REST-интерфейс для работы с сущностями Битрикс24: CRM, задачи, пользователи, календарь, диск, каталог, документооборот и другие. Формат запроса, ответа и ошибок у всех сущностей одинаковый.

> Базовый URL: `https://vibecode.bitrix24.tech`
> Авторизация: заголовок `X-Api-Key: ваш_ключ`
> Ключи `vibe_app_…` дополнительно требуют заголовок `Authorization: Bearer` с токеном сессии на каждый запрос к сущностям. Для фоновых задач по расписанию без пользователя у экрана берите ключ `vibe_api_…`. Подробнее — [Создание и использование ключа](./keys-auth.md).

## Доступные операции

Ниже описан стандартный набор операций. Путь строится по шаблону `/v1/{entity}`.

Набор операций у каждой сущности свой: у части сущностей отдельные операции недоступны. Какие операции доступны сущности, показывают её страница в [Справочнике API](/docs/api-reference) и поле `operations` в ответе [`GET /v1/guide`](/docs/keys-auth/guide). Запрос к операции, которой у сущности нет, отвечает `404 ROUTE_NOT_FOUND` — [Коды ошибок](./errors.md).

### Список — `GET /v1/{entity}`

Получить записи с пагинацией, сортировкой и выборкой полей.

```bash
curl -H "X-Api-Key: $KEY" \
  "https://vibecode.bitrix24.tech/v1/deals?limit=100&offset=0"
```

Параметры:

| Параметр | Тип | Описание |
|----------|-----|---------|
| `limit` | number | Количество записей (по умолчанию 50, максимум 5000). Значение `0` размером страницы не является: параметр отбрасывается, применяется значение по умолчанию, а в ответ добавляется предупреждение `LIMIT_ZERO_IGNORED` в `meta.warnings` |
| `offset` | number | Пропустить N записей |
| `select` | string[] | Выборка полей: `?select=id,title,amount`. Принимаются канонические имена из `GET /v1/{entity}/fields`, исходные имена Битрикс24 у объявленных полей и взаимозаменяемые имена дат `updatedAt` / `updatedTime`, `createdAt` / `createdTime`. Незнакомое имя отклоняется ошибкой `400 UNKNOWN_SELECT_FIELD` на сущностях, чей набор полей выверен по Битрикс24 — сделки, контакты, компании, лиды, предложения, дела, адреса, смарт-процессы, товары и разделы товаров, каталоги и их товары и разделы, заказы и статусы заказов, статусы, валюты, события и разделы календаря, файлы, папки, хранилища, подразделения, рабочие группы, шаблоны документов. На остальных оно даёт предупреждение `UNKNOWN_SELECT_FIELD` в `meta.warnings`. Пользовательские поля (`UF_*`, `ufCrm*`) не отклоняются нигде, а свойства товаров принимаются каждое на СВОЕЙ сущности: `PROPERTY_295` — на товарах, `property295` — на товарах каталога, где их номера назначает портал. Чужое написание метод не понимает, и его гейт отклоняет. На списке и поиске реквизитов и банковских реквизитов имя уходит дальше в Битрикс24, и там аккаунт, не знающий такого поля, отклоняет весь вызов с `422 BITRIX_ERROR`. Значение `*` (и `UF_*`, в любом регистре) означает «вернуть все поля» — отбор не применяется, а незнакомое имя рядом с ним даёт предупреждение вместо ошибки |
| `order` | object | Сортировка: `?order[createdAt]=desc` |
| `withTotal` | string | Нужно ли количество: `true` или `false`, ровно эти два значения. `false` — единственный способ гарантированно убрать `meta.total` из ответа. При `limit` не больше 50 он заодно отменяет подсчёт, при `limit` больше 50 — убирает число, а не подсчёт. Без параметра значение берётся из настройки ключа, затем из платформенного умолчания, и тогда на короткой странице точное количество приходит и без заказа — см. [Листание и количество записей](#листание-и-количество-записей) |

**Авто-пагинация:** при `limit > 50` Вайбкод автоматически запрашивает несколько страниц у Битрикс24 и возвращает все записи в одном ответе.

### Получить по ID — `GET /v1/{entity}/{id}`

```bash
curl -H "X-Api-Key: $KEY" \
  "https://vibecode.bitrix24.tech/v1/deals/123"
```

Ответ: `{ success: true, data: { id: 123, title: "...", ... } }`

Параметр `select` работает и при получении записи по `id` — ответ содержит только перечисленные поля. Поддерживаются три формы: список через запятую `?select=id,title,stageId`, повторяющийся ключ `?select[]=id&select[]=title` и индексируемый `?select[0]=id&select[1]=title`. Поле `id` возвращается всегда. Незнакомое имя поля отвечает ошибкой `400 UNKNOWN_SELECT_FIELD` на сущностях со сверенным набором полей (список — в таблице параметров выше), а на остальных дополняет ответ предупреждением `UNKNOWN_SELECT_FIELD` в `meta.warnings`. Пользовательское поле указывается именем из схемы — [Пользовательские поля (UF)](#пользовательские-поля-uf). Значение `*` (и `UF_*`) — привычная для Битрикс24 запись «вернуть все поля»: отбор не применяется, приходит полная запись. Параметр совместим с `include` — связанные данные подгружаются до отбора полей.

### Создать — `POST /v1/{entity}`

```bash
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  "https://vibecode.bitrix24.tech/v1/deals" \
  -d '{"title": "Новая сделка", "stageId": "NEW", "amount": 150000}'
```

Ответ: `{ success: true, data: { id: 456, ... } }` (HTTP 201)

Тело без единого поля возвращает `400 EMPTY_CREATE_BODY`, а у сущностей с обязательными полями создания — `400 MISSING_REQUIRED_FIELDS` с именем первого недостающего поля. Оба кода — [Коды ошибок](./errors.md).

### Обновить — `PATCH /v1/{entity}/{id}`

```bash
curl -X PATCH -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  "https://vibecode.bitrix24.tech/v1/deals/123" \
  -d '{"stageId": "WON", "amount": 200000}'
```

Тело без единого поля возвращает `400 EMPTY_UPDATE_BODY` — [Коды ошибок](./errors.md).

### Удалить — `DELETE /v1/{entity}/{id}`

```bash
curl -X DELETE -H "X-Api-Key: $KEY" \
  "https://vibecode.bitrix24.tech/v1/deals/123"
```

### Поиск — `POST /v1/{entity}/search`

Поиск с фильтрацией. Подробнее о синтаксисе фильтров: [Фильтрация](./filtering.md)

```bash
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
  "https://vibecode.bitrix24.tech/v1/deals/search" \
  -d '{"filter": {"stageId": "NEW", "amount": {"$gte": 100000}}, "sort": {"createdAt": "desc"}, "limit": 200}'
```

Параметры:

| Параметр | Тип | Описание |
|----------|-----|---------|
| `filter` | object | Условия фильтрации ([три синтаксиса](./filtering.md)) |
| `sort` | string \| object \| array | Сортировка. Поддерживаются: `"id"` / `"-amount"` / `"id,-createdAt"` (string), `{ id: "asc", amount: "desc" }` или `{ id: 1, amount: -1 }` (object), `["id", "-amount"]` (array). Некорректный тип → `400 INVALID_SORT_TYPE`. Подробнее про пагинацию — см. [filtering.md](./filtering.md#постраничный-вывод). |
| `limit` | number | Количество записей (по умолчанию 50, максимум 5000, авто-пагинация при > 50). Значение `0` отбрасывается — см. предупреждение `LIMIT_ZERO_IGNORED` |
| `offset` | number | Пропустить N записей |
| `autoWindow` | boolean | `false` — отключить разбивку по дате для больших выборок |
| `withTotal` | boolean | Нужно ли количество. `false` — единственный способ гарантированно убрать `meta.total` из ответа. При `limit` не больше 50 он заодно отменяет подсчёт, при `limit` больше 50 — убирает число, а не подсчёт. Без поля значение берётся из настройки ключа, затем из платформенного умолчания, и тогда на короткой странице точное количество приходит и без заказа — см. [Листание и количество записей](#листание-и-количество-записей) |

**Поиск по временным окнам:** для больших наборов данных поиск автоматически разбивает запрос на окна по дате. Если это вызывает таймауты — отключите через `autoWindow: false`.

### Агрегация — `POST /v1/{entity}/aggregate`

Подсчёт количества и числовые агрегации (`sum`, `avg`, `min`, `max`) с фильтрацией. Набор операций каждой сущности перечислен на её странице в [Справочнике API](/docs/api-reference). У сущностей без агрегации количество записей приходит в `meta.total` ответа списка.

```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.tech/v1/deals/aggregate"
```

Параметры (body):

| Параметр | Тип | Описание |
|----------|-----|---------|
| `aggregate` | array | Массив агрегаций: `{ "field": "amount", "function": "sum" }`. Функции: `count`, `sum`, `avg`, `min`, `max`. Без массива — только `count` |
| `filter` | object | Фильтрация по полям сущности |
| `groupBy` | string \| string[] | Поле или массив полей для группировки (максимум 5). Допустимые значения — из списка `aggregatable` конкретной сущности. У сущностей без этого списка (например, `statuses`, `currencies`, `deal-categories`) группировка не поддерживается, доступен `count` с фильтром |

> **Как это работает:** `count` считается одним быстрым вызовом в Битрикс24. Для `sum`/`avg`/`min`/`max` платформа подгружает записи под фильтр, максимум 5000, и считает на стороне Вайбкод. Прочитано меньше, чем обещал `data.count`, — ответ помечается `meta.truncated: true`, и потолок лишь одна из причин такой пометки, а размер нехватки приходит в `data.meta.recordsShortfall`. Полный разбор — [Агрегация POST — потолок 5000 записей](#агрегация-post-потолок-5000-записей). При некорректном поле ответ содержит список доступных.

> **Пользовательские поля (UF)** поддерживаются в `sum`/`avg`/`min`/`max` для UF-типов `integer`, `double`, `money`. `groupBy` принимает UF-поля любого типа. Подробно — в разделе [Агрегация POST — UF-поля](#агрегация-post-uf-поля) ниже.

> **Смарт-процессы (`items`)** передают тип в пути: `POST /v1/items/:entityTypeId/aggregate`. Зарезервированные `entityTypeId` (1, 2, 3, 4, 7, 31) обслуживаются специализированными API сделок, лидов, контактов, компаний, предложений, счетов.

### Агрегация POST — потолок 5000 записей

Потолок действует на запросе, которому для ответа нужны сами записи: это числовые функции и группировка. Простой `count` без `groupBy` под него не подпадает — он считается на стороне Битрикс24 и отвечает на выборке любого размера.

Поведение на выборке шире 5000 записей сейчас раскатывается по аккаунтам, поэтому вариантов два:

- **Пока возможность не включена на аккаунте** — приходит `200`, в ответе `meta.truncated: true`, результат посчитан по первым 5000 записям. Ту же пометку ставит и нехватка ниже потолка: прочитано меньше, чем обещал `data.count`, а её размер лежит в `data.meta.recordsShortfall`.
- **После включения** — приходит `422 AGGREGATION_LIMIT_EXCEEDED`, и ни одна запись не выгружается. В тексте ошибки перечислено, что делать дальше. Смысл замены: на крупной выборке выгрузка первых 5000 всё равно не успевала, запрос обрывался по таймауту, и усечённый ответ до вызывающего чаще всего не доходил вовсе.

Действие в обоих случаях одно: сузить фильтр либо взять `count` без группировки. Клиент, который сегодня ветвится по `meta.truncated`, после включения возможности на его аккаунте начнёт получать `422` — предусмотрите обе ветки.

**`meta.truncated` теперь поднимается ещё в одном случае — и это касается ВСЕХ сущностей, не только дел.** Если записей обработано меньше, чем ответ обещал, он ставит `truncated: true` и добавляет `meta.recordsShortfall` — сколько записей не хватило. Обещанное — это `data.count`, а в режиме счётчиков по стадиям (`meta.aggregatePath: "fanout"`) большее из `data.count` и суммы счётчиков стадий: пробы стадий могут в пределах допуска дать больше воронки, и тогда нехватка меряется от них. Поэтому предикат «неполно, если `recordsProcessed < totalRecords`» верен не всегда — читайте сам `truncated`. Раньше в этом случае приходило `truncated: false`, то есть группы и числовые агрегации считались по части записей, а ответ этого не сообщал. `count` и `meta.totalRecords` остаются полными.

**Признак усечения приходит рядом с самим числом.** Пока он лежал только в `meta`, клиент, который читает `data.aggregates.amount.sum` и больше ничего, получал уверенное число без единого намёка на неполноту. Поэтому при усечении каждый объект поля в `data.aggregates` и в `groups[].aggregates` дополнительно несёт `truncated: true`:

```json
{
  "data": {
    "count": 20000,
    "aggregates": {
      "amount": { "sum": 1234567, "truncated": true }
    },
    "meta": {
      "totalRecords": 20000,
      "recordsProcessed": 5000,
      "truncated": true,
      "warnings": [{ "code": "AGGREGATE_TRUNCATED", "message": "…" }]
    }
  }
}
```

Значения `sum` / `avg` / `min` / `max` остаются числами — в объект их никто не оборачивает. Ключ `truncated` **внутри объекта поля** появляется ТОЛЬКО при усечении: на полной выборке его нет вовсе, а не `false`. Это касается условных пометок — `data.aggregates.<поле>.truncated`, того же ключа внутри `groups[].aggregates` и `groups[].truncated`. На `data.meta.truncated` правило не распространяется: он присутствует **всегда** и на полной выборке равен `false`, поэтому проверять там нужно значение, а не наличие ключа.

Признак стоит в том объекте, из которого читается число: при усечении сам объект группы несёт `groups[].truncated` рядом со своим `count`. На группировке с одним только `count` это единственная пометка у числа — у `count`-выражения объекта поля не возникает вовсе, и оба бэга `aggregates` приходят пустыми.

Пометка говорит, что **ответ** — образец. Точен ли при этом сам счётчик группы, зависит от пути, и путь называет `meta.aggregatePath`: на обычном обходе (поля нет) `groups[].count` — это размер прочитанного среза, а не всей группы, и как итог по группе его использовать нельзя. В режиме счётчиков по стадиям (`meta.aggregatePath: "fanout"`) счётчик берётся отдельной пробой и точен — образцом там остаются только `aggregates` рядом с ним. Точные числа его не несут никогда: `data.count` и `meta.totalRecords` берутся отдельным быстрым подсчётом и верны на выборке любого размера. Тем же условием добавляется предупреждение `AGGREGATE_TRUNCATED` в `meta.warnings` — второй канал для клиента, который проверяет `meta.warnings` перед тем, как считать выдачу полной.

Что с этим делать — способа ровно два: сузить фильтр так, чтобы выборка уложилась в потолок, либо взять один `count` — без `groupBy` и без числовых функций. `count` точен на выборке любого размера, потому что записи для него не выгружаются вовсе.

Добавление `groupBy` к **числовой** агрегации потолок не снимает ни на одном аккаунте: группировка заставляет запрос выгружать записи, поэтому такая агрегация считается по той же обрезанной странице и приходит ровно такой же неполной. Повторять ту же сумму, дописав `groupBy`, — единственное, чего делать не стоит: это ровно тот дорогой вызов, ради ограничения которого потолок и существует.

У сделок есть отдельный режим счётчиков по стадиям, который отвечает точными числами, не читая строк, — но он включается по аккаунтам, и по усечённому ответу нельзя понять, включён ли он у вас. Не угадывайте, а проверьте одним запросом: `{ "aggregate": [{ "field": "*", "function": "count" }], "groupBy": "stageId" }` со скалярным `categoryId` в фильтре. Сработал режим — в ответе будет `meta.aggregatePath: "fanout"` и `meta.recordsProcessed: 0`, а счётчики точны. Не сработал — придёт обычный усечённый ответ, и вы потеряете только этот один вызов.

Само число суммы при усечении — образец по части записей, а не итог, и как итог его использовать нельзя.

У сделок есть отдельный режим счётчиков по стадиям, который отвечает без выгрузки записей, но **обходом потолка в общем случае он не является и по умолчанию выключен**: его включает администратор платформы по аккаунтам, он работает только для `groupBy` по `stageId` или `stageSemanticId` со скалярным `categoryId` и только без числовых функций. Пока он выключен — а это состояние по умолчанию, — такой запрос идёт обычным путём и выше потолка приходит усечённым. Условия целиком — [Агрегация сделок](./entities/deals/aggregate.md).

### Агрегация POST — сужающий фильтр у дел

У дел ограничение другого рода, и оно касается даже простого `count`: Битрикс24 не успевает посчитать все дела аккаунта за отведённое на вызов время. Поэтому агрегация дел требует **одного** сужения — пара `ownerTypeId` + `ownerId`, либо `responsibleId`, либо граница по дате на `createdAt` / `updatedAt` / `deadline`. Без сужения приходит `400 MISSING_REQUIRED_FILTER` с перечнем допустимых сужений, а если требование на аккаунте ещё не включено — `422 AGGREGATION_LIMIT_EXCEEDED` в тот момент, когда Битрикс24 действительно не ответил. Ни то, ни другое повторять бессмысленно. Подробно — [Агрегация дел](./entities/activities/aggregate.md).

### Агрегация POST — UF-поля

Канонический POST-вариант агрегации принимает массив выражений и поддерживает пользовательские поля. Имя поля — то же, что в схеме сущности: на другое написание приходит `400 INVALID_PARAMS` со списком доступных полей.

```bash
curl -X POST https://vibecode.bitrix24.tech/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-поддержки:**

| Функция | Принимает UF-типы |
|---------|-------------------|
| `sum`, `avg`, `min`, `max` | только `integer`, `double`, `money` |
| `groupBy` | любой UF-тип — `string`, `enumeration`, `date`, `integer` и другие |

**Поля типа `money`** хранятся в Битрикс24 как строка `"сумма|валюта"` (например `"1500.50|RUB"`). Агрегат извлекает числовую часть до `|` — все арифметические операции корректны.

**Ошибки:**

| HTTP | Код | Когда |
|------|-----|-------|
| 400 | `INVALID_PARAMS` | UF-тип не из списка `integer`/`double`/`money` для `sum`/`avg`/`min`/`max` — в `message` указан фактический тип |
| 400 | `INVALID_PARAMS` | Поле не найдено ни в схеме, ни в UF-кэше — в `message` перечислены доступные стандартные и UF-поля |

**Кэш UF-полей:** одно обращение к `crm.{entity}.fields` на комбинацию `портал+сущность` (плюс `entityTypeId` для `items`). TTL 5 минут — повторные агрегаты в пределах окна не вызывают дополнительных запросов.

### Схема полей — `GET /v1/{entity}/fields`

Получить описание всех полей сущности с типами.

```bash
curl -H "X-Api-Key: $KEY" \
  "https://vibecode.bitrix24.tech/v1/deals/fields"
```

Метаданные каждого поля в ответе содержат отображаемое название `label` и пояснение назначения `description`, где они заданы для поля. Названия полей не нужно искать в отдельном справочнике — они приходят вместе со схемой.

Названия и пояснения приходят на русском языке. Заголовками запроса язык не переключается. Названия полей, которые платформа берёт напрямую с портала, включая пользовательские, приходят на языке портала.

### Пакетные операции — `POST /v1/{entity}/batch`

Массовое создание, обновление или удаление записей одной сущности.

```json
{ "action": "create", "items": [{ "title": "Сделка 1" }, { "title": "Сделка 2" }] }
```

Действие `list` принимает `filter`, `select` и `limit`, как и одиночный список, и отбор применяется к ответу: в записях остаются только перечисленные поля плюс `id`. Незнакомое имя приезжает предупреждением `UNKNOWN_SELECT_FIELD` в `meta` **этого подвызова** — соседние подвызовы своё `meta` не получают. У сущностей, чей набор полей выверен по Битрикс24, такое имя вместо предупреждения отклоняет подвызов ошибкой `UNKNOWN_SELECT_FIELD`: отказывает только он, соседи по пакету выполняются. Значение `*` (и `UF_*`, в любом регистре) означает «вернуть все поля» — отбор не применяется, и незнакомое имя рядом с ним ничего не отклоняет. **Важно:** `order` эта дверь НЕ читает — порядок задаёт сама платформа, и переданный в подвызове `order` не применяется. Нужна сортировка — берите глобальный [POST /v1/batch](./batch.md) и передавайте её под именем `sort`: `order` он тоже не переводит в имена полей Битрикс24.

Для работы с **разными сущностями** в одном запросе используйте [Batch API](./batch.md).

## Формат ответа

Все эндпоинты возвращают единый формат:

```json
{
  "success": true,
  "data": [ ... ],
  "meta": { "total": 150, "hasMore": true }
}
```

| Поле | Описание |
|------|---------|
| `success` | `true` при успешном выполнении |
| `data` | Массив записей у `list` и `search`, объект у `get` и `create` |
| `meta.total` | Общее количество записей под фильтр у `list` и `search`. Необязательное поле: если количество не заказывалось, его в ответе нет — см. [Листание и количество записей](#листание-и-количество-записей) |
| `meta.hasMore` | Есть ли ещё записи для загрузки |
| `meta.nextAfterId` | Идентификатор последней отданной записи, строкой. Приходит у списка и у поиска при сортировке строго по `id` по возрастанию, пока `meta.hasMore` равен `true`. Передаётся обратно как `filter[>id]`. Сущности с курсором перечислены в [Листании и количестве записей](#листание-и-количество-записей) |
| `meta.pageErrorSample` | Приходит у списка и у поиска, когда часть страниц не загрузилась: `code` и `message` первой ошибки. Ответ при этом содержит непрерывное начало выборки, а не все записи. `code` — либо код ошибки Битрикс24, либо код самого Вайбкод. Последних три. `KEYSET_DISCONTINUITY` означает, что обход прервал сам Вайбкод, обнаружив разрыв в последовательности страниц, и вернул непрерывное начало вместо возможных дублей. `PAGE2_COUNT_FAILED` — не удался подсчёт записей (тайм-аут, лимит запросов, ошибка портала). `LAZY_COUNT_NO_PROGRESS` — метод вернул те же записи вместо следующих. `meta.total` отсутствует только у двух последних: там подсчёт не состоялся. При остальных обрывах количество уже сосчитано, и `meta.total` приходит рядом |
| `meta.windowErrorSample` | То же для поиска по окнам: `code` и `message` первого сорвавшегося окна. Приходит вместе с `meta.windowErrors` — числом неудачных окон. При полном отказе поиск возвращает ошибку, а не частичный ответ |
| `meta.warnings` | Массив предупреждений о выдаче: у списка, поиска, получения записи по `id`, запроса схемы полей, у товарных позиций сущности и у агрегации. У каждого есть `code` и `message`, а `field` — только когда предупреждение привязано к конкретному полю. Коды `LIMIT_ZERO_IGNORED` и `UNKNOWN_SELECT_FIELD` описаны в параметрах выше. `WINDOW_TRUNCATED` — оконный поиск отдал не всё, разбор в [неполной выдаче](./optimization.md#неполная-выдача). `OFFSET_BEYOND_FETCHED_PAGE` — страница вышла пустой на постраничном обходе, разбор в [обходе по страницам вручную](./filtering.md#идти-по-страницам-вручную). `AGGREGATE_TRUNCATED` — числовая агрегация посчитана по части записей, разбор в [потолке 5000 записей](#агрегация-post-потолок-5000-записей) |

Форма выше — про сущности этого справочника. Другие семейства маршрутов кладут служебные поля иначе:

- **Глобальный [POST /v1/batch](./batch.md)** — `results`, `errors`, `summary`, `totals` и `meta` лежат внутри `data`, с разбивкой по `id` каждого вызова.
- **Выделенные маршруты** — чаты, почта, лента, база знаний, звонки, рабочий день — несут свою форму `data`, она описана на страницах этих разделов.

### Листание и количество записей

Листайте по `meta.hasMore`, а не по арифметике от `meta.total`. Признак «есть ещё» выводится из полноты страницы: пришла полная — возможно, есть ещё, пришла неполная — список закончился. Цикл «читай, пока `hasMore`» доходит до конца всегда. Если коллекция ровно кратна `limit`, последний шаг вернёт пустой список — это штатный признак конца, а не ошибка.

При сортировке строго по `id` по возрастанию ответ дополнительно несёт `meta.nextAfterId` — идентификатор последней отданной записи. Передайте его обратно как `filter[>id]`, и следующая страница начнётся ровно за ней. Такое листание не зависит от смещения и не дорожает к концу коллекции, поэтому для обходов в десятки тысяч записей оно предпочтительнее растущего `offset`. Готовый цикл обхода — [Постраничный вывод](./filtering.md#постраничный-вывод).

Курсор доступен у сделок, лидов, контактов, компаний, предложений и элементов смарт-процессов. У остальных сущностей `meta.nextAfterId` в ответе нет: страницы там берутся `offset`, а размер выборки сокращается фильтром.

Количество записей — отдельный вопрос, и стоит он у Битрикс24 несоразмерно дорого: посчитать коллекцию заметно дороже, чем отдать из неё страницу. Поэтому:

- **Нужна цифра** — спросите её прямо: `POST /v1/{entity}/aggregate` с функцией `count` возвращает количество одним вызовом, без выгрузки записей.
- **Цифра не нужна** — уберите её из ответа: `withTotal=false` в запросе списка или настройка `totalDefault` на API-ключе, если так работает вся интеграция. Про то, в каких случаях `meta.total` при этом всё же приходит, — ниже в этом разделе. **Важно:** Нагрузку это снимает только на одностраничном вызове (`limit` не больше 50): там подсчёт действительно не заказывается. На многостраничном (`limit` больше 50) подсчёт платформе нужен, чтобы спланировать обход, поэтому параметр убирает число, а не стоимость. Ставить его ради экономии там незачем: дешевле вызов не станет, а точное количество, которое короткая первая страница отдаёт бесплатно, будет отброшено.
- **Не эмулируйте счётчик обходом.** Пролистать коллекцию, чтобы посчитать строки, — это десятки и сотни вызовов вместо одного и самый дорогой способ узнать одну цифру. Для этого есть `aggregate` с `count`.

Когда подсчёт не заказан, `meta.total` в ответе чаще всего отсутствует — но не всегда. Если страница пришла короче запрошенного `limit`, количество известно из самой страницы, и точное число всё равно приходит. Полная картина:

| Что было в запросе | `meta.total` в ответе |
|---|---|
| Подсчёт заказан — умолчание или `withTotal=true` | приходит |
| Передан `withTotal=false` | не приходит никогда |
| Подсчёт отключён настройкой ключа или платформенным умолчанием, `offset = 0`, страница короче `limit` | приходит, точное число — в том числе `0` |
| Подсчёт отключён настройкой ключа или платформенным умолчанием, страница полная либо `offset` больше нуля | не приходит |

Таблица описывает вызов, на котором подсчёт **можно** пропустить. Там, где пропустить его нельзя, `withTotal=false` просто игнорируется и `meta.total` приходит как раньше. Поэтому проверяйте наличие поля в конкретном ответе, а не выводите его из настроек.

Короче: если подсчёт не заказан, `total` приходит только на вызове с `offset = 0` и только если страница пришла короче запрошенного. Если подсчёт заказан — умолчанием или явным `withTotal=true` — он приходит всегда. Обратите внимание на разницу между параметром и настройкой: явный `withTotal=false` в запросе убирает и точное число из короткой страницы, а настройка `totalDefault` на ключе — нет. Поэтому два одинаковых запроса от двух разных ключей могут вернуть ответы разной формы.

Из «подсчёт заказан — приходит всегда» есть одно исключение — частичный ответ, у которого сорвался сам подсчёт. Если в ответе есть `meta.pageErrorSample` с кодом `PAGE2_COUNT_FAILED` или `LAZY_COUNT_NO_PROGRESS`, `meta.total` отсутствует: количество не сосчитано, а число отданных строк его не заменяет. При остальных обрывах — `KEYSET_DISCONTINUITY` и любой код Битрикс24 — подсчёт состоялся до обрыва, и `meta.total` приходит рядом с `meta.pageErrorSample`. И тот и другой ответ приходит с кодом `200`, содержит непрерывное начало выборки и `meta.hasMore`, равный `true`.

Сам подсчёт платформа выполняет, только когда без него не обойтись. На форме ответа это не сказывается: на многостраничном вызове (`limit` больше 50) `meta.total` приходит так же, как приходил, — просто иногда количество оказывается известно бесплатно.

`meta.total` — информативное поле. Оно может отставать от текущего состояния коллекции до минуты, поэтому число отданных строк иногда оказывается больше него. Полнота данных от `meta.total` не зависит: за неё отвечают `meta.hasMore` и `meta.nextAfterId`.

Действующее для вашего ключа умолчание по `meta.total` показывает блок `totalDefault` в [GET /v1/me](./keys-auth/me.md): `key` — настройка ключа, `platform` — платформенное умолчание, `effective` — что получится, если запрос не передаст `withTotal`.

## Преобразование полей

Вайбкод автоматически преобразует имена полей при отправке запроса. В запросах всегда используйте camelCase:

```json
{ "title": "Сделка", "stageId": "NEW", "assignedById": 1 }
```

В ответах объявленные поля сущностей приходят в camelCase — `id`, `title`, `stageId`, `responsibleId`.

Пользовательские поля портала — отдельный случай. Их состав зависит от портала, заранее они не описаны, а часть полей носит имя, сгенерированное самим Битрикс24. Имя, под которым такое поле принимается в запросе и приходит в ответе, берите из схемы `GET /v1/{entity}/fields` — написание различается по сущностям. Имена и форматы значений собраны в разделе [Пользовательские поля (UF)](#пользовательские-поля-uf).

Отличается не только регистр — у части полей отличается само имя. Например, сумма сделки в Вайбкод называется `amount`, а исходное имя этого поля в Битрикс24 — `opportunity`. Имя Вайбкод стоит в колонке «Поле» схемы `GET /v1/{entity}/fields`, исходное имя Битрикс24 — в колонке «Битрикс24». В запросах и при чтении ответа используйте имя из колонки «Поле» — под ним поле и приходит в ответе, исходное имя Битрикс24 не возвращается. Список соответствий для каждой сущности — на её странице полей, например [Поля сделки](./entities/deals/fields.md).

## Часовой пояс на записи

Дата-время, записанное без пояса (`2026-07-15T13:00:00`), Битрикс24 читает в поясе того аккаунта, через который платформа пишет на портал, — а не в вашем. Клиент из Берлина, отправивший `13:00`, получал сохранённые `10:00` по Гринвичу вместо `11:00`: разница в час летом и в два зимой.

Чтобы этого не было, объявите свой пояс заголовком запроса:

```
X-Vibe-Timezone: Europe/Berlin
```

Значение — имя пояса из базы IANA. В браузере оно берётся из `Intl.DateTimeFormat().resolvedOptions().timeZone`, серверная интеграция подставляет пояс, в котором ведёт свои данные.

**Заголовок необязателен и ничего не ломает.** Без него, с неизвестным именем пояса или с мусором вместо имени поведение остаётся прежним — значение уходит на портал как есть. Отказа не будет: заголовок добавлен так, чтобы ни один существующий вызов не изменил поведения молча.

**Смещение получает только запись вида `ГГГГ-ММ-ДДTЧЧ:ММ`, где пояс не указан, и только в тех полях, где заголовок действует.** Секунды и доли секунды не обязательны и на подстановку не влияют, а перечень полей — ниже. Остальные формы даты-времени уходят на портал без изменений, и заголовок на них не действует:

| Что отправлено | Что уходит на портал |
|---|---|
| `2026-09-15T13:00:00` | `2026-09-15T13:00:00+02:00` — смещение подставлено |
| `2026-09-15T13:00`, `2026-09-15T13:00:00.500` | смещение подставлено так же — без секунд и с долями секунды |
| `2026-09-15T13:00:00Z` или со смещением | как есть — пояс объявлен вами, платформа его не переписывает |
| `2026-09-15 13:00:00` через пробел | как есть — читается в поясе портального аккаунта |
| `15.09.2026 13:00` в местной форме | как есть — читается в поясе портального аккаунта |

Две последние формы отказа не вызывают: ответ приходит успешным, а сохранённое время отличается на разницу между вашим поясом и поясом портала. Для заголовка `Europe/Berlin` и портала в поясе UTC+3 запись `2026-09-15T13:00:00` сохраняется как `2026-09-15T11:00:00Z`, а те же 13:00 через пробел — как `2026-09-15T10:00:00Z`.

**Переход на летнее время учитывается по каждому значению отдельно.** Смещение берётся то, которое действовало в поясе на саму дату из значения, а не на момент запроса. Одна и та же интеграция, записывающая июльскую и январскую даты в одном запросе, получит корректные обе.

**Пояс подставляется не всем полям подряд, а только проверенным.** Битрикс24 объявляет частью полей тип «дата и время», а хранит в них голую дату — подставленное смещение сдвинуло бы у такого поля день. Поэтому список полей открывается по мере живой проверки каждого. Сегодня в нём поля задачи: `deadline`, `startDatePlan`, `endDatePlan`, а также служебные `createdDate`, `changedDate`, `closedDate` — значение вида `2019-05-15T13:47:00` получает у них смещение вашего пояса, а не читается в поясе владельца ключа. Где заголовок действует, видно по машинному описанию API — у операции он объявлен в списке параметров.

Отдельно стоят [события календаря](./entities/calendar-events.md): у них пояс задаётся собственными параметрами запроса (`timezoneFrom` / `timezoneTo`), поэтому заголовок для них игнорируется — иначе событие сдвинулось бы дважды.

**Заголовок влияет только на запись.** На фильтр он не действует — значения фильтра читаются в поясе портального аккаунта независимо от заголовка. Что из этого следует для поиска по датам и как пересчитать границы — [Часовой пояс в значении фильтра](./filtering.md#часовой-пояс-в-значении-фильтра).

## Мультиполя (email, телефон, сайт)

Контактные поля `email`, `phone` и `web` хранят несколько значений с типами. Набор зависит от сущности — `email` и `phone` есть у контактов, компаний и лидов, `web` только у компаний. Формат отличается на запись и на чтение.

**На запись** — массив объектов `[{ "value": "petrov@example.com", "typeId": "WORK" }]`. Также принимается одиночная строка `"petrov@example.com"` и массив строк. Значение `typeId` зависит от поля — `WORK` / `HOME` / `MOBILE` / `OTHER` для телефона, `WORK` / `HOME` / `MAILING` / `OTHER` для email. По умолчанию `WORK`.

```json
{ "email": [{ "value": "petrov@example.com", "typeId": "WORK" }] }
```

Форма с ключами `VALUE` и `VALUE_TYPE` в верхнем регистре не принимается — `400 INVALID_MULTIFIELD_SHAPE`. Используйте camelCase `value` и `typeId`.

**На чтение** — первичное значение приходит строкой (`"email": "petrov@example.com"`), а полный набор значений с типами лежит в массиве `fm`. Значения по типам доступны в отдельных полях (`emailWork`, `phoneMobile`).

Точный набор `typeId` и полей чтения для каждой сущности — на её странице, например [Контакты](./entities/contacts.md) и [Компании](./entities/companies.md).

## Пользовательские поля (UF)

Пользовательские поля портала читаются и записываются наравне с объявленными полями сущности — отдельного эндпоинта для значений нет. Создание, изменение и удаление самих полей — в разделе [Пользовательские поля](./userfields.md).

### Имя поля

Рабочее имя одно — то, под которым поле стоит в схеме `GET /v1/{entity}/fields`. Под этим именем поле принимается в теле запроса, в `filter` и в `select`, под ним же оно приходит в ответе. Написание различается по сущностям, и вывести его из имени, заданного при создании, нельзя:

| Сущность | Примеры имён в схеме |
|----------|----------------------|
| Сделки, лиды, контакты, компании, предложения | `ufCrmProjectCode`, `ufCrm_1729594209` |
| Элементы смарт-процессов | `ufCrm3_1628508847` |
| Реквизиты | `UF_CRM_1698325419` |
| Сотрудники | `UF_USR_1619099890455`, `UF_PHONE_INNER` |

Список определений полей и схема сущности называют одно и то же поле по-разному. Поле, созданное на сделках как `fieldName: "PROJECT_CODE"`, стоит в ответе `GET /v1/userfields/deals` под именем `UF_CRM_PROJECT_CODE`, а в схеме сделки и во всех запросах — под именем `ufCrmProjectCode`.

**Имя, которого нет в схеме, не даёт ошибки при записи.** У сделок, лидов, контактов, компаний, предложений и элементов смарт-процессов значение, отправленное под именем `UF_CRM_PROJECT_CODE` вместо `ufCrmProjectCode`, не сохраняется: ответ приходит с кодом `200` или `201`, а поле остаётся пустым. В `select` такое имя не добавляет поле в ответ, в агрегации — на него приходит `400 INVALID_PARAMS` со списком доступных полей. Поведение отбора — [Неизвестное имя поля в фильтре](./filtering.md#неизвестное-имя-поля-в-фильтре).

### Формат значения по типам

Тип поля задаётся при создании параметром `userTypeId` и приходит в схеме сущности в поле `type`.

Примеры ниже сняты на портале в поясе UTC+3.

| Тип | Что отправлять | Что приходит в ответе |
|-----|----------------|----------------------|
| `string` | `"Договор №17"` | Та же строка |
| `integer` | `42` или `"15"` | Число. Дробное значение обрезается до целого — `3.7` сохраняется как `3` |
| `double` | `3.14` или `"7.25"` | Число, округлённое до числа знаков после запятой, заданного настройкой `PRECISION` этого поля. При `PRECISION` равном `2` значение `3.14159` приходит как `3.14`. Поле, созданное без `settings`, получает `PRECISION` равный `0` и хранит целые: `3.14` приходит как `3`, а `3.99` — как `4` |
| `boolean` | `true`, `false`, `"Y"`, `"N"`. Значения `1`, `0`, `"1"` и `"0"` сохраняются как отрицательное значение — `false` у сделок, `"N"` у остальных | У сделок — `true` или `false`. У контактов, лидов, компаний, предложений и элементов смарт-процессов — строки `"Y"` и `"N"` |
| `enumeration` | Идентификатор варианта — `"3821"`. Варианты поля перечислены в массиве `items` схемы сущности | Идентификатор числом — `3821`. Текст варианта вместо идентификатора сохраняется как `0` |
| `datetime` | `"2026-08-10T12:30:00"` — время читается в поясе портала. Смещение можно указать явно | У сделок — момент времени по Гринвичу, `"2026-08-10T09:30:00.000Z"`. У контактов, лидов, компаний, предложений и элементов смарт-процессов — то же время со смещением портала, `"2026-08-10T12:30:00+03:00"` |
| `date` | `"2026-08-10"` | Полный момент времени со смещением портала — `"2026-08-10T03:00:00+03:00"` |
| `money` | `"100\|USD"` — сумма и код валюты через вертикальную черту. Значение без кода валюты сохраняется в базовой валюте портала | `"100\|USD"`. Отправленное `100` приходит как `"100\|RUB"`, `"250.50\|RUB"` — как `"250.5\|RUB"` |
| `url` | `"https://example.com/contract"` | Та же строка |
| `address` | `"Москва, Тверская 1\|55.7601;37.6055"` — адрес и координаты разделяет вертикальная черта, широту и долготу — знак `;` | Та же строка. Широта и долгота, разделённые вертикальной чертой вместо `;`, не сохраняются |
| `employee` | `1` — идентификатор сотрудника, список: `GET /v1/users` | `1` |
| `crm` | `"L_1000739"` — буквенный префикс типа и идентификатор записи: `L_` лид, `C_` контакт | Та же строка |
| `crm_status` | `"NEW"` — значение `statusId`, список: `GET /v1/statuses` | Та же строка |
| `file` | `["contract.pdf", "BASE64_CONTENT"]` — имя файла и содержимое в Base64 | У поля с одним значением — объект с полями `id`, `url`, `urlMachine`, у множественного — массив таких объектов. Подробнее — [Файлы в CRM](./recipes/crm-files.md) |

Настройка `PRECISION` задаётся при создании поля — `"settings": { "PRECISION": 2 }`, см. [Создать поле](./userfields/crm/create.md).

Заголовок `X-Vibe-Timezone` из раздела [Часовой пояс на записи](#часовой-пояс-на-записи) на пользовательские поля не действует: значение без смещения читается в поясе портала.

Значения типов `employee`, `crm` и `crm_status` сохраняются без сверки со справочником портала: идентификатор несуществующего сотрудника или значение вне справочника принимаются и возвращаются как есть. Проверяйте их на своей стороне.

### Несколько значений в одном поле

Признак множественности приходит в списке определений полей — поле `multiple` со значением `Y` или `N`. Для сделок, лидов, контактов, компаний, предложений и реквизитов это `GET /v1/userfields/:entity`, для смарт-процессов — `GET /v1/items/:entityTypeId/userfields`. У пользовательского поля в схеме `GET /v1/{entity}/fields` этого признака нет.

Значение множественного поля передаётся массивом и заменяет прежний набор целиком:

```json
{ "ufCrmProjectTags": ["первое", "второе"] }
```

Одиночное значение вместо массива не сохраняется. Пустой массив и `null` оставляют прежние значения — очистить множественное поле пустым массивом или `null` нельзя.

### Очистка значения

Поле с одним значением очищается значением `null` или пустой строкой, а файловое поле с одним значением — пустым массивом. В ответе очищенное поле приходит как `null`. Множественное поле ничем из перечисленного не очищается.

## Особые сущности

### Смарт-процессы (`items`)

Смарт-процессы используют `entityTypeId` в URL: `GET /v1/items/{entityTypeId}`.

```bash
# Список записей смарт-процесса с entityTypeId = 1058
curl -H "X-Api-Key: $KEY" \
  "https://vibecode.bitrix24.tech/v1/items/1058?limit=10"
```

Список доступных смарт-процессов: `GET /v1/smart-processes`.

### События календаря

Требуют обязательные параметры: `type` со значением `user`, `group` или `company`, и `ownerId`.

```bash
curl -H "X-Api-Key: $KEY" \
  "https://vibecode.bitrix24.tech/v1/calendar-events?type=user&ownerId=1"
```

### Файлы

Требуют `folderId`. Получите его из `GET /v1/storages` → поле `rootFolderId`.

## Лимиты

10 запросов/секунду — лимит Битрикс24 на уровне портала, общий для всех ключей.

**Как укладываться в лимит:**
- [Batch API](./batch.md) — 50 вызовов за 1 единицу лимита
- `POST /v1/{entity}/batch` — до 500 записей CRUD за 10 единиц (а не за 500)
- `POST /v1/{entity}/aggregate` — `count` идёт одним быстрым вызовом, а `sum`/`avg`/`min`/`max` подгружают до 5000 записей и считаются на стороне Вайбкод
- Параметр `select` — загрузка только нужных полей сокращает объём ответа

Подробнее: [Оптимизация](./optimization.md) — паттерны для дашбордов, массовых операций, сканирования больших объёмов

## Запись ключом в режиме «только чтение»

Ключ с режимом доступа «только чтение» выполняет чтение и получает `403 WRITE_BLOCKED_READONLY_KEY` на любой вызов записи — создание, обновление, удаление, действие над сущностью. Полное описание кода и полей `details` — [Коды ошибок](./errors.md). Как переключить режим и как работает политика портала — [Режим доступа](./keys-auth/access-mode.md).

## Паттерны использования

| Задача | Подход |
|--------|--------|
| Дашборд / аналитика | `POST /v1/{entity}/aggregate` — счётчики и суммы по фильтру |
| Массовое обновление | `POST /v1/{entity}/batch` с `action=update` |
| Данные из нескольких сущностей | `POST /v1/batch` — `deals`, `tasks` и `contacts` в одном запросе |
| Запись + связанные данные | `?include=company,contact` — [связанные данные](./includes.md) в одном ответе |
| Сканирование десятков тысяч записей | Курсор по `id` — `order[id]=asc` плюс `filter[>id]` из `meta.nextAfterId`, см. [Листание и количество записей](#листание-и-количество-записей). Где курсора нет — `POST /v1/{entity}/search` с `limit` до 5000 и сужением фильтра |

## Смотрите также

- [Связанные данные (include)](./includes.md) — загрузка связанных сущностей в одном запросе
- [Фильтрация](./filtering.md) — три синтаксиса фильтров, даты, фильтры отрицания
- [Batch API](./batch.md) — до 50 вызовов в одном запросе
- [Справочник API](./api-reference.md) — полный список сущностей со ссылками
- [Оптимизация](./optimization.md) — ограничения частоты, паттерны производительности
- [Коды ошибок](./errors.md) — справочник ошибок платформы
