## Shipment aggregation

`POST /v1/shipments/aggregate`

Counts shipments and computes the sum, average, minimum and maximum of the delivery cost, with filtering and grouping by order or delivery service.

## Standard fields

| Field | Purpose |
|------|------------|
| `priceDelivery` | Delivery cost — the main field for `sum` / `avg` / `min` / `max`. Also available for `groupBy` |
| `basePriceDelivery`, `discountPrice` | Base delivery cost and delivery discount — numeric fields for `sum` / `avg` / `min` / `max` |
| `orderId` | Order ID — used in `groupBy` to break shipments down by order |
| `deliveryId` | Delivery service ID — used in `groupBy` to break shipments down by service |

The fields for `groupBy` are the `aggregatable` array in [`GET /v1/shipments/fields`](./fields.md). Numeric functions accept any field of type `number` from the same schema, including fields outside `aggregatable`.

**User fields.** For a non-existent field name in `sum` / `avg` / `min` / `max`, the `400 INVALID_PARAMS` response lists the allowed numeric fields. That list contains only standard shipment fields — `basePriceDelivery`, `companyId`, `deliveryId`, `discountPrice`, `empAllowDeliveryId`, `empCanceledId`, `empDeductedId`, `empMarkedId`, `empResponsibleId`, `id`, `orderId`, `priceDelivery`, `responsibleId`, with no user fields. A rejection for a field outside `aggregatable` in `groupBy` names only `orderId`, `priceDelivery`, `deliveryId` as allowed.

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|---------|
| `aggregate` | array | no | Array of expressions `{ "field": "priceDelivery", "function": "sum" }`, up to 5 per request. Functions: `count`, `sum`, `avg`, `min`, `max`. For `count` the field is `"*"`. Without the parameter, only `count` is returned |
| `filter` | object | no | Filtering by the fields of [`GET /v1/shipments/fields`](./fields.md).<br>[Filtering syntax](/docs/filtering). Example: `{ "orderId": 1027 }` |
| `groupBy` | string \| string[] | no | A field or an array of fields to group by, up to 5 fields. Allowed values come from the `aggregatable` array: `orderId`, `priceDelivery`, `deliveryId` |
| `groupOrderBy` | array | no | Group sorting: `[{ "field": "priceDelivery:sum", "direction": "desc" }]`. `field` accepts `count`, a field name from `groupBy`, or `<field>:<function>` from `aggregate`. Works only together with `groupBy` |
| `groupLimit` | number | no | Limits the number of returned groups, from 1 to 1000. Works only together with `groupBy` |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/shipments/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "orderId": 1027 },
    "aggregate": [
      { "field": "*", "function": "count" },
      { "field": "priceDelivery", "function": "sum" },
      { "field": "priceDelivery", "function": "avg" }
    ],
    "groupBy": "deliveryId"
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/shipments/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "orderId": 1027 },
    "aggregate": [
      { "field": "*", "function": "count" },
      { "field": "priceDelivery", "function": "sum" },
      { "field": "priceDelivery", "function": "avg" }
    ],
    "groupBy": "deliveryId"
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/shipments/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { orderId: 1027 },
    aggregate: [
      { field: '*', function: 'count' },
      { field: 'priceDelivery', function: 'sum' },
      { field: 'priceDelivery', function: 'avg' },
    ],
    groupBy: 'deliveryId',
  }),
})

const { success, data } = await res.json()
// data.groups — delivery cost by delivery service
console.log(data.groups)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/shipments/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { orderId: 1027 },
    aggregate: [
      { field: '*', function: 'count' },
      { field: 'priceDelivery', function: 'sum' },
      { field: 'priceDelivery', function: 'avg' },
    ],
    groupBy: 'deliveryId',
  }),
})

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

## Other scenarios

The blocks below are request bodies.

Shipment count for an order only — without the `aggregate` array no records are fetched, and `data.meta.recordsProcessed` in the response is `0`:

```json
{ "filter": { "orderId": 1027 } }
```

Grouping by two fields — by order and delivery service:

```json
{
  "aggregate": [{ "field": "*", "function": "count" }],
  "groupBy": ["orderId", "deliveryId"]
}
```

Sum and average of the delivery cost without grouping:

