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

Aggregate activities

POST /v1/activities/aggregate

Count activities with filtering and grouping.

An activities aggregate needs a narrowing filter. Bitrix24 cannot count all the activities of an account within the time allowed for one call, so on a large account a request without a narrowing filter gets no answer at all — however many times it is repeated. One of the three narrowing filters is enough: the ownerTypeId + ownerId pair, or responsibleId, or a date bound on createdAt / updatedAt / deadline. The current list of narrowing filters, and whether the account requires them right now, live in data.aggregateFilterRequirement of the GET /v1/activities/fields response.

Standard fields:

  • typeId — activity type (for groupBy)
  • ownerTypeId — parent entity type (for groupBy)
  • responsibleId — responsible person (for groupBy)
  • completed — completion status (for groupBy)

All fields in aggregatable are categorical identifiers, so the main scenario is count with grouping. The numeric functions sum/avg/min/max are rarely used.

Request fields (body)

Parameter Type Required Description
aggregate array no Array of aggregations. Each element: { "field": "*", "function": "count" }. Without the array — count only
filter object yes Filter by GET /v1/activities/fields fields. Must carry one of the narrowing filters (see the callout above). 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

Number of activities for a deal (ownerTypeId: 2 — deal), grouped by activity type:

Terminal
curl -X POST "https://vibecode.bitrix24.com/v1/activities/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "ownerTypeId": 2, "ownerId": 741, "completed": "Y" },
    "groupBy": "typeId"
  }'

curl — OAuth application

Terminal
curl -X POST "https://vibecode.bitrix24.com/v1/activities/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "ownerTypeId": 2, "ownerId": 741, "completed": "Y" },
    "groupBy": "typeId"
  }'

JavaScript — personal key

javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/activities/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { ownerTypeId: 2, ownerId: 741, completed: 'Y' },
    groupBy: 'typeId',
  }),
})

const { success, data } = await res.json()
console.log('Total completed activities for the deal:', data.count)
console.log('Distribution by type:', data.groups)

JavaScript — OAuth application

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

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

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

Other scenarios

Number of activities for a period — a date bound counts as a narrowing filter:

JSON
{
  "filter": { ">=createdAt": "2026-07-01", "<createdAt": "2026-08-01" }
}

Number of activities assigned to one employee:

JSON
{
  "filter": { "responsibleId": 12 }
}

Activities for a contact (ownerTypeId: 3) grouped by completion status:

JSON
{
  "filter": { "ownerTypeId": 3, "ownerId": 485 },
  "groupBy": "completed"
}

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
data.meta.recordsShortfall number Present only when rows are missing: totalRecords − recordsProcessed. count and totalRecords stay complete — only groups and the numeric aggregations are partial

Response example

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

JSON
{
  "success": true,
  "data": {
    "count": 2600,
    "aggregates": {},
    "groups": [
      { "typeId": 1, "count": 1200 },
      { "typeId": 2, "count": 800 },
      { "typeId": 6, "count": 600 }
    ],
    "meta": {
      "totalRecords": 2600,
      "recordsProcessed": 2600,
      "truncated": false
    }
  }
}

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

Error response example

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

JSON
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "groupBy field 'subject' is not aggregatable on this entity. Available: typeId, ownerTypeId, responsibleId, completed"
  }
}

Errors

HTTP Code Description
400 MISSING_REQUIRED_FILTER The request carries no narrowing filter. The response lists the accepted narrowing filters and includes a ready-to-paste example body
400 INVALID_PARAMS groupBy on a non-aggregatable field or more than 5 fields in groupBy
401 TOKEN_MISSING The API key has no configured tokens
403 SCOPE_DENIED The API key lacks the crm scope
422 AGGREGATION_LIMIT_EXCEEDED Bitrix24 did not answer an aggregate without a narrowing filter within the time allowed for one call. Repeating the same request will not help — add a narrowing filter. There is no Retry-After header here: the refusal is not transient
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

Universal ownerTypeId. For counters by parent entity, use the ownerTypeId + ownerId pair: 2 — deal, 3 — contact, 4 — company, 1 — lead. This is the most common scenario for /v1/activities/aggregate.

Why exactly these three narrowing filters. These are the fields for which the activities table has an index whose leading column is the field itself: OWNER_ID+OWNER_TYPE_ID, RESPONSIBLE_ID, CREATED, LAST_UPDATED, DEADLINE. authorId, editorId and providerTypeId have no index at all, while completed, typeId, providerId, direction and status have too few distinct values — one value still covers almost the whole account. Bounds on startTime and endTime do not count as a narrowing filter on their own, but you can pass them alongside a real one.

The platform enables the requirement per account. While it is off, a request without a narrowing filter still goes to Bitrix24 and gets the same answer as before on a small account; on a large one it comes back with 422. To check the state, read data.aggregateFilterRequirement.enforcement in the GET /v1/activities/fields response — enforced (the requirement is on) or advisory (still only a recommendation).

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