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

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

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

## Коротко

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

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

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

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

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

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

```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-счёта своё правило скидки — следующий раздел.
- **`amount` не передаём**: при корзине сумма счёта считается по позициям (цена × количество − скидка). Если передать и `amount`, и корзину — итог посчитается по корзине.

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

## Как считается скидка на QR-счёте?

На QR-счёте (`POST /invoices/qr`, в том числе со `static: true`) и статическом QR (`POST /static-qr`) скидку считают по правилу Kaspi, а не так, как на счёте по номеру телефона:

- **Процент — только целый**, от 1 до 99. Дробный (`7.5`) отбивается `422` с ключом `discount_percentage` в `errors{}`. Счёт по номеру дробный процент принимает.
- **Скидка строки** = цена × количество × процент / 100, округлённая **вверх до целого тенге**.
- **К оплате** = сумма строк − сумма скидок строк.

Поэтому на дешёвой позиции фактическая скидка больше заявленной: 7 % от 5 ₸ — это скидка 1 ₸. Так же считает приложение Kaspi.

| Корзина | Процент | Скидки строк | К оплате |
|---|---|---|---|
| 5 ₸ × 1 | 10 % | 1 ₸ | 4 ₸ |
| 5 ₸ + 10 ₸ | 20 % | 1 ₸ + 2 ₸ | 12 ₸ |
| 5 ₸ + 10 ₸ | 7 % | 1 ₸ + 1 ₸ | 13 ₸ |
| 5 ₸ × 3 | 7 % | 2 ₸ | 13 ₸ |

Сумму к оплате и скидку берите из ответа создания — поля `amount` и `discount_sum` (у статического QR — `amount`), а не из своего расчёта.

Скидку на QR не примут в двух случаях. Ответ — `422 qr_discount_unsupported`, причина текстом в `errors.discount_percentage`, счёт не создаётся (в песочнице так же):

- цена позиции задана в тиынах — в `cart_items[].price` или в каталоге;
- скидка обнуляет позицию: у позиции за 1 ₸ скидка 10 % — это скидка 1 ₸.

Что делать: задайте цены в целых тенге и целый процент, а позицию, которую скидка обнуляет, сделайте дороже или уберите скидку. Счёт по номеру телефона (`POST /invoices`) подойдёт, только если итог после скидки выходит в целых тенге — иначе `422 amount_must_be_whole_tenge`. Количество в строке корзины QR-счёта — не больше 100 000. Скидка на статическом QR — в статье «[Статический QR для оплаты по счёту или сделке](/guides/pechatnyy-qr-dlya-oplaty-po-sdelke)».

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

| Текст ошибки | Причина | Лечение |
|---|---|---|
| `Catalog item has no price set.` | У позиции каталога пуста `selling_price` | Задать цену позиции в каталоге — это снимает ошибку на любом пути. Только на `POST /invoices` вместо этого можно передать `price` в строке; на `POST /invoices/qr`, `POST /static-qr`, `POST /invoices/bulk` и в подписках `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` в строке корзины (везде, кроме `POST /invoices`, сама цена в каталоге всё равно должна быть задана).
4. Скидка → только `discount_percentage` (1–99) на весь чек; `amount` при корзине не передавайте. На QR-счёте и статическом QR процент целый, а цены позиций при скидке — в целых тенге.

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

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

**Почему на QR-счёте скидка больше заявленной?**
На QR скидка каждой строки округляется вверх до целого тенге — так считает Kaspi. На дешёвых позициях разница заметна: 7 % от 5 ₸ дают скидку 1 ₸. Итог берите из ответа создания счёта (`amount`, `discount_sum`).

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

---

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