Для AI-агентов: markdown этой страницы — /docs-content/filtering.md индекс документации — /llms.txt
Фильтрация и поиск
Три синтаксиса фильтрации для API сущностей. Все стили можно смешивать в одном запросе.
Фильтрация работает в двух местах:
GET /v1/{entity}?filter[field]=value— параметр URL для спискаPOST /v1/{entity}/search— тело запроса{ "filter": { ... } }
Быстрый переход: Как передать фильтр в GET-запросе · Фильтр в curl · Фильтр по телефону и почте · Логика ИЛИ · Эндпоинт поиска · Постраничный вывод · Коды ошибок
Как передать фильтр в 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.
Фильтр в curl
Утилита curl читает квадратные и фигурные скобки в адресе как шаблон для перебора адресов. Без флага -g (полная форма --globoff) обе формы фильтра ломаются ещё до отправки:
- со скобочной записью
curlзавершается с кодом3и сообщениемbad range in URL, запрос не уходит, а с флагом-sсообщения нет вовсе - у JSON-формы пропадают фигурные скобки, и API отвечает
400 INVALID_FILTER. Если условий несколько,curlотправляет по отдельному запросу на каждое.
Передавайте -g и берите адрес в одинарные кавычки — в двойных оболочка подставит вместо $gte пустую переменную:
curl -g 'https://vibecode.bitrix24.tech/v1/deals?filter[stageId]=NEW&filter[amount][$gte]=50000' \
-H "X-Api-Key: YOUR_API_KEY"
В fetch и других HTTP-клиентах скобки шаблоном не считаются, флаг там не нужен.
Синтаксис 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" } } }
Найдёт записи, обновлённые с указанного момента.
Часовой пояс в значении фильтра
ISO 8601 со знаком Z или числовым смещением передаётся в Битрикс24 целиком, включая дробные секунды. Значения 2026-10-05T19:20:22.000Z и 2026-10-05T22:20:22.000+03:00 задают один момент. Можно передать updatedAt из ответа в $gte без ручной поправки часов. В query string знак + кодируйте как %2B.
Строка без зоны, например 2026-10-05T19:20:22, сохраняет прежнюю локальную семантику Битрикс24: она зависит от настроек пользователя, от имени которого выполняется запрос. Для личного ключа это владелец ключа: его идентификатор приходит в data.owner.userId ответа GET /v1/me, пояс — в timeZone ответа GET /v1/users/:id. Календарная дата YYYY-MM-DD передаётся без изменений. Для абсолютного момента используйте явно заданную зону.
Для диапазона с явно заданными зонами автоматическое разбиение на окна сохраняет абсолютные моменты границ. Границы без зоны остаются без зоны в каждом окне, поэтому Битрикс24 читает их в локальном времени, как и в запросе без разбиения. При смешении локальной границы и границы с зоной фильтр выполняется без разбиения, поскольку для расчёта общих окон нужен пояс аккаунта. Ответ поиска содержит предупреждение DATE_WINDOWING_SKIPPED в meta.warnings: на методах Битрикс24 с фильтрацией после пагинации выборка может оказаться неполной. Предупреждение сообщает о риске, а не о доказанной потере строк; оно появляется при любой смешанной паре, поскольку без пояса аккаунта нельзя точно вычислить даже ширину диапазона. Для разбиения задайте явную зону на обеих границах. При явном autoWindow: false или отключённом для сущности автоматическом разбиении это предупреждение не добавляется.
Заголовок X-Vibe-Timezone, которым клиент объявляет свой пояс, действует только на запись. На фильтр он не влияет. При записи наивной строки с заголовком и поиске по той же наивной строке моменты могут различаться: передайте в фильтр значение с зоной из ответа. Если интеграция компенсировала прежний срез зоны вручную, уберите эту компенсацию.
Границы диапазона задаются операторами $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 } }
Вместе две выборки дают полное разбиение набора (заполненные + пустые = все).
Ноль в числовом поле считается пустым значением. Условие { "amount": null } вернёт сделки с суммой 0, а { "amount": { "$ne": null } } их не вернёт. Точное сравнение с нулём { "amount": 0 } отбирает те же записи, что и сравнение с null, поэтому отличить ноль от незаполненного поля фильтром нельзя. Правило одинаково для стандартных числовых полей и для пользовательских полей типа «число». Если для задачи ноль и отсутствие значения — разные состояния, отбирайте записи условием «поле пусто» и разделяйте их по значению поля в ответе.
Важно для GET-запросов. URL не умеет передавать настоящий
null— в строке запроса он превращается в текст"null", и фильтр начинает искать буквальную строку «null» (вернёт мусор). В GET вместоnullпередавайте пустое значение. Оно работает в обе стороны:
- Правильно:
GET /v1/contacts?filter[ufCrm_taxId]=— поле пусто, то же, что{ "ufCrm_taxId": 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). Некорректный тип — не число и не строковая запись числа — отклоняется до вызова Битрикс24: 400 INVALID_LIMIT |
offset |
number | Пропустить N записей |
select |
string | string[] | Выборка полей. Некорректный тип возвращает 400 INVALID_SELECT_TYPE, незнакомое имя поля — UNKNOWN_SELECT_FIELD |
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. В сообщении перечислены параметры, которые
метод принимает.
Одно поле — одно написание
У поля бывает несколько принимаемых написаний: имя из схемы и родное имя Битрикс24
(amount и OPPORTUNITY у сделок), верхний и нижний регистр. Правило одно: два условия,
которые сводятся к одному имени фильтра Битрикс24, отклоняются с кодом
400 INVALID_DUPLICATE_FILTER_FIELD. В сообщении названы оба условия и общее имя, к которому
они свелись.
filter[amount]=5000&filter[OPPORTUNITY]=9000 → 400 INVALID_DUPLICATE_FILTER_FIELD
То же правило действует на два написания одного оператора ({ "amount": { "$gt": 1, ">": 2 } })
и на пару синонимов, объявленных у сущности отдельными именами: у лидов это
amount/opportunity, stageId/statusId, currency/currencyId.
Сюда же попадают парные имена полей даты — updatedAt с updatedTime и createdAt с
createdTime. У сущности объявлено одно из двух, а второе принимается как его псевдоним, поэтому
оба написания дают одно имя на проводе:
?filter[updatedAt]=2026-01-01T00:00:00&filter[updatedTime]=2026-02-01T00:00:00
→ 400 INVALID_DUPLICATE_FILTER_FIELD
Здесь работает то же общее правило: считается результат, а не написание. Два условия с ОДНИМ
оператором (или два точных значения, как выше) садятся на один ключ и отклоняются, а
?filter[updatedAt][$gte]=…&filter[updatedTime][$lte]=… даёт разные ключи >= и <=, то есть
обычный диапазон — он работает и не отклоняется. Путать эти два случая легко, поэтому диапазон
надёжнее задавать одним написанием: ?filter[>=updatedAt]=…&filter[<=updatedAt]=….
Раньше такой запрос отвечал 200, но применял только одно из двух условий — какое именно,
решал порядок ключей в запросе. Выборка выглядела отфильтрованной, хотя половина отбора не
применялась.
Решает результат, а не написание. Отказ приходит там, где два условия действительно занимают один и тот же ключ на проводе. Поэтому:
- Пользовательские поля. Пара «
UF_-форма + camelCase-написание» складывается в одно имя только там, где платформа сама переводит camelCase вUF_, — у сущностей со старым стилем именования (например задачи), и только для чисто буквенного написания (ufCrmProjectCode). Там она отклоняется. Написание с цифровым хвостом (ufCrm_1698325419) уезжает в Битрикс24 как есть, поэтому в паре сUF_CRM_1698325419даёт два разных имени и отказа НЕ вызывает — второе условие при этом Битрикс24 молча отбрасывает, как и до этой проверки. Исключение — реквизиты: там цифровое написание тоже переводится, и пара отклоняется. У сущностей с camelCase-именами и у элементов CRM (сделки, контакты, компании, лиды, счёты, предложения, элементы смарт-процессов)UF_-форма и camelCase — РАЗНЫЕ имена на проводе: оба написания уезжают в Битрикс24 как есть, и отказа нет. - Два оператора, дающие один ключ.
$ne(«не равно») и$nin(«не входит в список») оба превращаются в префикс!и занимают один ключ — у всех сущностей, кроме элементов CRM (у тех$ninдаёт отдельный префикс!@, и пара проходит). Граница здесь ДРУГАЯ, чем у пользовательских полей выше: под неё попадают и сущности с camelCase-именами — товары каталога, почтовые ящики, смарт-процессы. Передавайте одно из двух:{ "responsibleId": { "$nin": [1, 2] } }покрывает и одиночное исключение.
Что по-прежнему можно. Операторы, дающие РАЗНЫЕ ключи, работают как раньше:
{ "amount": { "$gte": 1000, "$lte": 5000 } } и ?filter[>=amount]=1000&filter[<=amount]=5000
— диапазон, а не дубль. Одно условие на поле — тоже без изменений.
⚠️ «Одно написание поля» само по себе не гарантирует прежнего поведения: значение имеет число
условий, которые из него получаются, и на каких сущностях они дают один ключ. Пара $ne + $nin
на одном поле — это одно написание, но два условия, и по правилу выше она отклоняется везде,
кроме элементов CRM: POST /v1/tasks/search {"filter":{"responsibleId":{"$ne":1,"$nin":[2,3]}}}
даёт 400, а тот же фильтр у сделок или контактов — 200, потому что там $nin получает
отдельный префикс !@ и ключи разные. У элементов CRM лишнее условие Битрикс24, как и прежде,
молча отбрасывает, поэтому дубль NOT-условий там всё равно лучше не отправлять.
Где проверки НЕТ. Правило работает на сущностях API — там, где фильтр разбирает стандартный код
списков, поиска, агрегации и пакетных вызовов. Отдельные маршруты со своим форматом фильтра
разбирают его собственным кодом и под проверку не попадают: там пара написаний по-прежнему
отвечает 200 с молча потерянным условием. Это, среди прочих, GET /v1/requisite-links с
POST /v1/requisite-links/search, статистика звонков, списки, комментарии задач и открытые линии —
перечень не закрытый, потому что такой маршрут добавляется независимо от этой проверки.
Ориентир простой: если у маршрута свой набор ключей фильтра, описанный на его собственной странице, а не общая схема полей сущности, — рассчитывайте, что дубль написаний там не отклонят. Присылайте одно написание сами.
Коды ошибок
| 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 | INVALID_DUPLICATE_FILTER_FIELD |
Два написания одного поля или одного оператора в одном фильтре — см. Одно поле — одно написание |
| 400 | UNKNOWN_FILTER_FIELD |
Поле отсутствует у сущности — см. Неизвестное имя поля в фильтре |
| 400 | UNSUPPORTED_FILTER |
Метод Битрикс24 у этой сущности не принимает фильтр. В сообщении перечислены принимаемые параметры |
| 400 | INVALID_SORT_TYPE |
sort не строка, не объект и не массив |
| 400 | INVALID_SORT_DIRECTION |
Направление сортировки не asc / desc / 1 / -1 |
| 400 | INVALID_LIMIT |
limit не число и не строковая запись числа. В сообщении назван фактический тип пришедшего значения. null, пустая строка и пустой перечень означают «параметр не задан» и ошибкой не считаются |
| 400 | INVALID_SELECT_TYPE |
select не строка и не массив строк — например 999, true, {} или [1,2]. Незнакомое ИМЯ поля даёт отдельный код UNKNOWN_SELECT_FIELD |
| 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 и не угадывайте путь.