## Batch operations on shipment items

`POST /v1/shipment-items/batch`

Create, update, delete or read shipment items in bulk with one request — up to 500 items per write and up to 50 calls per read. This is a dedicated endpoint for this entity, not to be confused with the [universal batch](/docs/batch), which combines operations on different entities and is limited to 50 calls.

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|---------|
| `action` | string | yes | Batch operation: `create`, `update`, `delete` for writes, `list`, `get` for reads. One request performs one operation |
| `items` | array | yes for `create` and `update` | List of items, up to 500 |
| `items[].orderDeliveryId` | number | yes for `create` | Shipment ID. List: [`GET /v1/shipments`](/docs/entities/shipments/list). Not passed in `update` — the field is read-only |
| `items[].basketId` | number | yes for `create` | ID of a basket item of the same order. List: [`GET /v1/basket-items`](/docs/entities/basket-items/list). Not passed in `update` — the field is read-only |
| `items[].quantity` | number | yes | Product quantity in the shipment; a fractional value is stored without rounding. Required both in `create` and in every `update` item. An `update` item without `quantity` does not reject the batch — it fails inside a `200` response |
| `items[].xmlId` | string | no | External code |
| `items[].id` | number | yes for `update` | Shipment item ID. List: [`GET /v1/shipment-items`](./list.md) |
| `ids` | number[] | yes for `delete` | IDs of the shipment items to delete, up to 500. List: [`GET /v1/shipment-items`](./list.md) |
| `calls` | array | yes for `list` and `get` | List of read calls, up to 50. Each call is an object `{ "params": { … } }` |
| `calls[].params` | object | yes | For `list` — the [item list](./list.md) parameters: `filter`, `select`, `limit`, `offset`. For `get` — `{ "id": 1363 }` |

The field set of an `items` element matches the body of [`POST /v1/shipment-items`](./create.md) and [`PATCH /v1/shipment-items/:id`](./update.md). The full list of item fields is in [`GET /v1/shipment-items/fields`](./fields.md).

The `create` body is in the "Examples" section. Bodies of the other operations:

- `update` — `{ "action": "update", "items": [{ "id": 1363, "quantity": 0.75 }] }`
- `delete` — `{ "action": "delete", "ids": [1363] }`
- `list` — `{ "action": "list", "calls": [{ "params": { "filter": { "orderDeliveryId": 985 }, "limit": 50 } }] }`
- `get` — `{ "action": "get", "calls": [{ "params": { "id": 1363 } }] }`

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/shipment-items/batch" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create",
    "items": [
      { "orderDeliveryId": 1215, "basketId": 1387, "quantity": 0.5 }
    ]
  }'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/shipment-items/batch" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "create",
    "items": [
      { "orderDeliveryId": 1215, "basketId": 1387, "quantity": 0.5 }
    ]
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/shipment-items/batch', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    action: 'create',
    items: [
      { orderDeliveryId: 1215, basketId: 1387, quantity: 0.5 },
    ],
  }),
})

const { data } = await res.json()
data.results.forEach((item) => {
  if (item.success) console.log(`#${item.index} → id=${item.id}`)
  else console.log(`#${item.index} → error: ${item.error} ${item.message}`)
})
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/shipment-items/batch', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    action: 'create',
    items: [
      { orderDeliveryId: 1215, basketId: 1387, quantity: 0.5 },
    ],
  }),
})

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

## Response fields

