> Источник: https://apipay.kz/guides/scheta-s-korzinoy-cart-items-ofd · Обновлено: 2026-07-06 · apipay.kz
> ApiPay — независимый сервис приёма платежей поверх вашего Kaspi Pay.

# Счета с корзиной (cart_items): как исправить ошибку 422?

**TL;DR.** Ошибки 422 вокруг `cart_items` означают одно: ваша организация работает с каталогом товаров (обычно это Kaspi ОФД / режим «Касса»), и счёт собирается из позиций каталога. Счёт по номеру (`POST /invoices`) такая организация может выставить и одной суммой в `amount`; для QR-счёта (`POST /invoices/qr`) и печатного QR (`POST /static-qr`) корзина обязательна. Формат: `cart_items[]` с обязательными `catalog_item_id` и `count` (1–100 позиций). У позиции каталога **должна быть задана цена** — пустую каталожную цену поле `price` в строке не заменяет ни на одном пути — задайте её в каталоге. Скидка — `discount_percentage` (1–99%) на весь чек, её **можно** сочетать с переопределённой ценой. Переданный `amount` при корзине игнорируется — сумма считается по позициям.

## Коротко

| Вопрос | Ответ |
|---|---|
| Когда нужна корзина | Организация с каталогом (Kaspi ОФД / «Касса»): обязательна для `POST /invoices/qr`, `POST /static-qr` и `POST /subscriptions`, для счёта по номеру — по желанию; без каталога `cart_items` слать нельзя |
| Схема позиции | `{ "catalog_item_id": 1, "count": 2 }` — оба поля обязательны |
| Позиций в счёте | От 1 до 100 |
| Цена позиции | Берётся из каталога; поле `price` в запросе переопределяет её для этой строки. Пустую каталожную цену `price` не заменяет ни на одном пути |
| Скидка | `discount_percentage` 1–99% на весь чек, только вместе с `cart_items`; сочетается с переопределённой ценой строки |
| `amount` при корзине | Игнорируется — сумма считается по позициям |

## Когда счёт требует cart_items?

Если у организации подключена Kaspi ОФД (касса), Kaspi ожидает чек с позициями — поэтому у таких организаций в ApiPay включается каталог, и счёт собирается из его позиций. Счёт по номеру телефона (`POST /invoices`) такая организация выставляет и без корзины, одной суммой в `amount`, а `POST /invoices/qr`, `POST /static-qr` и подписка `POST /subscriptions` принимаются **только с корзиной**:

```json
{ "message": "This organization requires cart items. Include cart_items in request." }
```

Обратное правило действует всегда: **организация без каталога не может слать `cart_items`** — получите 422 `This organization does not support catalog. Remove cart_items from request.`

## Как выглядит правильный запрос с корзиной?

```bash
curl -X POST https://api.apipay.kz/api/v1/invoices \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "87001234567",
    "description": "Заказ №123",
    "external_order_id_idempotency": "order-123",
    "cart_items": [
      { "catalog_item_id": 11, "count": 2 },
      { "catalog_item_id": 15, "count": 1, "price": 4900 }
    ],
    "discount_percentage": 10
  }'
```

Что здесь происходит:

- **`catalog_item_id` + `count`** — обязательные поля каждой строки (количество — целое, от 1).
- **`price` (необязательное)** — переопределяет цену каталога для этой строки (0.01–99 999 999.99). Не передали — берётся `selling_price` позиции из каталога. Достаточно одной позиции («Услуга») с заданной ценой — фактическую цену передавайте полем `price` в строке.
- **`discount_percentage`** — скидка 1–99% на весь чек. Считается от **фактической** цены строки: если вы передали `price`, процент берётся от него. Поле `discount` внутри позиции устарело: на него API ответит 422 с текстом «Поле discount устарело. Используйте discount_percentage».
- **Итог корзины на счёте по номеру телефона — только целые тенге.** `discount_percentage` считается построчно, поэтому даже при целых ценах итог бывает дробным (`999 ₸` со скидкой `10 %` дают `899.10`) и счёт отбивается `422 amount_must_be_whole_tenge`. Округлите цены позиций или процент скидки. На QR-счёте (`POST /invoices/qr`) такого ограничения нет.
- **`amount` не передаём**: при корзине сумма счёта считается по позициям (цена × количество − скидка). Если передать и `amount`, и корзину — итог посчитается по корзине.

