Каталог, корзина и Нацкаталог (ntin/gtin): как продавать с позициями?

Обновлено 11 июля 2026 · Справочник · Версия в Markdown
Содержание
  1. Зачем это нужно
  2. Завести каталог
  3. Продавать с корзиной
  4. Синхронизация чтением — GET /catalog
  5. Ошибки позиций
  6. Частые ошибки
  7. Частые вопросы

Зачем это нужно

Каталог нужен там, где в чеке Kaspi покупатель должен видеть позиции (наименование, цена, количество), а не «Оплата 5000 ₸». Так работают магазины и точки с Kaspi ОФД (режим «Касса»): счёт собирается из позиций каталога через cart_items, и Kaspi формирует по ним фискальный чек.

Нацкаталог (ntin/gtin) нужен только для маркированных товаров: эти коды долетают в чек Kaspi и корректно идентифицируют товар в системе маркировки. Немаркированным товарам ntin/gtin не нужны — заводите их обычными позициями.

У организации с каталогом поле cart_items в счёте обязательно — «голую» сумму amount слать нельзя (получите 422). Обратное правило: организация без каталога не может слать cart_items. Разбор всех 422 вокруг корзины — в статье Ошибка 422 cart_items и Счета с корзиной.

Важно: эта статья — про публичный API мерчанта с заголовком X-API-Key. Не путайте его с партнёрским X-Partner-Key (онбординг чужих мерчантов) — это другая поверхность.

Завести каталог

Создание товаров — POST /catalog. Батч 1–100 товаров за запрос. Обязательные поля позиции: name (≤255), selling_price (≥0.01), unit_id (единица измерения — список из GET /catalog/units). Опциональные: image_id (из upload-image), barcode (≤32 — лимит Kaspi, иначе barcode_too_long), ntin/gtin (≤50, из скана Нацкаталога), external_ref (≤191, клиентская ссылка — например код 1С), from_catalog.

curl -X POST https://api.apipay.kz/api/v1/catalog \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "name": "Кофе Латте", "selling_price": 1800, "unit_id": 1, "external_ref": "1c-0001" }
    ]
  }'
  • Песочница → 201: товар сразу active. Рабочий режим → 202: товар в статусе pending, синхронизация в Kaspi отдельным джобом — позже статус станет active или failed. Не используйте catalog_item_id в счёте, пока позиция не active.
  • external_ref — ваша клиентская ссылка (код/GUID номенклатуры 1С, SKU). Задаётся при создании, индексируется; потом по нему читаете точечно (?external_refs[]=). В PATCH не принимается — менять нельзя. UNIQUE в пределах организации и match-and-merge его не перетирает; удалённый товар можно переиздать по тому же external_ref.

Match-and-merge (создание идемпотентно, дублей нет). POST /catalog не плодит «мёртвые» строки: если позиция совпала с уже существующим товаром, возвращается живой id существующего товара с маркером matched_existing: true (а не ошибка и не дубль-призрак). Ярусы матчинга: (1) по external_ref; (2) по barcode/ntin при совпадении имени; (3) по barcode/ntin, но имя другое — тогда в ответе name_differs: true, и имя существующего товара НЕ перезаписывается. Практический итог: повторная заливка того же набора идемпотентна (все позиции matched_existing, ноль новых строк) — синк каталога по расписанию безопасен.

  • 409 catalog_busy в ответе POST /catalog — не удалось взять per-org lock за 5 с (параллельная заливка на ту же организацию). Не ошибка данных — просто повторите запрос.

Маркированные товары — POST /catalog/scan. Резолвит штрихкод в Нацкаталоге Kaspi (синхронно, на сессии кассира):

curl -X POST https://api.apipay.kz/api/v1/catalog/scan \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{ "input": "4607015232646" }'
  • Один штрихкод может дать несколько кандидатов (общий gtin, разные ntin) — какой из них товар, решает мерчант.
  • Пустой data[] и/или scan_result.code != "ok" = товар не из Нацкаталога. Это НЕ ошибка (200) — создавайте товар обычным POST /catalog без ntin/gtin.
  • Дальше берёте ntin/gtin/barcode выбранного кандидата и передаёте в POST /catalog.
  • Лимиты: 30/мин + 2000/сутки на ключ. Ошибки скана: 400 при недействительной сессии — требуется переподключить кассира Kaspi; 429 kaspi_throttled (тело — retry_after_seconds, заголовок Retry-After); 503 kaspi_scan_unavailable (Нацкаталог временно недоступен). При троттлинге около 90 секунд сразу отдаётся 429 без похода в Kaspi.

