## Aggregate payer types

`POST /v1/person-types/aggregate`

Counts payer types with filtering and grouping by activity or code, and calculates the sum, average, minimum, and maximum of numeric fields.

## Standard fields

| Field | Purpose |
|------|------------|
| `active` | Activity flag for `groupBy` |
| `code` | Unique code for `groupBy` |
| `sort` | Sort order: a numeric field for `sum`, `avg`, `min`, `max` |
| `id` | Payer type ID from [List payer types](./list.md). A numeric field for `sum`, `avg`, `min`, `max` |

Fields for `groupBy` are listed in `data.aggregatable` of [`GET /v1/person-types/fields`](./fields.md). Numeric functions accept fields with the `number` type from the same schema: `id` and `sort`.

## Request body fields

| Field | Type | Required | Description |
|------|-----|:-----:|---------|
| `aggregate` | array | no | An array of 1 to 5 expressions such as `{ "field": "sort", "function": "sum" }`. Functions: `count`, `sum`, `avg`, `min`, `max`. For `count`, the field is `"*"`. Defaults to `[{ "field": "*", "function": "count" }]` |
| `filter` | object | no | Filter by fields from [`GET /v1/person-types/fields`](./fields.md).<br>[Filtering syntax](/docs/filtering). Example: `{ "id": 21 }` |
| `groupBy` | string \| string[] | no | A field or array of up to 5 grouping fields. Allowed fields: `active`, `code` |
| `groupOrderBy` | array | no | Group sorting: `[{ "field": "count", "direction": "desc" }]`. The `field` is `count`, a field from `groupBy`, or `<field>:<function>` from `aggregate`. Directions: `asc`, `desc`. Defaults to `desc` when `direction` is omitted. Requires `groupBy` |
| `groupLimit` | number | no | Number of groups to return: an integer from 1 to 1000. If omitted, groups are not truncated. Requires `groupBy` |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/person-types/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "id": 21 },
    "aggregate": [{ "field": "*", "function": "count" }],
    "groupBy": ["active", "code"],
    "groupOrderBy": [{ "field": "count", "direction": "desc" }],
    "groupLimit": 10
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/person-types/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "id": 21 },
    "aggregate": [{ "field": "*", "function": "count" }],
    "groupBy": ["active", "code"],
    "groupOrderBy": [{ "field": "count", "direction": "desc" }],
    "groupLimit": 10
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/person-types/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { id: 21 },
    aggregate: [{ field: '*', function: 'count' }],
    groupBy: ['active', 'code'],
    groupOrderBy: [{ field: 'count', direction: 'desc' }],
    groupLimit: 10,
  }),
})

const { data } = await res.json()
console.log('Payer type counts by activity and code:', data.groups)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/person-types/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { id: 21 },
    aggregate: [{ field: '*', function: 'count' }],
    groupBy: ['active', 'code'],
    groupOrderBy: [{ field: 'count', direction: 'desc' }],
    groupLimit: 10,
  }),
})

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

## Other scenarios

The blocks below are request bodies.

Count types matching the filter without grouping or numeric functions. Records are not fetched:

```json
{ "filter": { "id": 21 } }
```

Sum, average, minimum, and maximum sort order:

```json
{
  "filter": { "id": 21 },
  "aggregate": [
    { "field": "*", "function": "count" },
    { "field": "sort", "function": "sum" },
    { "field": "sort", "function": "avg" },
    { "field": "sort", "function": "min" },
    { "field": "sort", "function": "max" }
  ]
}
```

Sum of sort order grouped by activity when the filter matches no records:

```json
{
  "filter": { "id": 21, "code": "ABSENT_DOCS_PERSON_397F55D8FB1D" },
  "aggregate": [{ "field": "sort", "function": "sum" }],
  "groupBy": "active"
}
```

