## Пакетные операции с позициями отгрузок

`POST /v1/shipment-items/batch`

Массовое создание, изменение, удаление или чтение позиций отгрузок одним запросом — до 500 элементов на запись и до 50 вызовов на чтение. Это отдельный эндпоинт сущности, не путать с [универсальным batch](/docs/batch), который объединяет операции разных сущностей и ограничен 50 вызовами.

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|---------|
| `action` | string | да | Операция пакета: `create`, `update`, `delete` — запись, `list`, `get` — чтение. Один запрос выполняет одну операцию |
| `items` | array | да при `create` и `update` | Список элементов, до 500 |
| `items[].orderDeliveryId` | number | да при `create` | ID отгрузки. Список: [`GET /v1/shipments`](/docs/entities/shipments/list). При `update` не передаётся — поле только для чтения |
| `items[].basketId` | number | да при `create` | ID позиции корзины того же заказа. Список: [`GET /v1/basket-items`](/docs/entities/basket-items/list). При `update` не передаётся — поле только для чтения |
| `items[].quantity` | number | да | Количество товара в отгрузке, дробное значение сохраняется без округления. Обязательно и при `create`, и в каждом элементе `update`. Элемент `update` без `quantity` не отклоняет пакет, а завершается ошибкой внутри ответа `200` |
| `items[].xmlId` | string | нет | Внешний код |
| `items[].id` | number | да при `update` | ID позиции отгрузки. Список: [`GET /v1/shipment-items`](./list.md) |
| `ids` | number[] | да при `delete` | ID позиций отгрузок для удаления, до 500. Список: [`GET /v1/shipment-items`](./list.md) |
| `calls` | array | да при `list` и `get` | Список вызовов чтения, до 50. Каждый вызов — объект `{ "params": { … } }` |
| `calls[].params` | object | да | Для `list` — параметры [списка позиций](./list.md): `filter`, `select`, `limit`, `offset`. Для `get` — `{ "id": 1363 }` |

Набор полей элемента `items` совпадает с телом [`POST /v1/shipment-items`](./create.md) и [`PATCH /v1/shipment-items/:id`](./update.md). Полный список полей позиции — [`GET /v1/shipment-items/fields`](./fields.md).

Тело `create` — в разделе «Примеры». Тела остальных операций:

- `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 } }] }`

## Примеры

### curl — личный ключ

```bash
curl -X POST "https://vibecode.bitrix24.tech/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-приложение

```bash
curl -X POST "https://vibecode.bitrix24.tech/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 — личный ключ

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/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} → ошибка: ${item.error} ${item.message}`)
})
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/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()
```

## Поля ответа

Форма `data` зависит от операции. Для `create`, `update` и `delete`:

| Поле | Тип | Описание |
|------|-----|---------|
| `success` | boolean | Всегда `true`, если запрос прошёл проверку целиком. Результат каждого элемента — в `data.results[i].success` |
| `data.results` | array | Массив результатов в том же порядке, что `items` или `ids` запроса |
| `data.results[].index` | number | Индекс элемента, начиная с `0` |
| `data.results[].success` | boolean | Результат этой операции |
| `data.results[].id` | number | ID позиции отгрузки — созданной, изменённой или удалённой. В элементе с ошибкой отсутствует |
| `data.results[].error` | string | Код ошибки элемента с ошибкой, например `201250000001` для повтора пары `basketId` + `orderDeliveryId` |
| `data.results[].message` | string | Текст ошибки элемента с ошибкой |
| `data.summary.total` | number | Всего обработано элементов |
| `data.summary.succeeded` | number | Сколько выполнено успешно |
| `data.summary.failed` | number | Сколько завершилось ошибкой |

Для `list` и `get` поле `data` — массив результатов в порядке `calls`, а не объект с `results`. Каждый элемент несёт либо `data`, либо `error`. Код ошибки здесь лежит в объекте `error.code`, а при записи — строкой в `data.results[].error`:

| Поле | Тип | Описание |
|------|-----|---------|
| `success` | boolean | Всегда `true`, если запрос прошёл проверку целиком |
| `data[].data` | array \| object \| null | Для `list` — массив позиций, для `get` — объект позиции или `null`. Поля позиции — [Поля позиции отгрузки](./fields.md). С `select` в позиции только перечисленные поля |
| `data[].total` | number | Только `list` — количество позиций под фильтр вызова |
| `data[].hasMore` | boolean | Только `list` — есть ли позиции за пределами `limit` и `offset` |
| `data[].error.code` | string | Код ошибки вызова с ошибкой: `CALL_FAILED` для несуществующего `id`, `INVALID_PARAMS` для нецелого `id`, код проверки фильтра для неверного `filter`, например `UNKNOWN_FILTER_FIELD` |
| `data[].error.message` | string | Текст ошибки вызова с ошибкой |

