#Filtering and search

Three filtering syntaxes for API entities. All styles can be mixed in a single request.

Filtering works in two places:

  • GET /v1/{entity}?filter[field]=value — URL parameter for lists
  • POST /v1/{entity}/search — request body { "filter": { ... } }

Quick jump: OR logic · Search endpoint · Pagination · Error codes

#Syntax 1: operators with a `$` sign

Operators prefixed with $ inside the field object. Convenient for AI agents.

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

Finds records with an amount from 50,000, a stage other than LOST, created since the start of 2026.

#Available operators

Operator Meaning Example
$gt > (greater than) { "amount": { "$gt": 10000 } }
$gte >= (greater than or equal) { "amount": { "$gte": 50000 } }
$lt < (less than) { "amount": { "$lt": 100000 } }
$lte <= (less than or equal) { "amount": { "$lte": 200000 } }
$ne != (not equal) { "stageId": { "$ne": "LOST" } }
$contains substring search { "title": { "$contains": "delivery" } }
$in is in array { "stageId": { "$in": ["NEW", "WON"] } }

An exact match is set by the value directly, without an operator: { "stageId": "NEW" }.

#Combining

Multiple conditions on a single field are joined with AND logic. Conditions on different fields are also joined with AND logic. For OR logic see the OR logic section below.

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

Finds records with an amount from 50,000 to 200,000 and a stage other than LOST.

#Syntax 2: prefix in the field name

The operator as part of the field name. The form Bitrix24 understands directly.

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

Finds records with an amount from 50,000 to 200,000 and a stage other than LOST.

#Operators

Prefix Meaning
>= greater than or equal
> greater than
<= less than or equal
< less than
! not equal
% substring

#Syntax 3: operator as the object key

The operator as the key of a nested object. The same form as in syntax 2, but with the field name and condition separated.

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

Finds records with an amount from 50,000 and a stage other than LOST.

#Filtering by date

Date fields (createdAt, updatedAt, closedAt, beginDate, etc.) accept ISO 8601 strings:

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

Finds records created since the start of 2026 and closed before the end of March.

#Examples

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

Finds records created since March 1, 2026.

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

Finds records updated since the specified moment.

#NOT filters

Excluding values:

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

All three variants find records whose stage differs from LOST.

#OR logic

All conditions in the filter object are joined with AND logic. An OR logical operator between different fields cannot be expressed directly in filter — attempting to pass LOGIC: "OR" or $or returns 400 INVALID_FILTER_OPERATOR. Below are three ways to get the same result.

#Way 1: `$in` — multiple values of one field

When you need "field equals A or B or C", use the $in operator from the table above:

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

Finds deals in the NEW or WON stage. The operator works on any field for which an exact comparison makes sense: assignedById, categoryId, id, sourceId, and so on.

#Way 2: Batch — multiple filters in one request

When the OR conditions touch different fields ("deals in the NEW stage or with an amount over 100,000"), move each condition into a separate call inside the 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 }
    }
  ]
}

The response arrives as data.results.by_stage and data.results.by_amount — two independent arrays that the client merges itself.

#Way 3: parallel requests + client-side merge

When you need a single list without duplicates, run the calls in parallel and merge the results by 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()]

A Map keyed by id removes duplicates when a record matches both conditions.

#What not to do

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

Response:

JSON
{
  "success": false,
  "error": {
    "code": "INVALID_FILTER_OPERATOR",
    "message": "Unknown filter operator: stageId. Supported: $gt, $gte, $lt, $lte, $ne, $contains, $in, >, >=, <, <=, !, %, !="
  }
}

Likewise, an attempt to pass $or returns 400 with a hint to use the Batch API.

#Mixing syntaxes

All three styles can be combined in a single filter:

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

Finds records with an amount from 50,000, a stage other than LOST, created since the start of 2026. Here all three syntaxes are used together.

#Search endpoint

POST /v1/{entity}/search — full-featured 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
  }'
Parameter Type Description
filter object Filtering conditions
sort string, object or array Sorting. Three forms are accepted: string ("id" / "-amount" / "id,-createdAt"), object ({ id: "asc", amount: "desc" } — keys in insertion order) or array (["id", "-amount"]). Instead of asc/desc, 1/-1 are accepted. An invalid type returns 400 INVALID_SORT_TYPE, an unknown direction — INVALID_SORT_DIRECTION.
limit number Number of records (default 50, maximum 5000)
offset number Skip N records
autoWindow boolean false — disable date windowing

#Error codes

HTTP Code Condition
400 INVALID_FILTER_OPERATOR Unknown operator in a field value or an attempt to pass LOGIC / $or
400 INVALID_FILTER_FIELD Field name starts with @ — that prefix is not supported
400 UNKNOWN_FILTER_FIELD Field is missing from the entity schema (for entities with camelCase fields)
400 INVALID_SORT_TYPE sort is not a string, object, or array
400 INVALID_SORT_DIRECTION Sort direction is not asc / desc / 1 / -1
400 UNSTABLE_OFFSET_PAGINATION offset > 0 with a wide date range (see Pagination)
502 WINDOWED_SEARCH_FAILED The service failed to retrieve part of the results during automatic pagination — retry the request

The full list of common API errors — Error codes.

#Examples by entity

#Deals — by stage and amount

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

Finds deals in the NEW stage with an amount from 100,000, sorted by creation date (newest first).

#Contacts — by phone

JSON
{
  "filter": {
    "phone": { "$contains": "+7916" }
  }
}

Finds contacts whose phone number contains the substring "+7916".

#Tasks — unfinished

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

Finds all tasks with a status other than 5 (completed). Statuses: 2 = pending, 3 = in progress, 4 = awaiting control, 5 = completed, 6 = deferred.

#Leads — for a period

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

Finds leads created in the first quarter of 2026.

#Calendar events

JSON
{
  "filter": {
    "dateFrom": { "$gte": "2026-04-01T00:00:00" }
  }
}

Finds events with a start date from April 1, 2026. For calendar-events, the type and ownerId parameters are required in the URL: /v1/calendar-events?type=user&ownerId=1.

#Filtering in Batch

Filters also work in the Batch API:

JSON
{
  "calls": [
    {
      "id": "new_deals",
      "entity": "deals",
      "action": "list",
      "params": {
        "filter": { "stageId": "NEW" }
      }
    },
    {
      "id": "search_contacts",
      "entity": "contacts",
      "action": "search",
      "params": {
        "filter": { "phone": { "$contains": "+7" } }
      }
    }
  ]
}

In one request, retrieves the list of deals in the NEW stage and finds contacts with Russian numbers.

#Pagination

Two ready-made approaches — choose by task.

#Get the whole result at once

Suitable for reports, exports, and any case where you need all records. Set limit up to 5000 — the service returns the entire matching result in a single response.

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 — all records; meta.total — total count of matching records

#Page through manually

Suitable when you need to process records in batches (for example, importing 50 at a time while saving progress). Add autoWindow: false, sort by id, and increase offset on each iteration.

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
}

#What not to do

Do not run parallel requests with different offset on the same filter — the service returns 400 UNSTABLE_OFFSET_PAGINATION. Pick one of the two approaches above.

#See also