Шаг 1. Заливка пачками по 100 с `Idempotency-Key`
Разбейте каталог на пачки до 100 позиций и отправляйте их по очереди, ровным потоком, без распараллеливания на один кассир.
На каждую пачку ставьте свой Idempotency-Key — стабильную строку, например
upload-2026-08-26-part-042.
Если сеть оборвалась и вы не знаете, дошёл ли запрос, повторите его с тем же ключом: позиции не создадутся второй раз.
curl -X POST https://api.apipay.kz/api/v1/catalog \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: upload-2026-08-26-part-042" \
-d '{
"items": [
{ "name": "Ручка гелевая синяя 0.5", "selling_price": 350, "unit_id": 1,
"barcode": "4870000000001", "external_ref": "1c-000123" },
{ "name": "Тетрадь 48 листов клетка", "selling_price": 420, "unit_id": 1,
"barcode": "4870000000002", "external_ref": "1c-000124" }
]
}'
Ответ — 202 («принято в обработку»), построчный:
{
"data": [
{ "id": 5001, "external_ref": "1c-000123", "status": "pending",
"operation": "create", "sellable": true, "in_kaspi_catalog": false,
"matched_existing": false, "name_differs": false }
],
"rejected": []
}
data— принятые позиции. Новые приходят соstatus: pendingиoperation: create: запрос принят, но в каталоге Kaspi позиции ещё нет.rejected— позиции, не прошедшие проверку (например безname). Ключ есть в ответе всегда; исправьте эти позиции и отправьте их отдельно.
Общего идентификатора пачки в ответе нет. Один запрос — это просто один запрос, а не
объект, за которым можно следить. Поэтому список отправленных external_ref сохраняйте у
себя: именно по нему вы потом сверяете результат.
Ставьте
external_ref(вашу ссылку 1С) у каждой позиции — это ключ маппинга и якорь идемпотентности: Каталог для 1С.
Что вернёт точный повтор
Повтор того же тела с тем же Idempotency-Key отвечает 200 с признаком
idempotent_replay: true — запрос не выполняется заново.
Строк позиций в таком ответе нет: перечитайте их запросом
GET /catalog?external_refs[]=….
Тот же ключ с другим телом (или на массовом удалении) — это 409 idempotency_key_conflict. Пространство ключей у приёма и у массового удаления общее,
поэтому берите новый ключ, а не подгоняйте старый.
Шаг 2. Следим за остатком очереди
Чтобы показать клиенту прогресс, читайте GET /catalog/queue:
curl -G https://api.apipay.kz/api/v1/catalog/queue \
-H "X-API-Key: YOUR_API_KEY"
{
"current_page": 1,
"data": [
{ "id": 5001, "external_ref": "1c-000123", "name": "Ручка гелевая синяя 0.5",
"queued_at": "2026-08-26T10:00:00+05:00" }
],
"total": 1234,
"updating": 0,
"deleting": 0,
"queue": {
"state": "draining",
"ahead_in_cashier_queue": 2200,
"eta_minutes": 11,
"throttle_retry_in_seconds": null
}
}
Три счётчика — три разных вида работы:
| Поле | Что считает |
|---|---|
total |
позиции, которые ещё заводятся в каталог |
updating |
позиции с непринятой ещё правкой |
deleting |
позиции, ожидающие снятия с продажи |
data и total описывают только заведение. Пока идёт массовое удаление, total может
быть нулём, а работа — продолжаться: смотрите на все три числа сразу.
Блок queue объясняет, почему очередь в текущем состоянии:
state |
Что значит | Что делать |
|---|---|---|
draining |
очередь двигается | показывать eta_minutes |
paused_throttle |
пауза из-за ограничения частоты обработки | подождать throttle_retry_in_seconds секунд, обработка возобновится сама |
paused_hold |
накопление приостановлено владельцем (hold-режим) | снять hold в кабинете, чтобы очередь пошла |
not_connected |
у организации не подключён кассир Kaspi | подключить или переподключить кассира |
sandbox |
режим песочницы | позиции активируются сразу, очереди нет |
queue.eta_minutes— оценка времени до конца очереди в минутах. Целое число приходит только приstate: draining; иначеnull— показывайте прочерк, а не «0 минут».queue.ahead_in_cashier_queue— сколько позиций стоит до конца вашей очереди включительно, считая позиции других организаций этого же кассира. ETA это уже учитывает.- Набор значений
stateоткрытый — неизвестное состояние обрабатывайте общей веткой, а не падением.
Опрашивайте очередь раз в 5–15 секунд: лимит 600 запросов/мин на ключ это позволяет, и общий бюджет ключа он не расходует.
Шаг 3. Сверяем конкретные позиции
Итог по вашему списку читается точечно — по тем самым external_ref, которые вы отправили:
curl -G https://api.apipay.kz/api/v1/catalog \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "external_refs[]=1c-000123" \
--data-urlencode "external_refs[]=1c-000124" \
--data-urlencode "statuses[]=active" \
--data-urlencode "statuses[]=pending" \
--data-urlencode "statuses[]=failed"
{
"data": [
{ "id": 5001, "external_ref": "1c-000123", "status": "active",
"operation": null, "sellable": true, "in_kaspi_catalog": true,
"kaspi_item_id": "90210", "ntin_missing": true },
{ "id": 4980, "external_ref": "1c-000124", "status": "pending",
"operation": "create", "sellable": true, "in_kaspi_catalog": false,
"kaspi_item_id": null, "ntin_missing": true }
]
}
- За один запрос можно спросить до 200 значений; больше —
422 catalog_match_overflow. Разбивайте сверку по 100. - Без параметра
statuses[]отдаются только активные позиции. Чтобы увидеть, что какая-то позиция ещё в работе или упала, статусы нужно перечислить явно. sellableотвечает на практический вопрос «можно ли уже ставить позицию в счёт». У только что залитой позиции онtrueуже приstatus: pending.falseбывает у снятой позиции, у позиции с открытым намерением удаления и у переиздания ранее снятой строки.in_kaspi_catalog— другой вопрос: уедет ли позиция каталожным товаром с маркировкой Нацкаталога. Уpendingонfalse, и до подтверждения счёт с этой позицией пройдёт, но маркировки в фискальном чеке не будет.
⛔ Фильтр batch_id у GET /catalog и GET /catalog/errors больше не существует и
отклоняется — 422 catalog_batch_filter_removed. Отбивается даже пустой ?batch_id=,
поэтому уберите параметр из запроса целиком, а не оставляйте его без значения.
Шаг 4. Вебхук по каждой позиции
Если у вас есть публичный URL для вебхуков, итог по каждой позиции приходит событием
catalog.item_processed — с той же подписью, что у остальных событий.
{
"event": "catalog.item_processed",
"catalog_item": {
"id": 5001,
"external_ref": "1c-000123",
"kaspi_item_id": "90210",
"name": "Ручка гелевая синяя 0.5",
"status": "active",
"operation": null,
"sellable": true,
"in_kaspi_catalog": true,
"ntin_missing": true,
"error_code": null,
"failed_at": null
},
"timestamp": "2026-08-26T10:12:00+00:00"
}
⚠️ Событие приходит по каждой позиции. Заливка на 50 позиций даёт до 50 доставок, а не одну — приёмник должен это выдержать. Планируйте нагрузку заранее.
Сверяйтесь по external_ref — это ваш ключ 1С. Поле status в событии — тот же словарь,
что в GET /catalog, и значения там всегда совпадают.
«Заливка завершена» — это ваш собственный вывод. Одного события «всё готово» нет:
отмечайте у себя приходящие external_ref и считайте залив законченным, когда закрыт весь
ваш список.
Доставка может повториться, поэтому обработка одной и той же позиции должна быть у вас безопасной при повторе.
Проверить, ушли ли доставки и с каким ответом, можно read-only логом
GET /catalog/webhook-logs (хранится 3 дня).
Шаг 5. Разбор отказов
Позиции, по которым работа отказала, читайте через GET /catalog/errors:
curl -G https://api.apipay.kz/api/v1/catalog/errors \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "from=2026-08-26 09:00" \
--data-urlencode "to=2026-08-26 18:00"
{
"current_page": 1,
"data": [
{ "id": 456, "external_ref": "1C-000456", "name": "Фильтр воздушный",
"barcode": "4600000000001", "ntin": "00000000000001",
"operation": "create",
"error_code": "catalog_item_duplicate",
"error_message": "Такая позиция уже есть в каталоге.",
"queued_at": "2026-08-26T10:00:00+05:00",
"failed_at": "2026-08-26T10:03:00+05:00" }
],
"total": 3
}
Окно from/to считается по моменту отказа — по failed_at. Без from отдаются
последние 7 дней; за более старым отказом передавайте from явно.
Дата без времени и время без смещения трактуются по Алматы (+05:00). Голая дата в to
включает весь день целиком.
Свои позиции в ответе отбирайте по собственному списку external_ref — ответ содержит
отказы всей организации за окно.
operation показывает, что именно не получилось: create — позиция не завелась,
update — не доехала правка, delete — не прошло снятие.
Причину всегда определяйте по error_code, а не по тексту error_message (текст обезличен):
error_code |
Что значит | Что делать |
|---|---|---|
catalog_item_duplicate |
Kaspi считает позицию дублем уже заведённого товара (похожее наименование) | Найдите товар через GET /catalog и правьте существующий через PATCH /catalog/{id}; проверьте, нет ли в вашем каталоге двух позиций с одинаковым именем |
barcode_too_long |
штрихкод длиннее 32 символов (лимит Kaspi) | Исправьте штрихкод и отправьте позицию заново |
catalog_item_invalid |
Kaspi отклонил данные позиции (наименование, цена, поля) | Проверьте наименование, цену и штрихкод; исправьте и отправьте заново |
catalog_multi_tradepoint |
у организации несколько торговых точек, и позицию не к чему отнести однозначно | Сами данные позиции здесь ни при чём — напишите в поддержку: 77003076512 |
Исправив вход, отправьте заново только упавшие позиции — по их external_ref.
Шаг 6. Повторная заливка идемпотентна
Гоняйте синхронизацию по расписанию без страха дублей:
- Повтор запроса с тем же
Idempotency-Keyи тем же телом ничего не выполняет заново. - Повтор каталога: позиция, совпавшая с уже заведённым товаром, возвращается его живым
idс маркеромmatched_existing: true— новая строка не создаётся. Изменённые поля обновляются, новые позиции создаются.
Механику совпадений разбирает Каталог для 1С.
Если ваш код писался под «партии»
Раньше POST /catalog возвращал блок batch с номером партии, и по нему можно было
следить за заливом. Этой оси больше нет.
| Что было | Что теперь |
|---|---|
batch.batch_id и poll_url в ответе |
ответа с партией нет — держите список external_ref у себя |
GET /catalog/batches/{id} |
ручки нет (404) |
?batch_id= у GET /catalog и GET /catalog/errors |
параметр отклоняется: 422 catalog_batch_filter_removed |
Один вебхук catalog.batch_processed на весь залив |
catalog.item_processed по каждой позиции |
⚠️ Если вы жили только на вебхуке catalog.batch_processed — обратите внимание
отдельно: это событие не отправляется никогда. Адрес и подпись прежние, просто тишина;
по ошибке об этом не узнать. Подпишитесь на catalog.item_processed.
Частые вопросы
Зачем нужен Idempotency-Key, если каталог и так идемпотентен?
Совпадение позиций защищает от дублей на уровне товаров, а Idempotency-Key — от повторной обработки того же запроса при обрыве сети: вы можете безопасно повторить POST, не зная, дошёл ли он.
Придёт ли вебхук catalog.item_processed при массовой заливке?
Да, по каждой позиции. Заливка на 50 позиций даёт до 50 доставок — рассчитывайте приёмник на это. Отдельного события «залив завершён» нет.
Как узнать, что заливка закончилась?
Своим списком: отмечайте у себя закрытые external_ref и дополнительно смотрите на total, updating и deleting в GET /catalog/queue.
Что дальше
- Логика без дублей и
external_ref: Каталог для 1С: синхронизация без дублей. - Базовые поля позиции, корзина и Нацкаталог: Каталог, корзина и Нацкаталог.