Фильтрация и поиск

Три синтаксиса фильтрации для API сущностей. Все стили можно смешивать в одном запросе.

Фильтрация работает в двух местах:

  • GET /v1/{entity}?filter[field]=value — параметр URL для списка
  • POST /v1/{entity}/search — тело запроса { "filter": { ... } }

Быстрый переход: Фильтр по телефону и почте · Логика ИЛИ · Эндпоинт поиска · Постраничный вывод · Коды ошибок

Синтаксис 1: операторы со знаком `$`

Операторы с префиксом $ внутри объекта поля.

JSON
{
  "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.

Комбинирование

Несколько условий на одном поле объединяются логикой И. Условия на разные поля — тоже логикой И. Для ИЛИ-логики см. раздел Логика ИЛИ ниже.

JSON
{
  "filter": {
    "amount": { "$gte": 50000, "$lte": 200000 },
    "stageId": { "$ne": "LOST" }
  }
}

Найдёт записи с суммой от 50 000 до 200 000 и стадией, отличной от LOST.

Синтаксис 2: префикс в имени поля

Оператор как часть имени поля. Форма, которую напрямую понимает Битрикс24.

JSON
{
  "filter": {
    ">=amount": 50000,
    "<=amount": 200000,
    "!stageId": "LOST"
  }
}

Найдёт записи с суммой от 50 000 до 200 000 и стадией, отличной от LOST.

Операторы

Префикс Значение
>= больше или равно
> больше
<= меньше или равно
< меньше
! не равно
% подстрока

Синтаксис 3: оператор как ключ объекта

Оператор как ключ вложенного объекта. Та же форма, что в синтаксисе 2, но с разделением имени поля и условия.

JSON
{
  "filter": {
    "amount": { ">=": 50000 },
    "stageId": { "!": "LOST" }
  }
}

Найдёт записи с суммой от 50 000 и стадией, отличной от LOST.

Фильтрация по дате

Поля даты (createdAt, updatedAt, closedAt, beginDate и др.) принимают строки ISO 8601:

JSON
{
  "filter": {
    "createdAt": { "$gte": "2026-01-01T00:00:00" },
    "closedAt": { "$lte": "2026-03-31T23:59:59" }
  }
}

Найдёт записи, созданные с начала 2026 года и закрытые до конца марта.

Примеры

JSON
{ "filter": { ">=createdAt": "2026-03-01T00:00:00" } }

Найдёт записи, созданные с 1 марта 2026.

JSON
{ "filter": { "updatedAt": { "$gte": "2026-05-01T00:00:00" } } }

Найдёт записи, обновлённые с указанного момента.

Границы диапазона задаются операторами $gte/$lte — или эквивалентными >=/<= из синтаксисов выше. Ключи from/to в значении поля не поддерживаются и вернут 400 INVALID_FILTER_OPERATOR:

JSON
{ "filter": { "createdAt": { "$gte": "2026-06-01T00:00:00", "$lte": "2026-06-30T23:59:59" } } }

NOT-фильтры

Исключение значений:

JSON
{ "filter": { "stageId": { "$ne": "LOST" } } }
JSON
{ "filter": { "!stageId": "LOST" } }
JSON
{ "filter": { "stageId": { "!": "LOST" } } }

Все три варианта найдут записи, у которых стадия отличается от LOST.

Фильтр по заполненности (не пусто / не null)

Частый кейс — отобрать только записи, где поле заполнено: например, контакты с указанным ИНН, чтобы не тянуть всю базу при дедупликации. Сравнение с null через оператор $ne делает эту выборку на стороне Битрикс24:

JSON
{ "filter": { "ufCrm_inn": { "$ne": null } } }

Найдёт только записи, где ufCrm_inn заполнено. Эквивалентная форма — сравнение с пустой строкой { "$ne": "" }. Работает и для пользовательских (UF) полей, и для стандартных (post, phone, email и др.).

Обратное условие — «поле пусто» — задаётся точным сравнением с null:

JSON
{ "filter": { "ufCrm_inn": null } }

Вместе две выборки дают полное разбиение набора (заполненные + пустые = все).

Важно для GET-запросов. URL не умеет передавать настоящий null — в строке запроса он превращается в текст "null", и фильтр начинает искать буквальную строку «null» (вернёт мусор). Для «не пусто» в GET передавайте пустое значение, а не слово null:

  • GET /v1/contacts?filter[ufCrm_inn][$ne]= — поле заполнено
  • GET /v1/contacts?filter[ufCrm_inn][$ne]=null — ищет текст «null», это НЕ то же самое

Либо используйте POST /v1/contacts/search с телом { "filter": { "ufCrm_inn": { "$ne": null } } } — в JSON-теле null передаётся корректно.

Фильтр по телефону и почте

Поля phone и email у лидов, контактов и компаний ведут себя не так, как остальные. У них две особенности, которые надо знать до того, как писать фильтр.

Значение сравнивается целиком, вместе со знаками. Запись с номером +7 (999) 123-45-67 найдётся только по этой же строке:

JSON
{ "filter": { "phone": "+7 (999) 123-45-67" } }

Плюс, пробелы, скобки и дефисы — часть сохранённого значения. Поэтому та же запись не найдётся ни по 79991234567, ни по 89991234567.

В каком виде номер попадёт в базу, решает портал: один и тот же телефон может лежать в ней и как +7 (999) 123-45-67, и как 89991234567. Ваша программа заранее не знает, какой вид выбран, поэтому фильтр по точному значению годится только тогда, когда сохранённая строка уже известна.

У записи может быть несколько номеров, а фильтр видит только первый. Если у контакта записаны рабочий и мобильный телефоны, фильтр найдёт его по рабочему — тому, который приходит в поле phone в ответе. По мобильному фильтр вернёт пустой список.

Найти запись по номеру телефона

Обе особенности снимает Поиск дубликатов — отдельный эндпоинт POST /v1/duplicates/find. Он сравнивает номера по существу, а не по написанию, и проверяет все номера записи, а не только первый:

Terminal
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 ищет кусок текста внутри сохранённого значения, и для почты это рабочий способ:

JSON
{ "filter": { "email": { "$contains": "@example.com" } } }

Найдёт записи с почтой в домене example.com. Для телефона результат зависит от того, где внутри сохранённого номера стоят пробелы и дефисы, и от того, первый это номер записи или нет. Поэтому запись по номеру ищут поиском дубликатов, а не оператором $contains.

Логика ИЛИ

Все условия в объекте filter объединяются логикой И. Логический оператор ИЛИ между разными полями нельзя выразить прямо в filter — попытка передать LOGIC: "OR" или $or возвращает 400 INVALID_FILTER_OPERATOR. Ниже три способа получить тот же результат.

Способ 1: `$in` — несколько значений одного поля

Когда нужно «поле равно A или B или C», используйте оператор $in из таблицы выше:

JSON
{
  "filter": {
    "stageId": { "$in": ["NEW", "WON"] }
  }
}

Найдёт сделки в стадии NEW или WON. Оператор работает на любом поле, для которого имеет смысл точное сравнение: assignedById, categoryId, id, sourceId и так далее.

Способ 2: Batch — несколько фильтров одним запросом

Когда условия ИЛИ затрагивают разные поля («сделки в стадии NEW или с суммой больше 100 000»), вынесите каждое условие в отдельный вызов внутри Batch API:

JSON
{
  "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:

JavaScript
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 устраняет повторы, если запись попадает под оба условия.

Чего делать нельзя

JSON
{ "filter": { "LOGIC": "OR", "0": { "stageId": "NEW" }, "1": { "stageId": "WON" } } }

Ответ:

JSON
{
  "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.

Смешивание синтаксисов

Все три стиля можно комбинировать в одном фильтре:

JSON
{
  "filter": {
    "amount": { "$gte": 50000 },
    "!stageId": "LOST",
    "createdAt": { ">=": "2026-01-01T00:00:00" }
  }
}

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

Эндпоинт поиска

POST /v1/{entity}/search — полнофункциональный поиск:

Terminal
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 — отключить разбивку по дате

Коды ошибок

HTTP Код Условие
400 INVALID_FILTER_OPERATOR Неизвестный оператор в значении поля или попытка передать LOGIC / $or
400 INVALID_FILTER_FIELD Имя поля начинается с родного префикса Битрикс24 @ (IN) или !@ (NOT IN) — используйте операторы $in / $nin
400 UNKNOWN_FILTER_FIELD Поле отсутствует в схеме сущности (для сущностей с camelCase-полями)
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 / BITRIX_ACCESS_DENIED / RATE_LIMITED / BITRIX_UNAVAILABLE

Полный список общих ошибок API — Коды ошибок.

Примеры по сущностям

Сделки — по стадии и сумме

JSON
{
  "filter": {
    "stageId": "NEW",
    "amount": { "$gte": 100000 }
  },
  "sort": { "createdAt": "desc" }
}

Найдёт сделки в стадии NEW с суммой от 100 000, отсортированные по дате создания (новые первыми).

Контакты — по телефону

Телефон фильтром не ищут: он сравнивается со всей сохранённой строкой и только по первому номеру записи — см. Фильтр по телефону и почте. Номер ищут отдельным эндпоинтом POST /v1/duplicates/findПоиск дубликатов:

Terminal
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 контактов с этим номером в любом его написании.

Задачи — незакрытые

JSON
{
  "filter": {
    "status": { "$ne": 5 }
  }
}

Найдёт все задачи со статусом, отличным от 5 (завершена). Статусы: 2 = ждёт выполнения, 3 = выполняется, 4 = ожидает контроля, 5 = завершена, 6 = отложена.

Лиды — за период

JSON
{
  "filter": {
    "createdAt": { "$gte": "2026-01-01T00:00:00", "$lte": "2026-03-31T23:59:59" }
  }
}

Найдёт лиды, созданные в первом квартале 2026 года.

Календарные события

JSON
{
  "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:

JSON
{
  "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.

Постраничный вывод

Два готовых способа — выбирайте по задаче.

Получить весь результат сразу

Подходит для отчётов, выгрузок и любых случаев, когда нужны все записи. Укажите limit до 5000 — сервис вернёт весь подходящий результат одним ответом.

JavaScript
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.total — общее количество подходящих записей

Идти по страницам вручную

Подходит, когда нужно обрабатывать записи порциями (например, импорт по 50 штук с сохранением прогресса). Добавьте autoWindow: false, сортировку по id и увеличивайте offset на каждой итерации.

JavaScript
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()
  for (const deal of data) process(deal)
  offset += data.length
  if (offset >= meta.total) break
}

Чего делать нельзя

Не запускайте параллельные запросы с разным offset на одном фильтре — сервис вернёт 400 UNSTABLE_OFFSET_PAGINATION. Выберите один из двух способов выше.

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