## Aggregate nodes

`POST /v1/humanresources/nodes/aggregate`

Counts org structure nodes of the selected type and computes numeric aggregations over them — sum, average, minimum, maximum. The node type is passed inside the filter in the request body, not in the query string as with the node list.

**Fields available to numeric functions:**

- `userCount` — the number of employees in the node, the only field where numeric aggregation is meaningful.
- `id`, `structureId`, `parentId` — identifiers. The `sum`, `avg`, `min`, `max` functions accept them, but the result carries no meaning as a metric.

## Request fields (body)

| Parameter | Type | Required | Description |
|----------|-----|:-----:|---------|
| `filter` | object | yes | An object with a single key `type` — the node type: `DEPARTMENT` (department) or `TEAM` (team). Other filter keys are not accepted |
| `aggregate` | array | no | Aggregations: `[{ "field": "userCount", "function": "sum" }]`, up to 5 expressions. Functions: `count`, `sum`, `avg`, `min`, `max`. For `count` the field is `"*"`. Without the parameter, only `count` is returned |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/humanresources/nodes/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "type": "DEPARTMENT" },
    "aggregate": [
      { "field": "userCount", "function": "sum" }
    ]
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/humanresources/nodes/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "type": "DEPARTMENT" },
    "aggregate": [
      { "field": "userCount", "function": "sum" }
    ]
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/humanresources/nodes/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { type: 'DEPARTMENT' },
    aggregate: [
      { field: 'userCount', function: 'sum' },
    ],
  }),
})

const { success, data } = await res.json()
console.log(`Departments: ${data.count}, employees in them: ${data.aggregates.userCount.sum}`)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/humanresources/nodes/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { type: 'DEPARTMENT' },
    aggregate: [
      { field: 'userCount', function: 'sum' },
    ],
  }),
})

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

## Other scenarios

The blocks below are request bodies.

The number of teams — the fastest query, without fetching records:

```json
{ "filter": { "type": "TEAM" } }
```

The same via an explicit `count` — field `"*"`, the result matches the request without `aggregate`:

```json
{ "filter": { "type": "DEPARTMENT" }, "aggregate": [{ "field": "*", "function": "count" }] }
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data.count` | number | Number of nodes of the selected type |
| `data.aggregates` | object | Numeric-function results: `{ "userCount": { "sum": 55 } }`. An empty object if no numeric functions were requested |
| `data.meta.totalRecords` | number | Total number of records |
| `data.meta.recordsProcessed` | number | Number of records processed. `0` for a request without numeric functions |
| `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 no numeric functions, no records are fetched and the flag is always `false` |

## Response example

Total employees across all departments:

```json
{
  "success": true,
  "data": {
    "count": 20,
    "aggregates": {
      "userCount": { "sum": 55 }
    },
    "meta": {
      "totalRecords": 20,
      "recordsProcessed": 20,
      "truncated": false
    }
  }
}
```

## Error response example

400 — the node type is not passed in `filter`:

```json
{
  "success": false,
  "error": {
    "code": "MISSING_REQUIRED_PARAMS",
    "message": "POST /v1/humanresources/nodes/aggregate requires filter params: type. Example body: { \"filter\": {\"type\":\"...\"} }"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `MISSING_REQUIRED_PARAMS` | `filter` has no `type` key, or the value is empty or passed as an array or object. The same response if `type` is passed at the top level of the body or in the query string |
| 400 | `INVALID_PARAMS` | The `type` value is neither `DEPARTMENT` nor `TEAM` — `validation[]` contains an element with `field: "INVALID_TYPE"` |
| 400 | `INVALID_PARAMS` | Unknown function — `Invalid aggregate function '<name>'. Supported: count, sum, avg, min, max` |
| 400 | `INVALID_PARAMS` | Numeric function on a non-numeric field — `Field '<name>' is not numeric (type: string)` |
| 400 | `INVALID_PARAMS` | Numeric function on a field that nodes do not have — `Field '<name>' not found`, the message lists the available numeric fields |
| 400 | `INVALID_PARAMS` | Numeric function without a field or with field `"*"` |
| 400 | `INVALID_PARAMS` | `count` with a field other than `"*"` |
| 400 | `INVALID_PARAMS` | Empty `aggregate` array or more than 5 expressions in it |
| 400 | `INVALID_PARAMS` | `groupBy` is passed — message `groupBy field '<name>' is not aggregatable on this entity` |
| 422 | `BITRIX_ERROR` | `filter` contains a key other than `type`, such as `parentId` or `name` — a message about an unrecognized filter expression |
| 403 | `SCOPE_DENIED` | The key lacks the `humanresources` scope |
| 401 | `TOKEN_MISSING` | The API key has no Bitrix24 tokens configured |
| 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](/docs/errors).

## Known specifics

**`count` takes a single request, numeric functions do not.** `count` is computed in one call regardless of the number of nodes. The `sum`, `avg`, `min`, `max` functions load records page by page — up to 5000 — and compute the values on the server side.

**Grouping is not available.** Nodes have no fields for `groupBy`: a request with any grouping field returns `400 INVALID_PARAMS` with an empty list of allowed fields. To get separate numbers for departments and teams, make one request per type or call [node count by type](/docs/humanresources/node-operations/count).

**The truncation marker arrives next to the number itself.** When the response comes with `meta.truncated: true`, a `truncated: true` marker sits inside the object of each field in `data.aggregates`, and a warning with code `AGGREGATE_TRUNCATED` is added to `data.meta.warnings`. This way a client that reads only the number itself sees that it was computed over only some of the records. A complete response carries none of these markers. Full breakdown — [Aggregation POST — the 5000-record ceiling](/docs/entity-api#aggregation-post-the-5000-record-ceiling).

## See also

- [List nodes](/docs/humanresources/nodes/list)
- [Search nodes](/docs/humanresources/nodes/search)
- [Node fields](/docs/humanresources/nodes/fields)
- [Node count by type](/docs/humanresources/node-operations/count)
- [Limits and optimization](/docs/optimization)