For the last request, `data.count` and the sum are `0`, and `data.groups` is an empty array.

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data.count` | number | Total number of types matching the filter |
| `data.aggregates` | object | Numeric results by field and function, for example `{ "sort": { "sum": 900 } }`. An empty object when only `count` is used |
| `data.aggregates.<field>.truncated` | boolean | Included as `true` when numeric results are calculated from an incomplete set of records |
| `data.groups` | array | Included with `groupBy`. Each group contains grouping field values, `count`, and `aggregates`. The `active` value is a `boolean`; the `code` value is a `string` or `null` |
| `data.groups[].count` | number | Number of processed records in the group |
| `data.groups[].aggregates` | object | Numeric results within the group, in the same format as `data.aggregates` |
| `data.groups[].truncated` | boolean | Included as `true` for an incomplete set of records. Group counts and calculations apply to processed records |
| `data.meta.totalRecords` | number | Total number of records matching the filter |
| `data.meta.recordsProcessed` | number | Number of processed records, up to 5000. `0` without grouping or numeric functions |
| `data.meta.truncated` | boolean | `true` if fewer records were read than the total count, including because of the 5000-record ceiling, or a subpage error stopped the read. `false` for a complete response. Always `false` without grouping or numeric functions |
| `data.meta.recordsShortfall` | number | Number of unread records: `totalRecords - recordsProcessed`. Included when there is a shortfall, including one caused by the 5000-record ceiling |
| `data.meta.pageErrorSample` | object | A sample subpage error. Included when an error stopped the read |
| `data.meta.warnings` | array | Contains a warning with the code `AGGREGATE_TRUNCATED` for an incomplete set of records |
| `data.meta.groupTotal` | number | Included with `groupBy`: the number of groups before applying `groupLimit` |
| `data.meta.groupsTruncated` | boolean | Included with `groupBy`. `true` if groups were truncated by `groupLimit` |

## Response example

```json
{
  "success": true,
  "data": {
    "count": 1,
    "aggregates": {},
    "groups": [
      {
        "active": false,
        "code": "DOCS_PERSON_397F55D8FB1D",
        "count": 1,
        "aggregates": {}
      }
    ],
    "meta": {
      "totalRecords": 1,
      "recordsProcessed": 1,
      "truncated": false,
      "groupTotal": 1,
      "groupsTruncated": false
    }
  }
}
```

Without `groupBy`, the response omits `data.groups`.

## Error response example

400 — the string field `name` was passed to the numeric function `sum`:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Field 'name' is not numeric (type: string). Only number fields support sum."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `INVALID_PARAMS` | The `aggregate` is not an array or is an empty array |
| 400 | `INVALID_PARAMS` | The `aggregate` contains more than 5 expressions |
| 400 | `INVALID_PARAMS` | The function is unknown or missing |
| 400 | `INVALID_PARAMS` | The field for `count` differs from `"*"` |
| 400 | `INVALID_PARAMS` | The field for `sum`, `avg`, `min`, or `max` is missing or is `"*"` |
| 400 | `INVALID_PARAMS` | A numeric function received a non-numeric field, such as `name` |
| 400 | `INVALID_PARAMS` | The numeric function field was not found. The message lists the allowed fields: `id`, `sort` |
| 400 | `INVALID_PARAMS` | The `op`, `field`, or `function` was provided at the root without an `aggregate` array |
| 400 | `INVALID_PARAMS` | The `groupBy` is neither a string nor an array of strings, or the array contains a value of another type |
| 400 | `INVALID_PARAMS` | The grouping field is neither `active` nor `code` |
| 400 | `INVALID_PARAMS` | The `groupBy` contains more than 5 fields |
| 400 | `INVALID_PARAMS` | The `groupBy` contains a reserved name: `count`, `aggregates`, `meta`, or `groups` |
| 400 | `INVALID_PARAMS` | The `groupOrderBy` was provided without `groupBy` |
| 400 | `INVALID_PARAMS` | The `groupOrderBy` is not an array, an item has no string `field`, the field is absent from grouping or aggregate expressions, or `direction` is neither `asc` nor `desc` |
| 400 | `INVALID_PARAMS` | The `groupLimit` was provided without `groupBy` |
| 400 | `INVALID_PARAMS` | The `groupLimit` is not an integer from 1 to 1000 |
| 400 | `UNKNOWN_FILTER_FIELD` | An unknown field in `filter` |
| 400 | `INVALID_FILTER_FIELD` | A field name in `filter` starts with the unsupported prefix `@` or `!@` |
| 400 | `INVALID_FILTER_OPERATOR` | An unsupported operator, an empty condition object, or logical conditions with `$or` / `$and` |
| 400 | `INVALID_FILTER_SHAPE` | The `filter` is not a condition object, such as a string, number, or array |
| 400 | `INVALID_DUPLICATE_FILTER_FIELD` | Two filter conditions resolve to the same condition, such as `$ne` and `$nin` for one field |
| 422 | `AGGREGATION_LIMIT_EXCEEDED` | With the limit enabled for the account, more than 5000 records match the filter and the request requires numeric calculations or grouping. No records are fetched. Narrow the filter |
| 403 | `SCOPE_DENIED` | The API key lacks the `sale` scope |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `TOKEN_MISSING` | The API key has no configured tokens |
| 429 | `RATE_LIMITED` | Request limit exceeded: 300 per minute per portal, shared by all API keys for the portal. The exact value is in the `x-ratelimit-limit` header. Retry after the delay in the `Retry-After` header |

For the full list of common API errors, see [Error codes](/docs/errors).

## Known specifics

A request without grouping or numeric functions counts records without fetching them. Numeric calculations and any grouping, including grouping with only `count`, are subject to a 5000-record limit. If the account is configured to reject requests above this limit and more records match the filter, the request returns `422 AGGREGATION_LIMIT_EXCEEDED` without fetching them. Otherwise, records are read page by page, up to 5000. For an exact count over a large set, use a request without grouping or numeric functions. Narrow the filter to calculate over the entire set. See [Aggregation POST — the 5000-record ceiling](/docs/entity-api#aggregation-post-the-5000-record-ceiling).

## See also

- [List payer types](./list.md)
- [Search payer types](./search.md)
- [Payer type fields](./fields.md)
- [Payer types](/docs/entities/person-types)
- [Filtering syntax](/docs/filtering)
- [Limits and optimization](/docs/optimization)