```json
{
  "filter": { "orderId": 1027 },
  "aggregate": [
    { "field": "*", "function": "count" },
    { "field": "priceDelivery", "function": "sum" },
    { "field": "priceDelivery", "function": "avg" }
  ]
}
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data.count` | number | Number of shipments matching the filter |
| `data.aggregates` | object | Results of numeric functions by field: `{ "priceDelivery": { "sum": 500, "avg": 500 } }`. An empty object if `aggregate` contains only `count` |
| `data.groups` | array | Present only with `groupBy`. Each element holds the grouping field values, `count` and `aggregates` for the group |
| `data.meta.totalRecords` | number | Total number of records matching the filter |
| `data.meta.recordsProcessed` | number | Number of processed records, up to 5000 |
| `data.meta.truncated` | boolean | `true` if the numbers were not computed over all records matching 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 subpage error. The size of the shortfall arrives in `data.meta.recordsShortfall`, the cut-short slice in `data.meta.pageErrorSample`. Always present, `false` on a complete response. If the request has neither `groupBy` nor numeric functions, no records are fetched and the flag is always `false` |
| `data.meta.groupTotal` | number | Present only with `groupBy` — the number of groups before `groupLimit` is applied |
| `data.meta.groupsTruncated` | boolean | Present only with `groupBy`. `true` if the group list was cut by `groupLimit` |

## Response example

```json
{
  "success": true,
  "data": {
    "count": 1,
    "aggregates": {
      "priceDelivery": {
        "sum": 500,
        "avg": 500
      }
    },
    "groups": [
      {
        "deliveryId": 1,
        "count": 1,
        "aggregates": {
          "priceDelivery": {
            "sum": 500,
            "avg": 500
          }
        }
      }
    ],
    "meta": {
      "totalRecords": 1,
      "recordsProcessed": 1,
      "truncated": false,
      "groupTotal": 1,
      "groupsTruncated": false
    }
  }
}
```

## Error response example

400 — the field is not in the schema, the message lists the allowed numeric fields:

```json
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Field 'weight' not found. Available numeric fields: basePriceDelivery, companyId, deliveryId, discountPrice, empAllowDeliveryId, empCanceledId, empDeductedId, empMarkedId, empResponsibleId, id, orderId, priceDelivery, responsibleId."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `INVALID_PARAMS` | `count` is passed with a field other than `"*"` |
| 400 | `INVALID_PARAMS` | `aggregate` is passed as a non-array or an empty array |
| 400 | `INVALID_PARAMS` | `aggregate` has more than 5 expressions |
| 400 | `INVALID_PARAMS` | Unknown function — the message lists the allowed ones |
| 400 | `INVALID_PARAMS` | `sum` / `avg` / `min` / `max` without a field or with the field `"*"` |
| 400 | `INVALID_PARAMS` | The field for `sum` / `avg` / `min` / `max` is not numeric, for example `trackingNumber` — the message names its type |
| 400 | `INVALID_PARAMS` | The field for `sum` / `avg` / `min` / `max` is not in the schema — the message contains the list of numeric fields |
| 400 | `INVALID_PARAMS` | The `op`, `field` or `function` fields are passed at the top level of the body instead of the `aggregate` array |
| 400 | `INVALID_PARAMS` | `groupBy` is neither a string nor an array of strings |
| 400 | `INVALID_PARAMS` | A field in `groupBy` is outside the `aggregatable` array — the message contains the list of allowed ones |
| 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` or `groupLimit` is passed without `groupBy`, or its value is invalid |
| 400 | `UNKNOWN_FILTER_FIELD` | Unknown field in `filter` — the message contains the list of shipment fields |
| 400 | `INVALID_FILTER_FIELD` | A field name in `filter` starts with the unsupported prefix `@` or `!@` |
| 400 | `INVALID_FILTER_OPERATOR` | Unsupported operator in `filter`, an empty object in a condition value, or the `$or` logical condition |
| 400 | `INVALID_FILTER_SHAPE` | `filter` is not an object of conditions, for example a string, a number or an array |
| 400 | `INVALID_DUPLICATE_FILTER_FIELD` | Two filter conditions reduce to one Bitrix24 condition, for example `$ne` and `$nin` on the same field. Pass only one of them |
| 403 | `SCOPE_DENIED` | The API key has no `sale` scope |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is not passed |
| 401 | `TOKEN_MISSING` | The API key has no configured tokens |
| 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` versus numeric functions.** A request without `groupBy` and without numeric functions is handled by a single request to Bitrix24 — no records are fetched, and the response time does not depend on the number of shipments. As soon as the request has `sum` / `avg` / `min` / `max` or `groupBy`, including together with a single `count`, records are loaded page by page, up to 5000, and computed on the Vibecode side. With `meta.truncated: true`, the `truncated: true` mark is also set inside the object of each field in `data.aggregates` and on each element of `data.groups`, and `data.meta.warnings` carries the `AGGREGATE_TRUNCATED` warning. For an exact number on a large selection, use a request without `groupBy` and numeric functions, or narrow the filter, for example by `orderId`. Full breakdown — [Aggregation POST — the 5000-record ceiling](/docs/entity-api#aggregation-post-the-5000-record-ceiling).

## See also

- [List shipments](./list.md)
- [Search shipments](./search.md)
- [Shipment fields](./fields.md)
- [Shipments](/docs/entities/shipments)
- [Filtering syntax](/docs/filtering)
- [Limits and optimization](/docs/optimization)
