## Aggregate order properties

`POST /v1/order-properties/aggregate`

Counts order properties by filter and grouping fields, and calculates the sum, average, minimum, and maximum of numeric fields.

## Standard fields

| Field | Purpose |
|------|------------|
| `personTypeId` | Payer type ID from [`GET /v1/person-types`](/docs/entities/person-types/list). For `groupBy` |
| `type` | Property type — for `groupBy` |
| `required` | Whether the field is required when placing an order — for `groupBy` |
| `active` | Active flag — for `groupBy` |
| `sort` | Sort order — for `sum`, `avg`, `min`, `max` |
| `inputFieldLocation` | Deprecated field. Bitrix24 does not use it — a numeric field |
| `id` | Property ID from [list order properties](./list.md). A numeric field |
| `propsGroupId` | Group ID from a property with the same payer type in [list order properties](./list.md). A numeric field |

Fields for `groupBy` are listed in `data.aggregatable` of [`GET /v1/order-properties/fields`](./fields.md). Numeric functions accept fields of type `number` from the same schema: `id`, `personTypeId`, `propsGroupId`, `sort`, `inputFieldLocation`.

## Request body fields

| Field | Type | Required | Description |
|------|-----|:-----:|---------|
| `aggregate` | array | no | An array of expressions such as `{ "field": "sort", "function": "sum" }`, with 1 to 5 expressions. Functions: `count`, `sum`, `avg`, `min`, `max`. For `count`, the field is `"*"`. When omitted, defaults to `[{ "field": "*", "function": "count" }]` |
| `filter` | object | no | Filter by fields from [`GET /v1/order-properties/fields`](./fields.md).<br>[Filtering syntax](/docs/filtering). Example: `{ "id": 125 }` |
| `groupBy` | string \| string[] | no | A grouping field or an array of up to 5 fields. Allowed fields: `personTypeId`, `type`, `required`, `active` |
| `groupOrderBy` | array | no | Sort groups: `[{ "field": "count", "direction": "desc" }]`. `field` accepts `count`, a field from `groupBy`, or `<field>:<function>` from `aggregate`. Directions: `asc`, `desc`. If `direction` is omitted, defaults to `desc`. Requires `groupBy` |
| `groupLimit` | number | no | Number of returned groups: an integer from 1 to 1000. When omitted, groups are not limited. Requires `groupBy` |

## Examples

### curl — personal key

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

### curl — OAuth application

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

### JavaScript — personal key

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

