[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-filtering":3,"docs-tabs-filtering":6},{"content":4,"lastmod":5},"# Фильтрация и поиск\n\nТри синтаксиса фильтрации для API сущностей. Все стили можно **смешивать** в одном запросе.\n\n> Фильтрация работает в двух местах:\n> - `GET \u002Fv1\u002F{entity}?filter[field]=value` — параметр URL для списка\n> - `POST \u002Fv1\u002F{entity}\u002Fsearch` — тело запроса `{ \"filter\": { ... } }`\n\n**Быстрый переход:** [Фильтр по телефону и почте](#фильтр-по-телефону-и-почте) · [Логика ИЛИ](#логика-или) · [Эндпоинт поиска](#эндпоинт-поиска) · [Постраничный вывод](#постраничный-вывод) · [Коды ошибок](#коды-ошибок)\n\n## Синтаксис 1: операторы со знаком `$`\n\nОператоры с префиксом `$` внутри объекта поля.\n\n```json\n{\n  \"filter\": {\n    \"amount\": { \"$gte\": 50000 },\n    \"stageId\": { \"$ne\": \"LOST\" },\n    \"createdAt\": { \"$gte\": \"2026-01-01T00:00:00\" }\n  }\n}\n```\n\nНайдёт записи с суммой от 50 000, стадией, отличной от LOST, созданные с начала 2026 года.\n\n### Доступные операторы\n\n| Оператор | Значение | Пример |\n|----------|---------|--------|\n| `$gt` | > (больше) | `{ \"amount\": { \"$gt\": 10000 } }` |\n| `$gte` | >= (больше или равно) | `{ \"amount\": { \"$gte\": 50000 } }` |\n| `$lt` | \u003C (меньше) | `{ \"amount\": { \"$lt\": 100000 } }` |\n| `$lte` | \u003C= (меньше или равно) | `{ \"amount\": { \"$lte\": 200000 } }` |\n| `$ne` | != (не равно) | `{ \"stageId\": { \"$ne\": \"LOST\" } }` |\n| `$contains` | поиск по подстроке | `{ \"title\": { \"$contains\": \"поставка\" } }` |\n| `$in` | входит в массив (IN) | `{ \"stageId\": { \"$in\": [\"NEW\", \"WON\"] } }` |\n| `$nin` | НЕ входит в массив (NOT IN) | `{ \"categoryId\": { \"$nin\": [1, 3] } }` |\n\nТочное совпадение задаётся значением напрямую, без оператора: `{ \"stageId\": \"NEW\" }`.\n\n> **Исключить набор значений.** «Поле НЕ входит в список» задаётся оператором `$nin`: `{ \"categoryId\": { \"$nin\": [1, 3] } }` вернёт сделки всех направлений, кроме 1 и 3. Родные префиксы Битрикс24 `@` (IN) и `!@` (NOT IN) в имени поля (`{ \"@categoryId\": [...] }`, `{ \"!@categoryId\": [...] }`) НЕ поддерживаются — используйте операторы `$in` \u002F `$nin`.\n\n### Комбинирование\n\nНесколько условий на одном поле объединяются логикой И. Условия на разные поля — тоже логикой И. Для ИЛИ-логики см. раздел [Логика ИЛИ](#логика-или) ниже.\n\n```json\n{\n  \"filter\": {\n    \"amount\": { \"$gte\": 50000, \"$lte\": 200000 },\n    \"stageId\": { \"$ne\": \"LOST\" }\n  }\n}\n```\n\nНайдёт записи с суммой от 50 000 до 200 000 и стадией, отличной от LOST.\n\n## Синтаксис 2: префикс в имени поля\n\nОператор как часть имени поля. Форма, которую напрямую понимает Битрикс24.\n\n```json\n{\n  \"filter\": {\n    \">=amount\": 50000,\n    \"\u003C=amount\": 200000,\n    \"!stageId\": \"LOST\"\n  }\n}\n```\n\nНайдёт записи с суммой от 50 000 до 200 000 и стадией, отличной от LOST.\n\n### Операторы\n\n| Префикс | Значение |\n|---------|---------|\n| `>=` | больше или равно |\n| `>` | больше |\n| `\u003C=` | меньше или равно |\n| `\u003C` | меньше |\n| `!` | не равно |\n| `%` | подстрока |\n\n## Синтаксис 3: оператор как ключ объекта\n\nОператор как ключ вложенного объекта. Та же форма, что в синтаксисе 2, но с разделением имени поля и условия.\n\n```json\n{\n  \"filter\": {\n    \"amount\": { \">=\": 50000 },\n    \"stageId\": { \"!\": \"LOST\" }\n  }\n}\n```\n\nНайдёт записи с суммой от 50 000 и стадией, отличной от LOST.\n\n## Фильтрация по дате\n\nПоля даты (`createdAt`, `updatedAt`, `closedAt`, `beginDate` и др.) принимают строки **ISO 8601**:\n\n```json\n{\n  \"filter\": {\n    \"createdAt\": { \"$gte\": \"2026-01-01T00:00:00\" },\n    \"closedAt\": { \"$lte\": \"2026-03-31T23:59:59\" }\n  }\n}\n```\n\nНайдёт записи, созданные с начала 2026 года и закрытые до конца марта.\n\n### Примеры\n\n```json\n{ \"filter\": { \">=createdAt\": \"2026-03-01T00:00:00\" } }\n```\n\nНайдёт записи, созданные с 1 марта 2026.\n\n```json\n{ \"filter\": { \"updatedAt\": { \"$gte\": \"2026-05-01T00:00:00\" } } }\n```\n\nНайдёт записи, обновлённые с указанного момента.\n\nГраницы диапазона задаются операторами `$gte`\u002F`$lte` — или эквивалентными `>=`\u002F`\u003C=` из синтаксисов выше. Ключи `from`\u002F`to` в значении поля не поддерживаются и вернут `400 INVALID_FILTER_OPERATOR`:\n\n```json\n{ \"filter\": { \"createdAt\": { \"$gte\": \"2026-06-01T00:00:00\", \"$lte\": \"2026-06-30T23:59:59\" } } }\n```\n\n## NOT-фильтры\n\nИсключение значений:\n\n```json\n{ \"filter\": { \"stageId\": { \"$ne\": \"LOST\" } } }\n```\n\n```json\n{ \"filter\": { \"!stageId\": \"LOST\" } }\n```\n\n```json\n{ \"filter\": { \"stageId\": { \"!\": \"LOST\" } } }\n```\n\nВсе три варианта найдут записи, у которых стадия отличается от LOST.\n\n## Фильтр по заполненности (не пусто \u002F не null)\n\nЧастый кейс — отобрать только записи, где поле **заполнено**: например, контакты с указанным ИНН, чтобы не тянуть всю базу при дедупликации. Сравнение с `null` через оператор `$ne` делает эту выборку на стороне Битрикс24:\n\n```json\n{ \"filter\": { \"ufCrm_inn\": { \"$ne\": null } } }\n```\n\nНайдёт только записи, где `ufCrm_inn` заполнено. Эквивалентная форма — сравнение с пустой строкой `{ \"$ne\": \"\" }`. Работает и для пользовательских (UF) полей, и для стандартных (`post`, `phone`, `email` и др.).\n\nОбратное условие — «поле пусто» — задаётся точным сравнением с `null`:\n\n```json\n{ \"filter\": { \"ufCrm_inn\": null } }\n```\n\nВместе две выборки дают полное разбиение набора (заполненные + пустые = все).\n\n> **Важно для GET-запросов.** URL не умеет передавать настоящий `null` — в строке запроса он превращается в текст `\"null\"`, и фильтр начинает искать буквальную строку «null» (вернёт мусор). Для «не пусто» в GET передавайте **пустое значение**, а не слово `null`:\n>\n> - ✅ `GET \u002Fv1\u002Fcontacts?filter[ufCrm_inn][$ne]=` — поле заполнено\n> - ❌ `GET \u002Fv1\u002Fcontacts?filter[ufCrm_inn][$ne]=null` — ищет текст «null», это НЕ то же самое\n>\n> Либо используйте `POST \u002Fv1\u002Fcontacts\u002Fsearch` с телом `{ \"filter\": { \"ufCrm_inn\": { \"$ne\": null } } }` — в JSON-теле `null` передаётся корректно.\n\n## Фильтр по телефону и почте\n\nПоля `phone` и `email` у лидов, контактов и компаний ведут себя не так, как остальные. У них две особенности, которые надо знать до того, как писать фильтр.\n\n**Значение сравнивается целиком, вместе со знаками.** Запись с номером `+7 (999) 123-45-67` найдётся только по этой же строке:\n\n```json\n{ \"filter\": { \"phone\": \"+7 (999) 123-45-67\" } }\n```\n\nПлюс, пробелы, скобки и дефисы — часть сохранённого значения. Поэтому та же запись не найдётся ни по `79991234567`, ни по `89991234567`.\n\nВ каком виде номер попадёт в базу, решает портал: один и тот же телефон может лежать в ней и как `+7 (999) 123-45-67`, и как `89991234567`. Ваша программа заранее не знает, какой вид выбран, поэтому фильтр по точному значению годится только тогда, когда сохранённая строка уже известна.\n\n**У записи может быть несколько номеров, а фильтр видит только первый.** Если у контакта записаны рабочий и мобильный телефоны, фильтр найдёт его по рабочему — тому, который приходит в поле `phone` в ответе. По мобильному фильтр вернёт пустой список.\n\n### Найти запись по номеру телефона\n\nОбе особенности снимает [Поиск дубликатов](.\u002Fduplicates.md) — отдельный эндпоинт `POST \u002Fv1\u002Fduplicates\u002Ffind`. Он сравнивает номера по существу, а не по написанию, и проверяет все номера записи, а не только первый:\n\n```bash\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fduplicates\u002Ffind \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"type\": \"phone\",\n    \"values\": [\"+79991234567\"]\n  }'\n```\n\nЗапросы `+7 (999) 123-45-67`, `+79991234567` и `89991234567` найдут одну и ту же запись. Номер передаётся целиком: без кода страны или без ведущей «8» он не опознаётся. В ответе приходят ID найденных лидов, контактов и компаний.\n\n### Поиск по части значения\n\nТочное сравнение — не единственная форма. Оператор `$contains` ищет кусок текста внутри сохранённого значения, и для почты это рабочий способ:\n\n```json\n{ \"filter\": { \"email\": { \"$contains\": \"@example.com\" } } }\n```\n\nНайдёт записи с почтой в домене `example.com`. Для телефона результат зависит от того, где внутри сохранённого номера стоят пробелы и дефисы, и от того, первый это номер записи или нет. Поэтому запись по номеру ищут поиском дубликатов, а не оператором `$contains`.\n\n## Логика ИЛИ\n\nВсе условия в объекте `filter` объединяются логикой И. Логический оператор ИЛИ между разными полями нельзя выразить прямо в `filter` — попытка передать `LOGIC: \"OR\"` или `$or` возвращает `400 INVALID_FILTER_OPERATOR`. Ниже три способа получить тот же результат.\n\n### Способ 1: `$in` — несколько значений одного поля\n\nКогда нужно «поле равно A **или** B **или** C», используйте оператор `$in` из таблицы выше:\n\n```json\n{\n  \"filter\": {\n    \"stageId\": { \"$in\": [\"NEW\", \"WON\"] }\n  }\n}\n```\n\nНайдёт сделки в стадии NEW или WON. Оператор работает на любом поле, для которого имеет смысл точное сравнение: `assignedById`, `categoryId`, `id`, `sourceId` и так далее.\n\n### Способ 2: Batch — несколько фильтров одним запросом\n\nКогда условия ИЛИ затрагивают разные поля («сделки в стадии NEW **или** с суммой больше 100 000»), вынесите каждое условие в отдельный вызов внутри [Batch API](.\u002Fbatch.md):\n\n```json\n{\n  \"calls\": [\n    {\n      \"id\": \"by_stage\",\n      \"entity\": \"deals\",\n      \"action\": \"list\",\n      \"params\": { \"filter\": { \"stageId\": \"NEW\" }, \"limit\": 200 }\n    },\n    {\n      \"id\": \"by_amount\",\n      \"entity\": \"deals\",\n      \"action\": \"list\",\n      \"params\": { \"filter\": { \"amount\": { \"$gte\": 100000 } }, \"limit\": 200 }\n    }\n  ]\n}\n```\n\nОтвет придёт в форме `data.results.by_stage` и `data.results.by_amount` — два независимых массива, которые клиент объединяет самостоятельно.\n\n### Способ 3: параллельные запросы + клиентская склейка\n\nКогда нужен единый список без дубликатов, выполните вызовы параллельно и объедините результаты по `id`:\n\n```js\nconst [a, b] = await Promise.all([\n  fetch('\u002Fv1\u002Fdeals\u002Fsearch', {\n    method: 'POST',\n    headers: { 'X-Api-Key': key, 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ filter: { stageId: 'NEW' }, limit: 200 }),\n  }),\n  fetch('\u002Fv1\u002Fdeals\u002Fsearch', {\n    method: 'POST',\n    headers: { 'X-Api-Key': key, 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({ filter: { stageId: 'WON' }, limit: 200 }),\n  }),\n])\nconst { data: dataA } = await a.json()\nconst { data: dataB } = await b.json()\n\nconst byId = new Map(dataA.map(d => [d.id, d]))\nfor (const d of dataB) byId.set(d.id, d)\nconst merged = [...byId.values()]\n```\n\n`Map` по `id` устраняет повторы, если запись попадает под оба условия.\n\n### Чего делать нельзя\n\n```json\n{ \"filter\": { \"LOGIC\": \"OR\", \"0\": { \"stageId\": \"NEW\" }, \"1\": { \"stageId\": \"WON\" } } }\n```\n\nОтвет:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"INVALID_FILTER_OPERATOR\",\n    \"message\": \"INVALID_FILTER_OPERATOR: 'LOGIC' is not supported. OR\u002FAND 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 \u002Fv1\u002Fbatch. AND is the default — combine conditions as sibling keys in one filter object.\"\n  }\n}\n```\n\nАналогично попытка передать `$or` возвращает `400` с подсказкой использовать Batch API.\n\n## Смешивание синтаксисов\n\nВсе три стиля можно комбинировать в одном фильтре:\n\n```json\n{\n  \"filter\": {\n    \"amount\": { \"$gte\": 50000 },\n    \"!stageId\": \"LOST\",\n    \"createdAt\": { \">=\": \"2026-01-01T00:00:00\" }\n  }\n}\n```\n\nНайдёт записи с суммой от 50 000, стадией, отличной от LOST, созданные с начала 2026 года. Здесь все три синтаксиса используются вместе.\n\n## Эндпоинт поиска\n\n`POST \u002Fv1\u002F{entity}\u002Fsearch` — полнофункциональный поиск:\n\n```bash\ncurl -X POST -H \"X-Api-Key: $KEY\" -H \"Content-Type: application\u002Fjson\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fdeals\u002Fsearch\" \\\n  -d '{\n    \"filter\": {\n      \"stageId\": { \"$ne\": \"LOST\" },\n      \"amount\": { \"$gte\": 100000 }\n    },\n    \"sort\": { \"amount\": \"desc\" },\n    \"limit\": 200\n  }'\n```\n\n| Параметр | Тип | Описание |\n|----------|-----|---------|\n| `filter` | object | Условия фильтрации |\n| `sort` | string, object или array | Сортировка. Принимаются три формы: строка (`\"id\"` \u002F `\"-amount\"` \u002F `\"id,-createdAt\"`), объект (`{ id: \"asc\", amount: \"desc\" }` — ключи в порядке вставки) или массив (`[\"id\", \"-amount\"]`). Вместо `asc`\u002F`desc` принимаются `1`\u002F`-1`. Некорректный тип возвращает `400 INVALID_SORT_TYPE`, неизвестное направление — `INVALID_SORT_DIRECTION`. |\n| `limit` | number | Количество записей (по умолчанию 50, максимум 5000) |\n| `offset` | number | Пропустить N записей |\n| `autoWindow` | boolean | `false` — отключить разбивку по дате |\n\n## Коды ошибок\n\n| HTTP | Код | Условие |\n|------|-----|---------|\n| 400 | `INVALID_FILTER_OPERATOR` | Неизвестный оператор в значении поля или попытка передать `LOGIC` \u002F `$or` |\n| 400 | `INVALID_FILTER_FIELD` | Имя поля начинается с родного префикса Битрикс24 `@` (IN) или `!@` (NOT IN) — используйте операторы `$in` \u002F `$nin` |\n| 400 | `UNKNOWN_FILTER_FIELD` | Поле отсутствует в схеме сущности (для сущностей с camelCase-полями) |\n| 400 | `INVALID_SORT_TYPE` | `sort` не строка, не объект и не массив |\n| 400 | `INVALID_SORT_DIRECTION` | Направление сортировки не `asc` \u002F `desc` \u002F `1` \u002F `-1` |\n| 400 | `UNSTABLE_OFFSET_PAGINATION` | `offset > 0` при широком диапазоне дат (см. [Постраничный вывод](#постраничный-вывод)) |\n| — | `WINDOWED_SEARCH_FAILED` | Ретайрнут: при полном отказе авто-окон возвращается реальный код Битрикс24 — `UNKNOWN_FILTER_FIELD` \u002F `BITRIX_ACCESS_DENIED` \u002F `RATE_LIMITED` \u002F `BITRIX_UNAVAILABLE` |\n\nПолный список общих ошибок API — [Коды ошибок](.\u002Ferrors.md).\n\n## Примеры по сущностям\n\n### Сделки — по стадии и сумме\n\n```json\n{\n  \"filter\": {\n    \"stageId\": \"NEW\",\n    \"amount\": { \"$gte\": 100000 }\n  },\n  \"sort\": { \"createdAt\": \"desc\" }\n}\n```\n\nНайдёт сделки в стадии NEW с суммой от 100 000, отсортированные по дате создания (новые первыми).\n\n### Контакты — по телефону\n\nТелефон фильтром не ищут: он сравнивается со всей сохранённой строкой и только по первому номеру записи — см. [Фильтр по телефону и почте](#фильтр-по-телефону-и-почте). Номер ищут отдельным эндпоинтом `POST \u002Fv1\u002Fduplicates\u002Ffind` — [Поиск дубликатов](.\u002Fduplicates.md):\n\n```bash\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Fduplicates\u002Ffind \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"type\": \"phone\",\n    \"values\": [\"+79161234567\"],\n    \"entityType\": \"contact\"\n  }'\n```\n\nВернёт ID контактов с этим номером в любом его написании.\n\n### Задачи — незакрытые\n\n```json\n{\n  \"filter\": {\n    \"status\": { \"$ne\": 5 }\n  }\n}\n```\n\nНайдёт все задачи со статусом, отличным от 5 (завершена). Статусы: 2 = ждёт выполнения, 3 = выполняется, 4 = ожидает контроля, 5 = завершена, 6 = отложена.\n\n### Лиды — за период\n\n```json\n{\n  \"filter\": {\n    \"createdAt\": { \"$gte\": \"2026-01-01T00:00:00\", \"$lte\": \"2026-03-31T23:59:59\" }\n  }\n}\n```\n\nНайдёт лиды, созданные в первом квартале 2026 года.\n\n### Календарные события\n\n```json\n{\n  \"filter\": {\n    \"dateFrom\": { \"$gte\": \"2026-04-01T00:00:00\" }\n  }\n}\n```\n\nНайдёт события с датой начала от 1 апреля 2026. Для `calendar-events` обязательны параметры `type` и `ownerId` в URL: `\u002Fv1\u002Fcalendar-events?type=user&ownerId=1`.\n\n## Фильтрация в Batch\n\nФильтры работают и в [Batch API](.\u002Fbatch.md):\n\n```json\n{\n  \"calls\": [\n    {\n      \"id\": \"new_deals\",\n      \"entity\": \"deals\",\n      \"action\": \"list\",\n      \"params\": {\n        \"filter\": { \"stageId\": \"NEW\" }\n      }\n    },\n    {\n      \"id\": \"search_contacts\",\n      \"entity\": \"contacts\",\n      \"action\": \"search\",\n      \"params\": {\n        \"filter\": { \"email\": { \"$contains\": \"@example.com\" } }\n      }\n    }\n  ]\n}\n```\n\nОдним запросом получит список сделок в стадии NEW и найдёт контакты с адресом в домене `example.com`.\n\n## Постраничный вывод\n\nДва готовых способа — выбирайте по задаче.\n\n### Получить весь результат сразу\n\nПодходит для отчётов, выгрузок и любых случаев, когда нужны все записи. Укажите `limit` до 5000 — сервис вернёт весь подходящий результат одним ответом.\n\n```js\nconst res = await fetch('\u002Fv1\u002Fdeals\u002Fsearch', {\n  method: 'POST',\n  headers: { 'X-Api-Key': key, 'Content-Type': 'application\u002Fjson' },\n  body: JSON.stringify({\n    filter: { closedAt: { $gte: '2026-03-22T00:00:00Z', $lte: '2026-04-22T00:00:00Z' } },\n    limit: 5000,\n  }),\n})\nconst { data, meta } = await res.json()\n\u002F\u002F data — все записи; meta.total — общее количество подходящих записей\n```\n\n### Идти по страницам вручную\n\nПодходит, когда нужно обрабатывать записи порциями (например, импорт по 50 штук с сохранением прогресса). Добавьте `autoWindow: false`, сортировку по `id` и увеличивайте `offset` на каждой итерации.\n\n```js\nlet offset = 0\nwhile (true) {\n  const res = await fetch('\u002Fv1\u002Fdeals\u002Fsearch', {\n    method: 'POST',\n    headers: { 'X-Api-Key': key, 'Content-Type': 'application\u002Fjson' },\n    body: JSON.stringify({\n      filter: { closedAt: { $gte: '2026-03-22T00:00:00Z', $lte: '2026-04-22T00:00:00Z' } },\n      sort: 'id',\n      limit: 50,\n      offset,\n      autoWindow: false,\n    }),\n  })\n  const { data, meta } = await res.json()\n  for (const deal of data) process(deal)\n  offset += data.length\n  if (offset >= meta.total) break\n}\n```\n\n### Чего делать нельзя\n\nНе запускайте параллельные запросы с разным `offset` на одном фильтре — сервис вернёт `400 UNSTABLE_OFFSET_PAGINATION`. Выберите один из двух способов выше.\n\n## Смотрите также\n\n- [Обзор API](.\u002Fentity-api.md)\n- [Поиск дубликатов](.\u002Fduplicates.md)\n- [Связанные данные](.\u002Fincludes.md)\n- [Batch API](.\u002Fbatch.md)\n- [Все сущности](.\u002Fentities-index.md)\n- [Коды ошибок](.\u002Ferrors.md)\n","2026-07-13",{}]