Изображения — POST /catalog/upload-image. multipart/form-data, поле image (jpg/png/gif/webp, ≤10 МБ), дедупликация по MD5. Ответ — { image_id }; этот image_id передаёте в POST /catalog или в PATCH (для удаления картинки — is_image_deleted: true).

Продавать с корзиной

Счёт с позициями — это cart_items[] в обычном POST /invoices или POST /invoices/qr. Отдельного POST /invoices/with-cart в публичном API нет (это внутренний роут SPA-кабинета, не /api/v1).

curl -X POST https://api.apipay.kz/api/v1/invoices \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "87770000001",
    "description": "Заказ №123",
    "cart_items": [
      { "catalog_item_id": 1, "count": 2, "price": 4500 },
      { "catalog_item_id": 5, "count": 3 }
    ],
    "discount_percentage": 10
  }'
  • catalog_item_id (обязателен) — это поле id товара из GET /catalog; товар должен принадлежать организации, быть не удалён и иметь цену. count (обязателен, ≥1). price (опц.) — кастомная цена за единицу, заменяет каталожную для этой строки.
  • cart_items: 1–100 позиций. Сумму считает сервер по позициям; переданный amount при наличии cart_items игнорируется.
  • Скидка на весь чек — discount_percentage (1–99). Скидку внутри позиции API не принимает.
  • Поля Нацкаталога позиций (barcode/ntin/gtin) подтягиваются автоматически из каталога — их НЕ нужно передавать в cart_items руками. В прочитанном счёте они видны в items[] (nullable).
  • QR-нюанс: у POST /invoices/qr поле description ограничено 100 символами (это наименование позиции в QR-чеке Kaspi), тогда как у счёта по номеру — до 500.

Синхронизация чтением — GET /catalog

Четыре режима чтения (по приоритету параметров). Выбирайте под задачу:

По умолчанию GET /catalog во ВСЕХ 4 режимах отдаёт только active. Прочие статусы (pending/failed/deleting/deleted) — строго по явному опт-ину ?statuses[]=….

Режим Параметры Что отдаёт Когда использовать
targeted ntins[] / barcodes[] / ids[] / external_refs[] (OR; суммарно ≤200 значений и ≤1000 строк) { data: [...] } без пагинации; по умолчанию active, прочее — через statuses[] Подтвердить конкретный батч: «что стало с товарами, которые я только что создал» (по external_ref/ntin)
incremental updated_after=<iso> { data, links, meta }; по умолчанию active — удалённые добери statuses[]=deleted Забрать только изменённое с прошлой синхронизации — дельта в свою систему (зеркалить удаления → добавь statuses[]=active&statuses[]=deleted)
keyset cursor=<из meta.next_cursor> meta-обёртка (курсорная: next_cursor/prev_cursor, без total); по умолчанию active Первичная/полная выгрузка 100k+ без deep-offset
default (offset) page / per_page≤200 (default 50) meta-обёртка, только active; прочие статусы — через statuses[] Обычный постраничный просмотр небольшого каталога
  • Точечная сверка ограничена суммарно. Больше 200 значений по всем наборам (external_refs[]+barcodes[]+ntins[]+ids[]) или больше 1000 строк соответствий → 422 catalog_match_overflow. Тихого усечения limit(200) больше нет — разбивайте сверку на батчи по ~100.
  • Призраки не отдаются никогда. Строки status='deleted' без kaspi_item_id (позиции, отклонённые ещё до отправки в Kaspi) исключаются во всех режимах — даже при явном ?statuses[]=deleted. Настоящие удаления (deleted с непустым kaspi_item_id) через ?statuses[]=deleted видны.
  • Доп. фильтры: statuses[] (active/pending/deleting/deleted/failed), search (по названию), barcode (точный), first_char.
  • Поля товара (CatalogItem): id, kaspi_item_id, name, unit_id, selling_price, image_url, barcode, ntin, gtin, unified_goods_id, external_ref, ntin_missing (barcode есть, НТИН нет → не попадёт в фискальный чек как маркированный), status, error_message, error_code, created_at, synced_at. Даты — UTC +00:00. Ещё два флага — matched_existing и name_differs — приходят только в ответе POST /catalog (в листинге всегда false).
  • Только для организаций с каталогом (иначе 400/404).

