## Aggregate shipment items

`POST /v1/shipment-items/aggregate`

Counts shipment items and computes the sum, average, minimum and maximum of the product quantity, with filtering and grouping by shipment or basket item.

## Standard fields

| Field | Purpose |
|------|------------|
| `quantity` | Product quantity in the item — the main field for `sum` / `avg` / `min` / `max`. Also available for `groupBy` |
| `reservedQuantity` | Reserved quantity — a numeric field for `sum` / `avg` / `min` / `max` and `groupBy` |
| `orderDeliveryId` | Shipment ID from [`GET /v1/shipments`](/docs/entities/shipments/list) — used in `groupBy` to break items down by shipment |
| `basketId` | Basket item ID from [`GET /v1/basket-items`](/docs/entities/basket-items/list) — used in `groupBy` to break items down by basket product |

The fields allowed in `groupBy` are listed in the `aggregatable` array of [`GET /v1/shipment-items/fields`](./fields.md). Numeric functions accept any field of type `number` from the same schema, including `id`, which is outside `aggregatable`.

**User fields.** If you pass a nonexistent field to `sum` / `avg` / `min` / `max`, the `400 INVALID_PARAMS` response lists the allowed numeric fields. That list contains only the standard shipment item fields — `basketId`, `id`, `orderDeliveryId`, `quantity`, `reservedQuantity`. It contains no user fields.

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|---------|
| `aggregate` | array | no | Array of expressions `{ "field": "quantity", "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/shipment-items/fields`](./fields.md).<br>[Filtering syntax](/docs/filtering). Example: `{ "orderDeliveryId": 985 }` |
| `groupBy` | string \| string[] | no | A field or array of fields to group by, up to 5 fields. Allowed values come from the `aggregatable` array: `quantity`, `reservedQuantity`, `orderDeliveryId`, `basketId` |
| `groupOrderBy` | array | no | Group sorting: `[{ "field": "quantity:sum", "direction": "desc" }]`. The `field` takes `count`, a field name from `groupBy`, or `<field>:<function>` from `aggregate`. Works only together with `groupBy` |
| `groupLimit` | number | no | Limit on 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/shipment-items/aggregate" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "orderDeliveryId": { "$in": [975, 985] } },
    "aggregate": [
      { "field": "*", "function": "count" },
      { "field": "quantity", "function": "sum" },
      { "field": "quantity", "function": "avg" }
    ],
    "groupBy": "orderDeliveryId"
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/shipment-items/aggregate" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "filter": { "orderDeliveryId": { "$in": [975, 985] } },
    "aggregate": [
      { "field": "*", "function": "count" },
      { "field": "quantity", "function": "sum" },
      { "field": "quantity", "function": "avg" }
    ],
    "groupBy": "orderDeliveryId"
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/shipment-items/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { orderDeliveryId: { $in: [975, 985] } },
    aggregate: [
      { field: '*', function: 'count' },
      { field: 'quantity', function: 'sum' },
      { field: 'quantity', function: 'avg' },
    ],
    groupBy: 'orderDeliveryId',
  }),
})

const { success, data } = await res.json()
// data.groups — product quantity per shipment
console.log(data.groups)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/shipment-items/aggregate', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    filter: { orderDeliveryId: { $in: [975, 985] } },
    aggregate: [
      { field: '*', function: 'count' },
      { field: 'quantity', function: 'sum' },
      { field: 'quantity', function: 'avg' },
    ],
    groupBy: 'orderDeliveryId',
  }),
})

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

To group by several fields, pass an array: `"groupBy": ["orderDeliveryId", "basketId"]`, up to 5 fields.

## Other scenarios

The blocks below are request bodies.

Only the number of items in a shipment — without the `aggregate` array no records are loaded, and `data.meta.recordsProcessed` in the response is `0`:

```json
{ "filter": { "orderDeliveryId": 985 } }
```

Shipments with the largest product quantity — the first two groups by descending sum of `quantity`:

```json
{
  "aggregate": [{ "field": "quantity", "function": "sum" }],
  "groupBy": "orderDeliveryId",
  "groupOrderBy": [{ "field": "quantity:sum", "direction": "desc" }],
  "groupLimit": 2
}
```

Total and maximum product quantity across all items, without grouping:

```json
{
  "aggregate": [
    { "field": "*", "function": "count" },
    { "field": "quantity", "function": "sum" },
    { "field": "quantity", "function": "max" }
  ]
}
```

## Response fields

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` on success |
| `data.count` | number | Number of shipment items matching the filter |
| `data.aggregates` | object | Results of numeric functions by field: `{ "quantity": { "sum": 16, "avg": 5.33 } }`. An empty object if `aggregate` contains only `count`: the `count` result comes in `data.count` and `data.groups[].count`, not here |
| `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 records processed, 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 comes 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 loaded 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": 3,
    "aggregates": {
      "quantity": {
        "sum": 16,
        "avg": 5.333333333333333
      }
    },
    "groups": [
      {
        "orderDeliveryId": 975,
        "count": 1,
        "aggregates": {
          "quantity": {
            "sum": 1,
            "avg": 1
          }
        }
      },
      {
        "orderDeliveryId": 985,
        "count": 2,
        "aggregates": {
          "quantity": {
            "sum": 15,
            "avg": 7.5
          }
        }
      }
    ],
    "meta": {
      "totalRecords": 3,
      "recordsProcessed": 3,
      "truncated": false,
      "groupTotal": 2,
      "groupsTruncated": false
    }
  }
}
```

Without `groupBy`, the `data.groups`, `data.meta.groupTotal` and `data.meta.groupsTruncated` fields are absent from the response.

## Error response example

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

```json
{
  "success": false,
  "error": {
    "code": "INVALID_PARAMS",
    "message": "Field 'weight' not found. Available numeric fields: basketId, id, orderDeliveryId, quantity, reservedQuantity."
  }
}
```

## 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` contains 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 `xmlId` — 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 `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, for example `xmlId` — the message contains the list of allowed ones |
| 400 | `INVALID_PARAMS` | `groupBy` contains 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 the value of one of them is invalid |
| 400 | `UNKNOWN_FILTER_FIELD` | Unknown field in `filter` — the message contains the list of shipment item fields |
| 400 | `INVALID_FILTER_FIELD` | A field name in `filter` starts with the unsupported prefix `@` or `!@` |
| 400 | `INVALID_FILTER_OPERATOR` | Unsupported operator in `filter` or a logical condition, for example `$or` |
| 400 | `INVALID_FILTER_SHAPE` | `filter` is not passed as an object of conditions, for example as a string |
| 400 | `INVALID_DUPLICATE_FILTER_FIELD` | Two filter conditions reduce to one Bitrix24 condition, for example `$ne` and `$nin` on the same field. Pass one of them |
| 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, and all API keys of the portal share one limit. The exact value is in the `x-ratelimit-limit` header (the ceiling is divided across replicas). Retry after the period 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 runs as a single request to Bitrix24 — no records are loaded, and the response time does not depend on the number of items. As soon as the request contains `sum` / `avg` / `min` / `max` or `groupBy`, including together with a single `count`, records are loaded page by page, at most 5000, and aggregated on the Vibecode side. With `meta.truncated: true`, the `truncated: true` marker is set both inside each field object 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 result set, use a request without `groupBy` and numeric functions, or narrow the filter, for example by `orderDeliveryId`. Full details — [Aggregation POST — the 5000-record ceiling](/docs/entity-api#aggregation-post-the-5000-record-ceiling).

**The count includes items of the system shipment.** The selection includes the same records as the [item list](./list.md), including items of the system shipment, which holds the unallocated remainder of the basket. Therefore, the sum of `quantity` for a basket item without a shipment filter equals the product quantity in the basket. To count only what has been shipped, filter by the `orderDeliveryId` of shipments from [`GET /v1/shipments`](/docs/entities/shipments/list).

## See also

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