В вебхуке оплаченного счёта со скидкой придут поля `subtotal`, `discount_sum`, `discount_percentage` — удобно для сверки.

## Разбор 422-ошибок по строкам корзины

| Текст ошибки | Причина | Лечение |
|---|---|---|
| `Catalog item has no price set.` | У позиции каталога пуста `selling_price` | Задать цену позиции в каталоге. Поле `price` здесь не помогает: проверка каталожной цены идёт до него и одинаково на всех путях |
| `Catalog item does not belong to your organization.` | `catalog_item_id` чужой организации (частая причина — id из песочницы в проде) | Взять id из `GET /catalog` того же ключа/организации |
| `Catalog item has been deleted.` | Позиция удалена | Создать заново или взять живую позицию |
| `This organization does not support catalog…` | Каталога у организации нет, а `cart_items` переданы | Убрать `cart_items`, слать `amount` |
| `Поле discount устарело…` | Скидка передана внутри позиции | Использовать `discount_percentage` на весь чек |

Ошибки валидации приходят с индексом строки корзины: `"cart_items.0.catalog_item_id": ["Catalog item has no price set."]` — индекс `0` указывает, какая именно позиция сломана. Текст с добавкой `Pass cart_items[].price explicitly.` приходит только с `POST /invoices` — там ошибку снимает `price` в строке.

## Где взять catalog_item_id?

`catalog_item_id` — это `id` позиции из `GET /catalog` того же ключа. Готовность позиции к продаже читайте по полю `sellable`, а не по `status`: при `sellable: false` счёт с этой позицией не пройдёт. Только что залитая позиция принимается в счёт сразу (`sellable: true`), но пока у неё `in_kaspi_catalog: false`, она уедет разовой продажей и маркировка Нацкаталога в фискальный чек по ней не проводится — для фискального чека дождитесь `in_kaspi_catalog: true`. Как заводить товары (`POST /catalog`), грузить изображения, сканировать штрихкод в Нацкаталоге и синхронизировать каталог — в статье [Каталог, корзина и Нацкаталог](/guides/katalog-korzina-nackatalog).

## Чек-лист исправления 422 за 5 минут

1. `GET /catalog` тем же ключом: есть ли у организации каталог и какие id у позиций.
2. Каталога нет, а вы шлёте `cart_items` → уберите корзину, передавайте `amount`. Каталог есть → соберите счёт из `cart_items[] = {catalog_item_id, count}`.
3. `has no price set` → задайте `selling_price` позиции в каталоге. Нужна другая цена, чем в каталоге, → поле `price` в строке корзины (но сама цена в каталоге всё равно должна быть задана).
4. Скидка → только `discount_percentage` (1–99) на весь чек; `amount` при корзине не передавайте.

## Вопросы и ответы

**Влияет ли корзина на возвраты?**
Да, возвраты можно делать поэлементно (`return_items[]` с `count` или `amount`) — подробнее в «[Возвраты Kaspi через API](/guides/vozvraty-kaspi-cherez-api)». Совет: не забивайте все товары одной позицией «Услуги» — поштучный возврат станет неудобным.

Смотрите также: [Как создать счёт по номеру](/guides/kak-sozdat-schet-kaspi-po-nomeru) · [Вебхуки ApiPay](/guides/nastroyka-webhookov-apipay) · пиллар «[Как принимать оплату Kaspi через API](/kaspi-api)».

---

ApiPay — независимый сервис и не аффилирован с АО «Kaspi Bank». Kaspi и Kaspi Pay — товарные знаки их правообладателя.
База знаний: https://apipay.kz/guides