## Пример ответа

`action: create` — позиция создана:

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

`action: create` — второй элемент повторил пару `basketId` + `orderDeliveryId` первого и завершился ошибкой, а верхний `success` остался `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` — два вызова по отгрузкам `975` и `985` с фильтром `{ "orderDeliveryId": { "$in": [975, 985] } }`: первый с `select: ["id", "orderDeliveryId", "quantity"]`, второй с `limit: 1` и `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
    }
  ]
}
```

## Пример ответа при ошибке

400 — в элементе `create` нет обязательного поля, пакет отклонён целиком:

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

## Ошибки

| HTTP | Код | Описание |
|------|-----|---------|
| 400 | `INVALID_BATCH_ACTION` | `action` не передан или не входит в список допустимых операций — список приходит в тексте ошибки |
| 400 | `BATCH_ITEM_VALIDATION` | `items`, `ids` или `calls` пустой либо не массив, или элемент `items` не объект |
| 400 | `BATCH_ITEM_VALIDATION` | В элементе `create` нет `orderDeliveryId`, `basketId` или `quantity`, либо передано поле только для чтения: `id`, `dateInsert`, `reservedQuantity` |
| 400 | `BATCH_ITEM_VALIDATION` | В элементе `update` нет `id`, нет ни одного поля, кроме `id`, или передано поле только для чтения: `orderDeliveryId`, `basketId` |
| 400 | `BATCH_ITEM_VALIDATION` | Значение поля не того типа, например объект в `quantity` — сообщение называет поле и ожидаемый тип |
| 400 | `BATCH_ITEM_VALIDATION` | В элементе передан объект `fields`; сообщение содержит индекс элемента и `NESTED_FIELDS_ENVELOPE`. Передавайте поля позиции плоско в корне элемента |
| 400 | `BATCH_ITEM_VALIDATION` | ID в `ids` или `items[].id` не целое неотрицательное число, например `"1.5"` |
| 400 | `BATCH_LIMIT_EXCEEDED` | Больше 500 элементов в `items` или `ids`, либо больше 50 вызовов в `calls` |
| 403 | `SCOPE_DENIED` | API-ключ не имеет скоупа `sale` |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | `create`, `update` или `delete` ключом в режиме «только чтение» — пакет не выполняется |
| 403 | `MANAGEMENT_KEY_NO_ENTITY_ACCESS` | Управляющий ключ: разделы с сущностями ему недоступны, нужен личный ключ или ключ OAuth-приложения |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `TOKEN_MISSING` | API-ключ не имеет настроенных токенов |
| 429 | `RATE_LIMITED` | Превышен лимит запросов: 30 в минуту на портал — пакетный запрос дороже одиночного чтения, все API-ключи портала делят один лимит. Точное значение — в заголовке `x-ratelimit-limit` (потолок делится на реплики). Повторите после срока из заголовка `Retry-After` |

Ответ `400` означает, что пакет отклонён целиком до записи. Ошибки отдельных элементов приходят внутри ответа `200`: при записи — в `data.results[i]` полями `error` и `message`, при чтении — в `data[i].error`. Так завершается, например, элемент `update` без `quantity` — с ошибкой «Required fields: quantity».

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Позиция системной отгрузки в `get` возвращается как `null`, а `update` по ней не проходит.** В отличие от одиночного [`GET /v1/shipment-items/:id`](./get.md), который отвечает `404`, вызов `get` в пакете возвращает `{ "data": null }` без ошибки. Элемент `update` для такой позиции завершается ошибкой, как и одиночный [`PATCH`](./update.md), а остальные элементы пакета продолжают обрабатываться. Что такое системная отгрузка — [Позиции отгрузок](/docs/entities/shipment-items).

## Смотрите также

- [Создать позицию отгрузки](./create.md)
- [Обновить позицию отгрузки](./update.md)
- [Удалить позицию отгрузки](./delete.md)
- [Список позиций отгрузок](./list.md)
- [Позиции отгрузок](/docs/entities/shipment-items)
- [Универсальный batch](/docs/batch)