The shape of `data` depends on the operation. For `create`, `update` and `delete`:

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` if the request passed validation as a whole. The result of each item is in `data.results[i].success` |
| `data.results` | array | Array of results in the same order as the request's `items` or `ids` |
| `data.results[].index` | number | Item index, starting from `0` |
| `data.results[].success` | boolean | Result of this operation |
| `data.results[].id` | number | ID of the shipment item — created, updated or deleted. Absent in a failed item |
| `data.results[].error` | string | Error code of a failed item, for example `201250000001` for a repeated `basketId` + `orderDeliveryId` pair |
| `data.results[].message` | string | Error text of a failed item |
| `data.summary.total` | number | Total items processed |
| `data.summary.succeeded` | number | How many succeeded |
| `data.summary.failed` | number | How many failed |

For `list` and `get`, the `data` field is an array of results in the order of `calls`, not an object with `results`. Each element carries either `data` or `error`. Here the error code is in the `error` object as `error.code`, while for writes it is a string in `data.results[].error`:

| Field | Type | Description |
|------|-----|---------|
| `success` | boolean | Always `true` if the request passed validation as a whole |
| `data[].data` | array \| object \| null | For `list` — an array of items, for `get` — an item object or `null`. Item fields — [Shipment item fields](./fields.md). With `select`, an item contains only the listed fields |
| `data[].total` | number | `list` only — number of items matching the call's filter |
| `data[].hasMore` | boolean | `list` only — whether there are items beyond `limit` and `offset` |
| `data[].error.code` | string | Error code of a failed call: `CALL_FAILED` for a non-existent `id`, `INVALID_PARAMS` for a non-integer `id`, a filter validation code for an invalid `filter`, for example `UNKNOWN_FILTER_FIELD` |
| `data[].error.message` | string | Error text of a failed call |

## Response example

`action: create` — the item is created:

```json
{
  "success": true,
  "data": {
    "results": [
      { "index": 0, "success": true, "id": 1363 }
    ],
    "summary": { "total": 1, "succeeded": 1, "failed": 0 }
  }
}
```

`action: create` — the second item repeated the `basketId` + `orderDeliveryId` pair of the first one and failed, while the top-level `success` stayed `true`:

```json
{
  "success": true,
  "data": {
    "results": [
      { "index": 0, "success": true, "id": 1361 },
      {
        "index": 1,
        "success": false,
        "error": "201250000001",
        "message": "Duplicate entry for key [basketId, orderDeliveryId]"
      }
    ],
    "summary": { "total": 2, "succeeded": 1, "failed": 1 }
  }
}
```

`action: list` — two calls for shipments `975` and `985` with the filter `{ "orderDeliveryId": { "$in": [975, 985] } }`: the first with `select: ["id", "orderDeliveryId", "quantity"]`, the second with `limit: 1` and `offset: 1`:

```json
{
  "success": true,
  "data": [
    {
      "data": [
        { "id": 875, "orderDeliveryId": 975, "quantity": 1 },
        { "id": 885, "orderDeliveryId": 985, "quantity": 10 },
        { "id": 887, "orderDeliveryId": 985, "quantity": 5 }
      ],
      "total": 3,
      "hasMore": false
    },
    {
      "data": [
        {
          "basketId": 953,
          "dateInsert": "2025-03-03T22:02:39.000Z",
          "id": 885,
          "orderDeliveryId": 985,
          "quantity": 10,
          "reservedQuantity": 0,
          "xmlId": "bx_67c618efc3256"
        }
      ],
      "total": 3,
      "hasMore": true
    }
  ]
}
```

## Error response example

400 — a `create` item lacks a required field, the whole batch is rejected:

```json
{
  "success": false,
  "error": {
    "code": "BATCH_ITEM_VALIDATION",
    "message": "Item at index 0: field 'basketId' is required to create shipmentItem"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|---------|
| 400 | `INVALID_BATCH_ACTION` | `action` is not passed or is not one of the allowed operations — the list comes in the error text |
| 400 | `BATCH_ITEM_VALIDATION` | `items`, `ids` or `calls` is empty or not an array, or an `items` element is not an object |
| 400 | `BATCH_ITEM_VALIDATION` | A `create` item lacks `orderDeliveryId`, `basketId` or `quantity`, or a read-only field is passed: `id`, `dateInsert`, `reservedQuantity` |
| 400 | `BATCH_ITEM_VALIDATION` | An `update` item lacks `id`, has no field other than `id`, or a read-only field is passed: `orderDeliveryId`, `basketId` |
| 400 | `BATCH_ITEM_VALIDATION` | A field value has the wrong type, for example an object in `quantity` — the message names the field and the expected type |
| 400 | `BATCH_ITEM_VALIDATION` | An ID in `ids` or `items[].id` is not a non-negative integer, for example `"1.5"` |
| 400 | `BATCH_LIMIT_EXCEEDED` | More than 500 elements in `items` or `ids`, or more than 50 calls in `calls` |
| 403 | `SCOPE_DENIED` | The API key lacks the `sale` scope |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | `create`, `update` or `delete` with a read-only key — the batch is not executed |
| 403 | `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | Management key: entity sections are not available to it, a personal key or an OAuth application key is required |
| 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: 30 requests per minute per portal (a batch costs more than a plain read), 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 |

A `400` response means the whole batch is rejected before any write. Errors of individual items come inside a `200` response: for writes — in `data.results[i]` as the `error` and `message` fields, for reads — in `data[i].error`. This is how, for example, an `update` item without `quantity` fails — with the error "Required fields: quantity".

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

**An item of a system shipment is returned as `null` by `get`, and `update` on it fails.** Unlike the single [`GET /v1/shipment-items/:id`](./get.md), which responds with `404`, a `get` call in the batch returns `{ "data": null }` without an error. An `update` element for such an item fails, just like the single [`PATCH`](./update.md), while the other items of the batch continue to be processed. For what a system shipment is, see [Shipment items](/docs/entities/shipment-items).

## See also

- [Create a shipment item](./create.md)
- [Update a shipment item](./update.md)
- [Delete a shipment item](./delete.md)
- [List shipment items](./list.md)
- [Shipment items](/docs/entities/shipment-items)
- [Universal batch](/docs/batch)
