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.
Aggregating activities requires 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+ownerIdpair, orresponsibleId, or a date bound oncreatedAt/updatedAt/deadline. The current list of narrowing filters, and whether the account requires them right now, live indata.aggregateFilterRequirementof the GET /v1/activities/fields response.
Standard fields:
typeId— activity type (forgroupBy)ownerTypeId— parent entity type (forgroupBy)responsibleId— responsible person (forgroupBy)completed— completion status (forgroupBy)
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 |
|---|---|---|---|
Parameteraggregate |
Typearray | Requiredno | DescriptionArray of aggregations. Each element: { "field": "*", "function": "count" }. Without the array — count only |
Parameterfilter |
Typeobject | Requiredyes | DescriptionFilter by GET /v1/activities/fields fields. Must carry one of the narrowing filters (see the callout above). Filtering syntax |
ParametergroupBy |
Typestring | string[] | Requiredno | DescriptionField 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:
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
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
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
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:
{
"filter": { ">=createdAt": "2026-07-01", "<createdAt": "2026-08-01" }
}
Number of activities assigned to one employee:
{
"filter": { "responsibleId": 12 }
}
Activities for a contact (ownerTypeId: 3) grouped by completion status:
{
"filter": { "ownerTypeId": 3, "ownerId": 485 },
"groupBy": "completed"
}
Response fields
| Field | Type | Description |
|---|---|---|
Fieldsuccess |
Typeboolean | DescriptionAlways true on success |
Fielddata.count |
Typenumber | DescriptionTotal number of records matching the filter |
Fielddata.aggregates |
Typeobject | DescriptionNumeric-function results grouped by field name. An empty object when the request carries no sum, avg, min or max |
Fielddata.groups |
Typearray | DescriptionGroups (only with groupBy). Each element: grouping fields + count |
Fielddata.meta.totalRecords |
Typenumber | DescriptionTotal number of records matching the filter |
Fielddata.meta.recordsProcessed |
Typenumber | DescriptionHow many records were processed (for count — 0, records are not retrieved) |
Fielddata.meta.truncated |
Typeboolean | Descriptiontrue 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 |
Fielddata.meta.recordsShortfall |
Typenumber | DescriptionPresent 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"):
{
"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:
{
"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 |
|---|---|---|
| HTTP400 | CodeMISSING_REQUIRED_FILTER |
DescriptionThe request carries no narrowing filter. The response lists the accepted narrowing filters and includes a ready-to-paste example body |
| HTTP400 | CodeINVALID_PARAMS |
DescriptiongroupBy on a non-aggregatable field or more than 5 fields in groupBy |
| HTTP401 | CodeTOKEN_MISSING |
DescriptionThe API key has no configured tokens |
| HTTP403 | CodeSCOPE_DENIED |
DescriptionThe API key lacks the crm scope |
| HTTP422 | CodeAGGREGATION_LIMIT_EXCEEDED |
DescriptionBitrix24 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 |
| HTTP429 | CodeRATE_LIMITED |
DescriptionRate limit exceeded: 300 requests per minute per portal, all API keys of the portal share one limit. The current limit is returned in the x-ratelimit-limit header. Retry after the delay in the Retry-After header |
For the full list of common API errors, see 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.