Рецепт связки «создал → подтвердил». Создали товары с external_ref (коды 1С) → в рабочем режиме получили 202 (pending) → через время читаете GET /catalog?external_refs[]=1c-0001&external_refs[]=1c-0002 → смотрите status: active — синхронизирован, failed — см. error_code.

Подтверждение вебхуком — catalog.item_processed. Кроме поллинга, финальный статус можно получить push-вебхуком catalog.item_processed — он включён по умолчанию (kill-switch на стороне сервера — env KASPI_CATALOG_WEBHOOKS_ENABLED). Payload несёт external_ref как ключ сверки (плюс id, kaspi_item_id, status, error_code, ntin_missing), так что SaaS с публичным URL не обязан поллить. Аудит доставок — новый read-only эндпоинт GET /catalog/webhook-logs (лог ротируется 3 дня). Поллинг по external_refs[] и вебхук — равноправные пути; поллинг проще для 1С/on-prem, вебхук дешевле для SaaS.

Обновление и удаление:

  • PATCH /catalog/{id} — все поля опциональны; песочница → 200, рабочий режим → 202. Важно: ntin/gtin затирают идентичность Нацкаталога безвозвратно — передавайте их только при реальном изменении маркировки, иначе null сотрёт коды. external_ref в PATCH не принимается.
  • DELETE /catalog/{id} — песочница → 200 (hard delete), рабочий режим → 202 (статус → deleting, удаление в Kaspi джобом).

Ошибки позиций

  • У товара в каталоге при провале синхронизации статус failed + поле error_codeerror_message) — читаются из GET /catalog. Определяйте причину по error_code, а не по тексту.
  • В корзине счёта: если catalog_item_id невалиден (не принадлежит организации, удалён, без цены) — счёт не пройдёт валидацию (422, с индексом строки cart_items.N). Проверяйте товары до продажи.
  • Slug'и catalog_item_not_found и image_upload_failed в каталоге зарезервированы: на практике провал загрузки картинки не фатален (товар создаётся без изображения), а отказы удаления/несопоставления уходят в kaspi_error/unknown_error. Полный каталог error-кодов — на странице /errors и в публичной документации.

Частые ошибки

  • Искать POST /invoices/with-cart. Его нет в публичном API — счёт с позициями создаётся полем cart_items в POST /invoices / POST /invoices/qr.
  • Слать amount вместе с корзиной и ждать, что итог возьмётся из него. При cart_items сумму считает сервер; управляйте ценой через price в строке.
  • Использовать catalog_item_id из песочницы в рабочем режиме. Тестовые организации и их каталоги удаляются при переходе в прод — id там другие.
  • Передавать ntin/gtin в обычном PATCH. Это безвозвратно затрёт идентичность Нацкаталога. Трогайте их только осознанно.
  • Слать счёт сразу после создания позиции в рабочем режиме. Позиция создаётся асинхронно (202, pending) — дождитесь active.

Частые вопросы

Как выставить счёт с позициями?

Через cart_items[] в POST /invoices или POST /invoices/qr. Каждая позиция — catalog_item_id (поле id из GET /catalog) + count. Отдельного with-cart в публичном API нет.

Обязательно ли слать корзину?

Для организаций с каталогом — да; amount при наличии cart_items игнорируется. Без каталога наоборот — cart_items слать нельзя.

Штрихкод не нашёлся в скане — это ошибка?

Нет. Пустой data[] (или scan_result.code != "ok") означает, что товара нет в Нацкаталоге. Ответ 200, не ошибка — создавайте товар обычной позицией без ntin/gtin.

Как выгрузить весь каталог на 100k+ позиций?

Keyset-режим GET /catalog?cursor= (без deep-offset), листая по meta.next_cursor. Только изменённое с прошлого раза — ?updated_after=<iso> (включая удалённые).

Можно ли поменять ntin/gtin через PATCH?

Технически да, но при обычном обновлении это затрёт идентичность Нацкаталога безвозвратно. Правьте их только при реальном изменении маркировки.

Где взять catalog_item_id для корзины?

Это поле id из GET /catalog. Оно же используется в return_items[] при поэлементных возвратах — см. Возвраты Kaspi через API.

Интеграция с ИИ

Не настраивайте вручную — поручите ИИ

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он подключит приём платежей примерно за 15 минут. Настраивает ИИ, а не вы.

Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/katalog-korzina-nackatalog.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.