## Aggregate

`POST /v1/recurring-deals/aggregate`

Counts deal recurrence settings and computes numeric aggregates using a filter. Use it to analyse execution counts and repetition limits.

## Standard fields

| Field | Purpose |
|---|---|
| `counterRepeat` | Execution count. Meaningful for `sum`, `avg`, `min`, `max` |
| `limitRepeat` | Repetition count limit. Meaningful for numeric functions |
| `id`, `dealId`, `basedId`, `categoryId` | Identifiers. Accepted by numeric functions, their sum does not describe execution counts |

Standard fields are unavailable for `groupBy`: recurring deals can be grouped only by UF fields. A request with a standard field returns `INVALID_PARAMS` with an empty list of available fields.

## Custom fields (UF)

The method accepts UF fields of types `integer`, `double`, `money` for numeric functions, and any UF type for `groupBy` and filtering. Names use `UF_CRM_*` or `ufCrm_*`. For an unknown numeric UF field, `INVALID_PARAMS` lists available numeric fields. To use a UF field, configure it in your Bitrix24 account.

## Request body fields

| Field | Type | Required | Description |
|---|---|---|---|
| `aggregate` | object[] | No | Up to 5 expressions. Default: `count` over `*` |
| `aggregate[].field` | string | Yes | Expression field: `*` for `count`, a numeric field for `sum`, `avg`, `min`, `max` |
| `aggregate[].function` | string | Yes | Function: `count`, `sum`, `avg`, `min`, `max` |
| `filter` | object | No | Conditions using the [filter syntax](/docs/filtering) |
| `groupBy` | string / string[] | No | Up to 5 fields. Only UF fields that exist in the Bitrix24 account: standard fields cannot be used for grouping |
| `groupOrderBy` | object[] | No | Group sorting. Only with `groupBy` |
| `groupOrderBy[].field` | string | Yes | A grouping key, `count`, or a requested result such as `counterRepeat:sum` |
| `groupOrderBy[].direction` | string | No | Direction: `asc` or `desc`. Default: `desc` |
| `groupLimit` | number | No | With `groupBy`: an integer from 1 to 1000 |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/recurring-deals/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"aggregate":[{"field":"counterRepeat","function":"sum"},{"field":"counterRepeat","function":"avg"}],"filter":{"active":true}}'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/recurring-deals/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"aggregate":[{"field":"counterRepeat","function":"sum"},{"field":"counterRepeat","function":"avg"}],"filter":{"active":true}}'
```

### JavaScript — personal key

```javascript
const response = await fetch('https://vibecode.bitrix24.com/v1/recurring-deals/aggregate', {
  method: 'POST',
  headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify({"aggregate":[{"field":"counterRepeat","function":"sum"},{"field":"counterRepeat","function":"avg"}],"filter":{"active":true}}),
})
const result = await response.json()
if (!result.success) throw new Error(result.error.message)
console.log(result.data)
```

### JavaScript — OAuth application

```javascript
const response = await fetch('https://vibecode.bitrix24.com/v1/recurring-deals/aggregate', {
  method: 'POST',
  headers: { 'X-Api-Key': 'YOUR_APP_KEY', 'Authorization': 'Bearer USER_SESSION_TOKEN', 'Content-Type': 'application/json' },
  body: JSON.stringify({"aggregate":[{"field":"counterRepeat","function":"sum"},{"field":"counterRepeat","function":"avg"}],"filter":{"active":true}}),
})
const result = await response.json()
if (!result.success) throw new Error(result.error.message)
console.log(result.data)
```

For a `groupBy` array, supply up to 5 existing UF field names.

## Other scenarios

The blocks below are request bodies.

Count all settings:

```json
{}
```

Count with a filter:

```json
{
  "filter": {
    "id": 1
  },
  "aggregate": [
    {
      "field": "*",
      "function": "count"
    }
  ]
}
```

## Response fields

| Field | Type | Description |
|---|---|---|
| `success` | boolean | Whether the request succeeded |
| `data` | object | The aggregation result. Its fields are the `data.*` rows below |
| `data.count` | number | Total matching records |
| `data.aggregates` | object | Results by field and function |
| `data.aggregates.<field>.truncated` | boolean | Incomplete calculation marker, when set |
| `data.groups` | object[] | Groups, only with `groupBy` |
| `data.groups[].count` | number | Read records in a group |
| `data.groups[].aggregates` | object | Group results by field and function |
| `data.groups[].truncated` | boolean | Incomplete group marker, when set |
| `data.meta.totalRecords` | number | Total matching records |
| `data.meta.recordsProcessed` | number | Number of records read, an optional field |
| `data.meta.truncated` | boolean | Whether the calculation uses an incomplete selection |
| `data.meta.recordsShortfall` | number | Missing records: `totalRecords - recordsProcessed`. Also returned at the 5000-record cap |
| `data.meta.pageErrorSample` | object | Sample subpage error when reading is interrupted |
| `data.meta.warnings` | object[] | Contains `AGGREGATE_TRUNCATED` for an incomplete selection |
| `data.meta.groupTotal` | number | Number of groups before the `groupLimit` cap |
| `data.meta.groupsTruncated` | boolean | Whether the group list was limited by `groupLimit` |

## Response example

```json
{
  "success": true,
  "data": {
    "count": 2,
    "aggregates": {
      "counterRepeat": {
        "sum": 699,
        "avg": 349.5
      }
    },
    "meta": {
      "totalRecords": 2,
      "recordsProcessed": 2,
      "truncated": false
    }
  }
}
```

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

## Error response example

400 — unknown numeric aggregation field:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Field 'UF_CRM_NOT_REAL' not found. Available numeric fields: id, dealId, basedId, counterRepeat, categoryId, limitRepeat."
  }
}
```

