Для AI-агентов: markdown этой страницы — /docs-content/filtering.md индекс документации — /llms.txt
Фильтрация и поиск
Три синтаксиса фильтрации для API сущностей. Все стили можно смешивать в одном запросе.
Фильтрация работает в двух местах:
GET /v1/{entity}?filter[field]=value— параметр URL для спискаPOST /v1/{entity}/search— тело запроса{ "filter": { ... } }
Быстрый переход: Как передать фильтр в GET-запросе · Фильтр по телефону и почте · Логика ИЛИ · Эндпоинт поиска · Постраничный вывод · Коды ошибок
Как передать фильтр в GET-запросе
У параметра filter две равноправные формы записи. Выберите одну и передайте в ней весь фильтр целиком.
Скобочная запись — каждое условие отдельным параметром URL:
GET /v1/deals?filter[stageId]=NEW&filter[amount][$gte]=50000
JSON-объект — весь фильтр одним значением. Та же форма, что в теле POST /v1/{entity}/search:
GET /v1/deals?filter={"stageId":"NEW","amount":{"$gte":50000}}
Значение JSON-формы нужно закодировать для URL:
const filter = { stageId: 'NEW', amount: { $gte: 50000 } }
const query = `filter=${encodeURIComponent(JSON.stringify(filter))}&limit=50`
Значение, которое не является ни скобочной записью, ни JSON-объектом, отклоняется с 400 INVALID_FILTER — вместо того чтобы молча вернуть всю коллекцию. Пустое ?filter= означает «без фильтра».
Две формы в одном запросе смешивать нельзя. В запросе
?filter={"id":3}&filter[amount]=5до фильтра доходит только одна из них: разборщик строки запроса пишет обе в одно и то же место, и вторая замещает первую. Ответ при этом выглядит корректно отфильтрованным, хотя половина условий не применена, поэтому такой запрос отклоняется с400 INVALID_FILTER.
Синтаксис 1: операторы со знаком `$`
Операторы с префиксом $ внутри объекта поля.
{
"filter": {
"amount": { "$gte": 50000 },
"stageId": { "$ne": "LOST" },
"createdAt": { "$gte": "2026-01-01T00:00:00" }
}
}
Найдёт записи с суммой от 50 000, стадией, отличной от LOST, созданные с начала 2026 года.
Доступные операторы
| Оператор | Значение | Пример |
|---|---|---|
$gt |
> (больше) | { "amount": { "$gt": 10000 } } |
$gte |
>= (больше или равно) | { "amount": { "$gte": 50000 } } |
$lt |
< (меньше) | { "amount": { "$lt": 100000 } } |
$lte |
<= (меньше или равно) | { "amount": { "$lte": 200000 } } |
$ne |
!= (не равно) | { "stageId": { "$ne": "LOST" } } |
$contains |
поиск по подстроке | { "title": { "$contains": "поставка" } } |
$in |
входит в массив (IN) | { "stageId": { "$in": ["NEW", "WON"] } } |
$nin |
НЕ входит в массив (NOT IN) | { "categoryId": { "$nin": [1, 3] } } |
Точное совпадение задаётся значением напрямую, без оператора: { "stageId": "NEW" }.
Исключить набор значений. «Поле НЕ входит в список» задаётся оператором
$nin:{ "categoryId": { "$nin": [1, 3] } }вернёт сделки всех направлений, кроме 1 и 3. Родные префиксы Битрикс24@(IN) и!@(NOT IN) в имени поля ({ "@categoryId": [...] },{ "!@categoryId": [...] }) НЕ поддерживаются — используйте операторы$in/$nin.
Комбинирование
Несколько условий на одном поле объединяются логикой И. Условия на разные поля — тоже логикой И. Для ИЛИ-логики см. раздел Логика ИЛИ ниже.
{
"filter": {
"amount": { "$gte": 50000, "$lte": 200000 },
"stageId": { "$ne": "LOST" }
}
}
Найдёт записи с суммой от 50 000 до 200 000 и стадией, отличной от LOST.
Синтаксис 2: префикс в имени поля
Оператор как часть имени поля. Форма, которую напрямую понимает Битрикс24.
{
"filter": {
">=amount": 50000,
"<=amount": 200000,
"!stageId": "LOST"
}
}
Найдёт записи с суммой от 50 000 до 200 000 и стадией, отличной от LOST.
Операторы
| Префикс | Значение |
|---|---|
>= |
больше или равно |
> |
больше |
<= |
меньше или равно |
< |
меньше |
! |
не равно |
% |
подстрока |
Синтаксис 3: оператор как ключ объекта
Оператор как ключ вложенного объекта. Та же форма, что в синтаксисе 2, но с разделением имени поля и условия.
{
"filter": {
"amount": { ">=": 50000 },
"stageId": { "!": "LOST" }
}
}
Найдёт записи с суммой от 50 000 и стадией, отличной от LOST.
Фильтрация по дате
Поля даты (createdAt, updatedAt, closedAt, beginDate и др.) принимают строки ISO 8601:
{
"filter": {
"createdAt": { "$gte": "2026-01-01T00:00:00" },
"closedAt": { "$lte": "2026-03-31T23:59:59" }
}
}
Найдёт записи, созданные с начала 2026 года и закрытые до конца марта.
Примеры
{ "filter": { ">=createdAt": "2026-03-01T00:00:00" } }
Найдёт записи, созданные с 1 марта 2026.
{ "filter": { "updatedAt": { "$gte": "2026-05-01T00:00:00" } } }
Найдёт записи, обновлённые с указанного момента.
Часовой пояс в значении фильтра
Значения фильтра всегда читаются в поясе портального аккаунта. Битрикс24 обрабатывает фильтр по дате без учёта пояса: значение с суффиксом (Z или +02:00) он молча отбрасывает вместе со всем условием — запрос возвращает успех и всю таблицу. Поэтому платформа срезает суффикс сама, и в Битрикс24 уходит голое ГГГГ-ММ-ДДTЧЧ:ММ:СС. Указывать пояс в значении фильтра бессмысленно, а полагаться на него — опасно.
Заголовок X-Vibe-Timezone, которым клиент объявляет свой пояс, действует только на запись. Значит запись 2026-07-15T13:00:00 с этим заголовком и фильтр по тому же литералу не совпадут: запись легла на портал со смещением вашего пояса, а фильтр ищет в поясе портала. Пересчитайте границы фильтра в пояс портала сами.
Границы диапазона задаются операторами $gte/$lte — или эквивалентными >=/<= из синтаксисов выше. Ключи from/to в значении поля не поддерживаются и вернут 400 INVALID_FILTER_OPERATOR:
{ "filter": { "createdAt": { "$gte": "2026-06-01T00:00:00", "$lte": "2026-06-30T23:59:59" } } }
NOT-фильтры
Исключение значений:
{ "filter": { "stageId": { "$ne": "LOST" } } }
{ "filter": { "!stageId": "LOST" } }
{ "filter": { "stageId": { "!": "LOST" } } }
Все три варианта найдут записи, у которых стадия отличается от LOST.
Фильтр по заполненности (не пусто / не null)
Отобрать только записи, где поле заполнено, — например контакты с заполненным идентификатором налогоплательщика, чтобы не тянуть всю базу при дедупликации. Сравнение с null через оператор $ne делает эту выборку на стороне Битрикс24:
{ "filter": { "ufCrm_taxId": { "$ne": null } } }
Найдёт только записи, где ufCrm_taxId заполнено. Эквивалентная форма — сравнение с пустой строкой { "$ne": "" }. Работает и для пользовательских (UF) полей, и для стандартных (post, phone, email и др.).
Обратное условие — «поле пусто» — задаётся точным сравнением с null:
{ "filter": { "ufCrm_taxId": null } }
Вместе две выборки дают полное разбиение набора (заполненные + пустые = все).
Важно для GET-запросов. URL не умеет передавать настоящий
null— в строке запроса он превращается в текст"null", и фильтр начинает искать буквальную строку «null» (вернёт мусор). Для «не пусто» в GET передавайте пустое значение, а не словоnull:
- Правильно:
GET /v1/contacts?filter[ufCrm_taxId][$ne]=— поле заполнено- Неправильно:
GET /v1/contacts?filter[ufCrm_taxId][$ne]=null— ищет текст «null», а это другое условиеЛибо используйте
POST /v1/contacts/searchс телом{ "filter": { "ufCrm_taxId": { "$ne": null } } }— в JSON-телеnullпередаётся корректно.
Фильтр по телефону и почте
Поля phone и email у лидов, контактов и компаний ведут себя не так, как остальные. У них две особенности, которые надо знать до того, как писать фильтр.
Значение сравнивается целиком, вместе со знаками. Запись с номером +7 (999) 123-45-67 найдётся только по этой же строке:
{ "filter": { "phone": "+7 (999) 123-45-67" } }
Плюс, пробелы, скобки и дефисы — часть сохранённого значения. Поэтому та же запись не найдётся ни по 79991234567, ни по 89991234567.
В каком виде номер попадёт в базу, решает портал: один и тот же телефон может лежать в ней и как +7 (999) 123-45-67, и как 89991234567. Ваша программа заранее не знает, какой вид выбран, поэтому фильтр по точному значению годится только тогда, когда сохранённая строка уже известна.
У записи может быть несколько номеров, а фильтр видит только первый. Если у контакта записаны рабочий и мобильный телефоны, фильтр найдёт его по рабочему — тому, который приходит в поле phone в ответе. По мобильному фильтр вернёт пустой список.
Найти запись по номеру телефона
Обе особенности снимает Поиск дубликатов — отдельный эндпоинт POST /v1/duplicates/find. Он сравнивает номера по существу, а не по написанию, и проверяет все номера записи, а не только первый:
curl -X POST https://vibecode.bitrix24.tech/v1/duplicates/find \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "phone",
"values": ["+79991234567"]
}'
Запросы +7 (999) 123-45-67, +79991234567 и 89991234567 найдут одну и ту же запись. Номер передаётся целиком: без кода страны или без ведущей «8» он не опознаётся. В ответе приходят ID найденных лидов, контактов и компаний.
Поиск по части значения
Точное сравнение — не единственная форма. Оператор $contains ищет кусок текста внутри сохранённого значения, и для почты это рабочий способ:
{ "filter": { "email": { "$contains": "@example.com" } } }
Найдёт записи с почтой в домене example.com. Для телефона результат зависит от того, где внутри сохранённого номера стоят пробелы и дефисы, и от того, первый это номер записи или нет. Поэтому запись по номеру ищут поиском дубликатов, а не оператором $contains.
Логика ИЛИ
Все условия в объекте filter объединяются логикой И. Логический оператор ИЛИ между разными полями нельзя выразить прямо в filter — попытка передать LOGIC: "OR" или $or возвращает 400 INVALID_FILTER_OPERATOR. Ниже три способа получить тот же результат.
Способ 1: `$in` — несколько значений одного поля
Когда нужно «поле равно A или B или C», используйте оператор $in из таблицы выше:
{
"filter": {
"stageId": { "$in": ["NEW", "WON"] }
}
}
Найдёт сделки в стадии NEW или WON. Оператор работает на любом поле, для которого имеет смысл точное сравнение: assignedById, categoryId, id, sourceId и так далее.
Способ 2: Batch — несколько фильтров одним запросом
Когда условия ИЛИ затрагивают разные поля («сделки в стадии NEW или с суммой больше 100 000»), вынесите каждое условие в отдельный вызов внутри Batch API:
{
"calls": [
{
"id": "by_stage",
"entity": "deals",
"action": "list",
"params": { "filter": { "stageId": "NEW" }, "limit": 200 }
},
{
"id": "by_amount",
"entity": "deals",
"action": "list",
"params": { "filter": { "amount": { "$gte": 100000 } }, "limit": 200 }
}
]
}
Ответ придёт в форме data.results.by_stage и data.results.by_amount — два независимых массива, которые клиент объединяет самостоятельно.
Способ 3: параллельные запросы + клиентская склейка
Когда нужен единый список без дубликатов, выполните вызовы параллельно и объедините результаты по id:
const [a, b] = await Promise.all([
fetch('/v1/deals/search', {
method: 'POST',
headers: { 'X-Api-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({ filter: { stageId: 'NEW' }, limit: 200 }),
}),
fetch('/v1/deals/search', {
method: 'POST',
headers: { 'X-Api-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({ filter: { stageId: 'WON' }, limit: 200 }),
}),
])
const { data: dataA } = await a.json()
const { data: dataB } = await b.json()
const byId = new Map(dataA.map(d => [d.id, d]))
for (const d of dataB) byId.set(d.id, d)
const merged = [...byId.values()]
Map по id устраняет повторы, если запись попадает под оба условия.
Чего делать нельзя
{ "filter": { "LOGIC": "OR", "0": { "stageId": "NEW" }, "1": { "stageId": "WON" } } }
Ответ:
{
"success": false,
"error": {
"code": "INVALID_FILTER_OPERATOR",
"message": "INVALID_FILTER_OPERATOR: 'LOGIC' is not supported. OR/AND logic cannot be expressed in a single filter. For same-field OR use { field: { $in: [v1, v2] } }. For cross-field OR run parallel requests via POST /v1/batch. AND is the default — combine conditions as sibling keys in one filter object."
}
}
Аналогично попытка передать $or возвращает 400 с подсказкой использовать Batch API.
Смешивание синтаксисов
Речь идёт о стилях записи операторов внутри одного фильтра. Две формы передачи самого параметра filter в GET-запросе — скобочную и JSON — смешивать нельзя, см. Как передать фильтр в GET-запросе.
Все три стиля можно комбинировать в одном фильтре:
{
"filter": {
"amount": { "$gte": 50000 },
"!stageId": "LOST",
"createdAt": { ">=": "2026-01-01T00:00:00" }
}
}
Найдёт записи с суммой от 50 000, стадией, отличной от LOST, созданные с начала 2026 года. Здесь все три синтаксиса используются вместе.
Эндпоинт поиска
POST /v1/{entity}/search — полнофункциональный поиск:
curl -X POST -H "X-Api-Key: $KEY" -H "Content-Type: application/json" \
"https://vibecode.bitrix24.tech/v1/deals/search" \
-d '{
"filter": {
"stageId": { "$ne": "LOST" },
"amount": { "$gte": 100000 }
},
"sort": { "amount": "desc" },
"limit": 200
}'
| Параметр | Тип | Описание |
|---|---|---|
filter |
object | Условия фильтрации |
sort |
string, object или array | Сортировка. Принимаются три формы: строка ("id" / "-amount" / "id,-createdAt"), объект ({ id: "asc", amount: "desc" } — ключи в порядке вставки) или массив (["id", "-amount"]). Вместо asc/desc принимаются 1/-1. Некорректный тип возвращает 400 INVALID_SORT_TYPE, неизвестное направление — INVALID_SORT_DIRECTION. |
limit |
number | Количество записей (по умолчанию 50, максимум 5000) |
offset |
number | Пропустить N записей |
autoWindow |
boolean | false — отключить разбивку по дате |
Фильтр передан не объектом
filter — это объект условий. Строка, число, булево значение или массив вместо него
отклоняются с кодом INVALID_FILTER_SHAPE, и в сообщении сказано, что именно пришло.
POST /v1/tasks/search { "filter": [{ "responsibleId": 1 }] } → 400 INVALID_FILTER_SHAPE
POST /v1/tasks/search { "filter": { "responsibleId": 1 } } → 200
В строке запроса действует другое правило: условия пишутся скобочной формой
(?filter[responsibleId]=1), а filter, закодированный в JSON одной строкой,
разбирается и применяется — значение, которое не является ни той, ни другой формой,
отклоняется с кодом INVALID_FILTER.
Раньше такие значения молча терялись: Битрикс24 получал вызов вообще без отбора и отвечал
200 со всей коллекцией — то же самое, что и с неизвестным именем поля, только на уровне
формы, а не имени.
Неизвестное имя поля в фильтре
Имя поля, которого нет у сущности, отклоняется до вызова Битрикс24 — ответ 400 с кодом
UNKNOWN_FILTER_FIELD и списком доступных имён в сообщении. Сверяться нужно с этим списком,
а не с GET /v1/{entity}/fields: тот отвечает на другой вопрос — какие поля у сущности
есть, — и на части сущностей он шире, потому что дополняет схему живым ответом Битрикс24, а
фильтровать по такому полю нельзя. Сверх перечисленного принимаются пользовательские поля, а у сущностей, где он объявлен, — ключ id.
Служебные имена языка — __proto__, constructor, prototype, toString и прочие служебные
имена объекта — отклоняются тем же кодом
400 UNKNOWN_FILTER_FIELD, как любое другое неизвестное поле. Это важно, когда имя поля фильтра
собирается из пользовательского ввода: такой ключ не проходит и выборка не остаётся
неотфильтрованной. Правило действует на всех сущностях и на агрегации.
Пользовательское поле указывается тем же именем, под которым оно стоит в схеме сущности:
ufCrmProjectCode у сделок, UF_CRM_1698325419 у реквизитов. На другое написание того же поля
у сделок, контактов и элементов смарт-процессов приходит 400 UNKNOWN_FILTER_FIELD, а у
остальных сущностей имя отбрасывается: условие не применяется и приходит вся коллекция. Формат
имён и значений — Пользовательские поля (UF).
Исключение — реквизиты: там принимаются оба написания одного поля, UF_CRM_1698325419 и
ufCrm_1698325419, и отбор применяется одинаково. Раньше второе написание отбрасывалось.
Написание с подчёркиванием перед буквой (ufCrm_taxId) остаётся неизвестным именем: Битрикс24
такого имени не даёт, вывести из него настоящее нельзя, и условие по-прежнему теряется.
Проверка идёт по имени, а не по значению: операторы, диапазоны, $in/$nin и логика И
работают без изменений. У смарт-процессов сверх перечисленного принимаются динамические связи
parentId<N>.
Почему это важно. Битрикс24 неизвестный ключ фильтра не отклоняет, а молча
выбрасывает и отвечает 200 со ВСЕЙ коллекцией. То есть опечатка в имени поля выглядела как
успешный запрос с неправдоподобно большим результатом — а не как ошибка. Отказ до вызова
превращает эту ситуацию в явную.
Где проверка ещё НЕ включена. У части сущностей схема полей заведомо уже настоящего контракта Битрикс24, поэтому включать проверку нельзя — она отклонила бы работающее поле. Там поведение прежнее: неизвестное имя уходит в Битрикс24 и молча теряется. Узнать, включена ли проверка у конкретной сущности, можно одним запросом: отправьте фильтр по заведомо несуществующему имени и посмотрите на код ответа.
Отдельный случай — метод вообще без фильтра. У нескольких сущностей метод Битрикс24 не
принимает фильтр ни в каком виде (он читает только именованные аргументы). Там отклоняется
любой ключ фильтра, с кодом UNSUPPORTED_FILTER. В сообщении перечислены параметры, которые
метод принимает.
Коды ошибок
| HTTP | Код | Условие |
|---|---|---|
| 400 | INVALID_FILTER |
Значение filter не является ни скобочной записью, ни JSON-объектом — либо в одном запросе смешаны обе формы (см. Как передать фильтр в GET-запросе) |
| 400 | INVALID_FILTER_OPERATOR |
Неизвестный оператор в значении поля или попытка передать LOGIC / $or |
| 400 | INVALID_FILTER_SHAPE |
filter в теле запроса передан не объектом — строкой, числом или массивом |
| 400 | INVALID_FILTER_FIELD |
Имя поля начинается с родного префикса Битрикс24 @ (IN) или !@ (NOT IN) — используйте операторы $in / $nin |
| 400 | UNKNOWN_FILTER_FIELD |
Поле отсутствует у сущности — см. Неизвестное имя поля в фильтре |
| 400 | UNSUPPORTED_FILTER |
Метод Битрикс24 у этой сущности не принимает фильтр. В сообщении перечислены принимаемые параметры |
| 400 | INVALID_SORT_TYPE |
sort не строка, не объект и не массив |
| 400 | INVALID_SORT_DIRECTION |
Направление сортировки не asc / desc / 1 / -1 |
| 400 | UNSTABLE_OFFSET_PAGINATION |
offset > 0 при широком диапазоне дат (см. Постраничный вывод) |
| — | WINDOWED_SEARCH_FAILED |
Больше не возвращается: при полном отказе авто-окон возвращается реальный код Битрикс24 — UNKNOWN_FILTER_FIELD / INVALID_PARAMS / BITRIX_ACCESS_DENIED / RATE_LIMITED / BITRIX_UNAVAILABLE / BITRIX_TIMEOUT (503) |
Полный список общих ошибок API — Коды ошибок.
Примеры по сущностям
Сделки — по стадии и сумме
{
"filter": {
"stageId": "NEW",
"amount": { "$gte": 100000 }
},
"sort": { "createdAt": "desc" }
}
Найдёт сделки в стадии NEW с суммой от 100 000, отсортированные по дате создания (новые первыми).
Контакты — по телефону
Телефон фильтром не ищут: он сравнивается со всей сохранённой строкой и только по первому номеру записи — см. Фильтр по телефону и почте. Номер ищут отдельным эндпоинтом POST /v1/duplicates/find — Поиск дубликатов:
curl -X POST https://vibecode.bitrix24.tech/v1/duplicates/find \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "phone",
"values": ["+79161234567"],
"entityType": "contact"
}'
Вернёт ID контактов с этим номером в любом его написании.
Задачи — незакрытые
{
"filter": {
"status": { "$ne": 5 }
}
}
Найдёт все задачи со статусом, отличным от 5 (завершена). Статусы: 2 = ждёт выполнения, 3 = выполняется, 4 = ожидает контроля, 5 = завершена, 6 = отложена.
Лиды — за период
{
"filter": {
"createdAt": { "$gte": "2026-01-01T00:00:00", "$lte": "2026-03-31T23:59:59" }
}
}
Найдёт лиды, созданные в первом квартале 2026 года.
Календарные события
{
"filter": {
"dateFrom": { "$gte": "2026-04-01T00:00:00" }
}
}
Найдёт события с датой начала от 1 апреля 2026. Для calendar-events обязательны параметры type и ownerId в URL: /v1/calendar-events?type=user&ownerId=1.
Фильтрация в Batch
Фильтры работают и в Batch API:
{
"calls": [
{
"id": "new_deals",
"entity": "deals",
"action": "list",
"params": {
"filter": { "stageId": "NEW" }
}
},
{
"id": "search_contacts",
"entity": "contacts",
"action": "search",
"params": {
"filter": { "email": { "$contains": "@example.com" } }
}
}
]
}
Одним запросом получит список сделок в стадии NEW и найдёт контакты с адресом в домене example.com.
Постраничный вывод
Три готовых способа — выбирайте по задаче. Для отдельного ответа границей цикла служит meta.hasMore, а не арифметика от meta.total: признак «есть ещё» выводится из полноты страницы. Само поле meta.total необязательное — его может не быть, если количество не заказывалось. Ни один способ не создаёт неизменяемый снимок коллекции и не обещает полный обход при изменениях данных или прав доступа.
Получить весь результат сразу
Подходит для отчётов и выгрузок, когда под фильтр попадает не больше 5000 записей. Укажите limit до 5000 — сервис вернёт весь подходящий результат одним ответом. Коллекция в десятки тысяч записей читается следующим способом, курсором — Выгрузка большой коллекции.
Одного ответа хватает не всегда. Когда поиск разбивает диапазон дат на окна, выдача упирается либо в потолок в 5000 записей, либо в размер одного чтения окна. Признак этого приходит в meta.warnings кодом WINDOW_TRUNCATED — разбор в неполной выдаче.
const res = await fetch('/v1/deals/search', {
method: 'POST',
headers: { 'X-Api-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({
filter: { closedAt: { $gte: '2026-03-22T00:00:00Z', $lte: '2026-04-22T00:00:00Z' } },
limit: 5000,
}),
})
const { data, meta } = await res.json()
// data — все записи; meta.hasMore говорит, осталось ли что-то за пределами limit
Идти по курсору `nextAfterId`
Этот способ применим к методам, ответы которых содержат meta.nextAfterId, и является основным для больших обходов. Отсортируйте по id по возрастанию, отключите точный подсчёт через withTotal: false и передавайте meta.nextAfterId предыдущего ответа обратно в фильтр >id. Запрашивайте через select только нужные поля и обязательно включайте id. Курсор не зависит от смещения и не дорожает к концу коллекции. Каждый вызов читает одну короткую страницу, а прерванный обход продолжается с последнего meta.nextAfterId, не начиная сначала.
Курсор доступен у сделок, лидов, контактов, компаний, предложений и элементов смарт-процессов. У остальных сущностей meta.nextAfterId в ответе нет — для них подходят два других способа.
let after = null
while (true) {
const res = await fetch('/v1/deals/search', {
method: 'POST',
headers: { 'X-Api-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({
filter: { ...(after ? { '>id': after } : {}) },
sort: 'id',
select: ['id', 'title'],
limit: 50,
withTotal: false,
}),
})
const { data, meta } = await res.json()
for (const deal of data) process(deal)
if (!meta.hasMore) break
if (!meta.nextAfterId) {
throw new Error('В ответе нет meta.nextAfterId при meta.hasMore=true')
}
after = meta.nextAfterId
}
Внутренний запрос следует рекомендованной схеме Битрикс24: start=-1, order=ID ASC, фильтр ID больше последнего полученного идентификатора. Клиент не передаёт start в тело запроса к этому эндпоинту — у подкоманд пакета он принимается, см. Пакетные запросы. Вайбкод применяет его только к методам, для которых подтверждена совместная работа фильтра по id и start=-1. Для остальных методов сервис сохраняет корректность вызова и может не применить режим без подсчёта.
withTotal: false убирает meta.total и, в поддерживаемом режиме, отдельный COUNT. Не обходите коллекцию ради подсчёта. Если точная цифра нужна, сначала прочитайте operations.search.paginationStability.counting сущности в GET /v1/guide. Когда указание содержит путь агрегации, используйте функцию count. Когда указание есть, но пути нет, дешёвого точного подсчёта нет — читайте meta.total, только когда поле пришло, а обход ограничивайте по meta.hasMore. Если весь блок counting отсутствует вместе с общей операцией поиска, не угадывайте путь агрегации: перейдите по указателю на документацию сущности или домена из того же руководства и используйте только явно описанную операцию счёта.
Обход предполагает, что записи не удаляются, а права доступа не меняются до его завершения. Если любое из условий нарушено, часть записей может быть пропущена. API Вайбкод не обещает полный обход как инвариант клиента.
Идти по страницам вручную
Подходит, когда нужно обрабатывать записи порциями (например, импорт по 50 штук с сохранением прогресса). Добавьте autoWindow: false, сортировку по id и увеличивайте offset на каждой итерации. offset считается по записям, а meta.hasMore учитывает уже пропущенное, поэтому цикл останавливается по нему.
let offset = 0
while (true) {
const res = await fetch('/v1/deals/search', {
method: 'POST',
headers: { 'X-Api-Key': key, 'Content-Type': 'application/json' },
body: JSON.stringify({
filter: { closedAt: { $gte: '2026-03-22T00:00:00Z', $lte: '2026-04-22T00:00:00Z' } },
sort: 'id',
limit: 50,
offset,
autoWindow: false,
}),
})
const { data, meta } = await res.json()
if (data.length === 0) break // защита от бесконечного цикла на пустой странице
for (const deal of data) process(deal)
offset += data.length
if (!meta.hasMore) break
}
Пустая страница в середине обхода — отдельный случай. Когда на запрошенной позиции доступно меньше строк, чем пропускает offset, страница выходит пустой, хотя записи под фильтр ещё есть. Ответ тогда несёт meta.warnings с кодом OFFSET_BEYOND_FETCHED_PAGE и полем field: "offset". Проверяйте этот код перед выходом из цикла: увеличьте limit, сузьте фильтр или перейдите на курсор.
Чего делать нельзя
Не запускайте параллельные запросы с разным offset на одном фильтре — сервис вернёт 400 UNSTABLE_OFFSET_PAGINATION. Выберите один из трёх способов выше.
Не обходите коллекцию постранично, чтобы узнать количество записей. Пролистать пять тысяч сделок ради цифры «4863» — это сто вызовов вместо одного. Если точная цифра нужна, используйте функцию count только по пути, явно указанному в operations.search.paginationStability.counting. Когда весь блок отсутствует вместе с общей операцией поиска, следуйте указателю на документацию сущности или домена из GET /v1/guide и не угадывайте путь.