const { data } = await res.json()
console.log('Properties by type and active status:', data.groups)
```

### JavaScript — OAuth application

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

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

## Other scenarios

The blocks below are request bodies.

Count properties matching the filter without grouping or numeric functions — records are not downloaded:

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

Sum the sort order with grouping when the filter matches no records:

```json
{
  "filter": { "id": 125, "code": "ABSENT_DELIVERY_COMMENT" },
  "aggregate": [{ "field": "sort", "function": "sum" }],
  "groupBy": ["type", "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 properties matching the filter |
| `data.aggregates` | object | Numeric results by field and function, such as `{ "sort": { "sum": 500 } }`. Empty for `count` alone |
| `data.aggregates.<field>.truncated` | boolean | Present as `true` when numeric results were calculated from an incomplete set of records |
| `data.groups` | array | Present with `groupBy`. Each group contains grouping field values, `count`, and `aggregates`. `personTypeId` is a `number`, `type` is a `string`, and `required` and `active` are `boolean` |
| `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 | Present as `true` for an incomplete set of records. The group's count and calculations cover the 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` when fewer records were read than the reported total, including due to the 5000-record cap, or when reading stopped on a subpage error. `false` for a complete response. Always `false` without grouping or numeric functions |
| `data.meta.recordsShortfall` | number | Number of unread records: `totalRecords - recordsProcessed`. Present when records are missing, including at the 5000-record cap |
| `data.meta.pageErrorSample` | object | A sample subpage error. Present when reading stopped on an error |
| `data.meta.warnings` | array | Contains a warning with code `AGGREGATE_TRUNCATED` for an incomplete set of records |
| `data.meta.groupTotal` | number | Present with `groupBy`: the number of groups before `groupLimit` is applied |
| `data.meta.groupsTruncated` | boolean | Present with `groupBy`. `true` when `groupLimit` limits the returned groups |

## Response example

```json
{
  "success": true,
  "data": {
    "count": 1,
    "aggregates": {
      "sort": {
        "sum": 500
      }
    },
    "groups": [
      {
        "type": "STRING",
        "active": false,
        "count": 1,
        "aggregates": {
          "sort": {
            "sum": 500
          }
        }
      }
    ],
    "meta": {
      "totalRecords": 1,
      "recordsProcessed": 1,
      "truncated": false,
      "groupTotal": 1,
      "groupsTruncated": false
    }
  }
}
```

## 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` | `aggregate` is not an array or is empty |
| 400 | `INVALID_PARAMS` | `aggregate` contains more than 5 expressions |
| 400 | `INVALID_PARAMS` | The function is unknown or missing |
| 400 | `INVALID_PARAMS` | The field for `count` is not `"*"` |
| 400 | `INVALID_PARAMS` | The field for `sum`, `avg`, `min`, or `max` is missing or is `"*"` |
| 400 | `INVALID_PARAMS` | A nonnumeric field such as `name` or `defaultValue` was passed to a numeric function |
| 400 | `INVALID_PARAMS` | The numeric function field was not found. The message lists allowed fields: `id`, `personTypeId`, `propsGroupId`, `sort`, `inputFieldLocation` |
| 400 | `INVALID_PARAMS` | `op`, `field`, or `function` was passed at the top level without the `aggregate` array |
| 400 | `INVALID_PARAMS` | `groupBy` is not a string or an array of strings, or the array contains a value of another type |
| 400 | `INVALID_PARAMS` | The grouping field is not one of `personTypeId`, `type`, `required`, `active` |
| 400 | `INVALID_PARAMS` | `groupBy` has more than 5 fields |
| 400 | `INVALID_PARAMS` | `groupBy` contains a reserved name: `count`, `aggregates`, `meta`, `groups` |
| 400 | `INVALID_PARAMS` | `groupOrderBy` was passed without `groupBy` |
| 400 | `INVALID_PARAMS` | `groupLimit` was passed without `groupBy` |
| 400 | `INVALID_PARAMS` | `groupOrderBy` is not an array |
| 400 | `INVALID_PARAMS` | A `groupOrderBy` item is not an object with a nonempty string `field` |
| 400 | `INVALID_PARAMS` | `groupOrderBy.direction` is neither `asc` nor `desc` |
| 400 | `INVALID_PARAMS` | The `<field>:<function>` key in `groupOrderBy.field` is absent from the numeric `aggregate` expressions |
| 400 | `INVALID_PARAMS` | `groupOrderBy.field` is not `count`, a field from `groupBy`, or a numeric `aggregate` expression key |
| 400 | `INVALID_PARAMS` | `groupLimit` is not an integer from 1 to 1000 |
| 400 | `UNKNOWN_FILTER_FIELD` | Unknown field in `filter` |
| 400 | `INVALID_FILTER_FIELD` | A field name in `filter` starts with the unsupported prefix `@` or `!@` |
| 400 | `INVALID_FILTER_OPERATOR` | Unsupported operator, an empty condition object, or `$or` / `$and` logical conditions |
| 400 | `INVALID_FILTER_SHAPE` | `filter` is not an object of conditions, for example a string, number, or array |
| 400 | `INVALID_DUPLICATE_FILTER_FIELD` | Two filter conditions resolve to the same condition, for example `$ne` and `$nin` for one field |
| 422 | `AGGREGATION_LIMIT_EXCEEDED` | When the cap is enforced for the portal, more than 5000 records match the filter and the request requires numeric calculations or grouping. Records are not downloaded. Narrow the filter |
| 422 | `BITRIX_ERROR` | The key's user does not have permission in Bitrix24. The Bitrix24 code is in `error.b24Code`: `200040300010` |
| 403 | `BITRIX_ACCESS_DENIED` | The portal credentials do not have the `sale` scope (`insufficient_scope`) |
| 422 | `BITRIX_ERROR` | A business error with the Bitrix24 message |
| 503 | `BITRIX_TIMEOUT` | Bitrix24 did not respond in time. Retry after the interval in `Retry-After` |
| 502 | `BITRIX_UNAVAILABLE` | Bitrix24 could not be reached |
| 403 | `SCOPE_DENIED` | The API key lacks the `sale` scope |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `TOKEN_MISSING` | No tokens are configured for the API key |
| 429 | `RATE_LIMITED` | The request rate limit was exceeded. The effective limit is in the `x-ratelimit-limit` header. Retry after the interval in the `Retry-After` header |

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

## See also

- [List order properties](./list.md)
- [Search order properties](./search.md)
- [Order property fields](./fields.md)
- [Order properties](/docs/entities/order-properties)
- [Filtering syntax](/docs/filtering)
- [Limits and optimization](/docs/optimization)
- [Aggregation POST — the 5000-record ceiling](/docs/entity-api#aggregation-post-the-5000-record-ceiling)