## Errors

| HTTP | Code | Description |
|---|---|---|
| 400 | `INVALID_PARAMS` | `aggregate` is not an array or the array is empty |
| 400 | `INVALID_PARAMS` | The `aggregate` array contains more than 5 expressions |
| 400 | `INVALID_PARAMS` | An unknown or missing function. Allowed: `count`, `sum`, `avg`, `min`, `max` |
| 400 | `INVALID_PARAMS` | The field for `count` is not `*` |
| 400 | `INVALID_PARAMS` | A numeric function has no field or uses `*` |
| 400 | `INVALID_PARAMS` | A standard field does not have type `number` |
| 400 | `INVALID_PARAMS` | A UF field has a non-numeric type. The message names the UF type |
| 400 | `INVALID_PARAMS` | The field was not found. The message lists available standard and custom numeric fields |
| 400 | `INVALID_PARAMS` | Separate `op`, `field` or `function` values are supplied without an `aggregate` array |
| 400 | `INVALID_PARAMS` | `groupBy` is not a string or an array of strings |
| 400 | `INVALID_PARAMS` | The `groupBy` array contains more than 5 fields |
| 400 | `INVALID_PARAMS` | `groupBy` contains a standard field: recurring deals cannot be grouped by standard fields |
| 400 | `INVALID_PARAMS` | `groupBy` uses a reserved name: `count`, `aggregates`, `meta` or `groups` |
| 400 | `INVALID_PARAMS` | `groupOrderBy` is supplied without `groupBy` |
| 400 | `INVALID_PARAMS` | `groupLimit` is supplied without `groupBy` |
| 400 | `INVALID_PARAMS` | `groupOrderBy` is not an array |
| 400 | `INVALID_PARAMS` | A `groupOrderBy` entry is not an object with a non-empty `field` string |
| 400 | `INVALID_PARAMS` | The `groupOrderBy` direction is neither `asc` nor `desc` |
| 400 | `INVALID_PARAMS` | A sort key in `<field>:<function>` format is not requested in `aggregate` |
| 400 | `INVALID_PARAMS` | A group sort field is not a `groupBy` key, `count` or a numeric result key |
| 400 | `INVALID_PARAMS` | `groupLimit` is not an integer from 1 to 1000 |
| 400 | `UNKNOWN_FILTER_FIELD` | The filter references an unknown field. See the error message and [field definitions](./fields.md) |
| 400 | `INVALID_FILTER_FIELD` | A field name starts with the unsupported prefix `@` or `!@` |
| 400 | `INVALID_FILTER_OPERATOR` | An unknown operator, an empty condition object, or a logical condition using `$or` or `$and` |
| 400 | `INVALID_DUPLICATE_FILTER_FIELD` | Two conditions map to the same filter condition. Supply only one |
| 400 | `INVALID_FILTER_SHAPE` | `filter` is not an object |
| 422 | `AGGREGATION_LIMIT_EXCEEDED` | The numeric or grouped selection exceeds 5000 records and rejection before reading records is enabled |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `TOKEN_MISSING` | No Bitrix24 access tokens are configured |
| 403 | `SCOPE_DENIED` | The API key lacks the `crm` scope |
| 403 | `BITRIX_ACCESS_DENIED` | Bitrix24 denied access, including a missing required scope |
| 422 | `BITRIX_ERROR` | Bitrix24 rejected the operation or a plan restriction applies. See `error.message` |
| 503 | `BITRIX_TIMEOUT` | Bitrix24 did not respond in time. Retry after the interval in `Retry-After` |
| 502 | `BITRIX_UNAVAILABLE` | A connection to Bitrix24 could not be established |
| 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).

## Known specifics

Without `groupBy` and without numeric functions, the method returns a count without reading records: `recordsProcessed: 0`, `truncated: false`. A `count` request with `groupBy` requires records to be read. Numeric functions use the records read, up to 5000. Narrow the filter for an exact result.

`data.meta.truncated` becomes `true` if fewer records were read than the reported total, or a subpage error interrupted reading. A shortfall can occur below the 5000-record cap. The response always includes `recordsShortfall` or `pageErrorSample` in this case, and field and group results are marked as `truncated`.

## See also

- [List recurring deals](./list.md)
- [Get a recurring deal](./get.md)
- [Recurring deal fields](./fields.md)
- [Search recurring deals](./search.md)
- [Recurring deals](../recurring-deals.md)
- [Filtering syntax](/docs/filtering)
