For AI agents: markdown of this page — /docs-content-en/entities/contacts/aggregate.md documentation index — /llms.txt

Aggregate contacts

POST /v1/contacts/aggregate

Count contacts with filtering and grouping.

Standard fields:

  • typeId — contact type (for groupBy)
  • sourceId — source (for groupBy)
  • assignedById — assigned user (for groupBy)

All fields in aggregatable are categorical identifiers, so count and groupBy work on the standard fields. Numeric functions (sum/avg/min/max) accept any field of type number, but on a contact those are identifiers — a meaningful sum usually comes from user fields.

User fields (UF): UF fields of types integer, double, money — for numeric functions; UF of any type — for groupBy. The full list of UF fields for a specific Bitrix24 account is returned in the INVALID_PARAMS error text if you pass a non-existent name.

Request fields (body)

Parameter Type Required Description
aggregate array no Array of aggregations. Each element: { "field": "*", "function": "count" }. Without the array — only count
filter object no Filtering by GET /v1/contacts/fields fields. Filtering syntax
groupBy string | string[] no Field or array of fields to group by (maximum 5). Allowed values — from the list above

Examples

curl — personal key

Terminal
curl -X POST "https://vibecode.bitrix24.com/v1/contacts/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "assignedById": 1 },
    "groupBy": "typeId"
  }'

curl — OAuth application

Terminal
curl -X POST "https://vibecode.bitrix24.com/v1/contacts/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "assignedById": 1 },
    "groupBy": "typeId"
  }'

JavaScript — personal key

javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/contacts/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { assignedById: 1 },
    groupBy: 'typeId',
  }),
})

const { success, data } = await res.json()
console.log('Total contacts:', data.count)
console.log('By type:', data.groups)

JavaScript — OAuth application

javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/contacts/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { assignedById: 1 },
    groupBy: 'typeId',
  }),
})

const { success, data } = await res.json()

To group by multiple fields, pass an array: "groupBy": ["typeId", "sourceId"] (maximum 5).

Other scenarios

Total number of contacts in the Bitrix24 account — the fastest query, with no record retrieval:

JSON
{}

Working with user fields (UF) — sum over a UF + grouping by another UF:

JSON
{
  "aggregate": [{ "field": "UF_CRM_LTV", "function": "sum" }],
  "groupBy": "UF_CRM_SEGMENT"
}

Response fields

Field Type Description
success boolean Always true on success
data.count number Total number of records matching the filter
data.aggregates object Numeric-function results grouped by field name. An empty object when the request carries no sum, avg, min or max
data.groups array Groups (only with groupBy). Each element: grouping fields + count
data.meta.totalRecords number Total number of records matching the filter
data.meta.recordsProcessed number How many records were processed (for count — 0, records are not retrieved)
data.meta.truncated boolean true when the numbers were computed over only some of the records that match the filter: fewer records were read than data.count promised — including when more than 5000 matched the filter — or the slice was cut short by a sub-page error. The size of the gap is reported in data.meta.recordsShortfall, the interrupted slice in data.meta.pageErrorSample. Always present, and false on a complete response. If the request has neither groupBy nor a numeric function, no records are fetched and the flag is always false

Response example

Response to the main request (groupBy: "typeId"):

JSON
{
  "success": true,
  "data": {
    "count": 1200,
    "aggregates": {},
    "groups": [
      { "typeId": "CLIENT", "count": 800 },
      { "typeId": "SUPPLIER", "count": 300 },
      { "typeId": "PARTNER", "count": 100 }
    ],
    "meta": {
      "totalRecords": 1200,
      "recordsProcessed": 1200,
      "truncated": false
    }
  }
}

Without groupBy, the data.groups field is absent from the response.

Error response example

400 — groupBy by a non-aggregatable or non-existent field:

JSON
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "groupBy field 'name' is not aggregatable on this entity. Available: typeId, sourceId, assignedById"
  }
}

Errors

HTTP Code Description
400 INVALID_PARAMS groupBy by a non-aggregatable field or more than 5 fields in groupBy
401 TOKEN_MISSING API key has no configured tokens
403 SCOPE_DENIED API key lacks the crm scope
429 RATE_LIMITED Rate limit exceeded: 300 requests per minute per portal, all API keys of the portal share one limit. The exact value arrives in the x-ratelimit-limit header (the cap is divided across replicas). Retry after the delay in the Retry-After header

Full list of common API errors — Errors.

Known specifics

Money fields. UF fields of type money are stored in the "amount|currency" format ("1500|USD") — the aggregate extracts the numeric part automatically, so you can sum them without parsing.

Filtering by UF works. In filter you can pass any fields — standard and user fields, of any type. For example, { "filter": { "ufCrm_1234": "value" } } returns the number of contacts with that UF value.

The truncation marker travels with the number itself. When a response arrives with meta.truncated: true, the marker truncated: true sits inside every field object in data.aggregates and on every element of data.groups, and data.meta.warnings gains a warning with the code AGGREGATE_TRUNCATED. A client that reads only the number itself therefore sees that it was computed over only some of the records. None of these markers appear on a complete response. Full details — Aggregation POST — the 5000-record ceiling.

See also