Шаг 1. Заливка батчами по 100 с `Idempotency-Key`
Разбейте каталог на батчи до 100 позиций и отправляйте их по очереди. На каждый батч
ставьте свой Idempotency-Key — стабильную строку (например upload-2026-07-11-part-042).
Если сеть оборвалась и вы не знаете, дошёл ли запрос, повторите его с тем же ключом —
ApiPay вернёт уже принятый батч, а не создаст позиции второй раз.
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-07-11-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 (принято в обработку), в нём приходит блок batch с batch_id:
{
"data": [
{ "id": 5001, "external_ref": "1c-000123", "status": "pending",
"matched_existing": false, "name_differs": false }
],
"rejected": [],
"batch": {
"batch_id": "8f14e45f-ceea-467e-9a2c-0000000000aa",
"status": "accepted",
"totals": { "total": 2, "created": 0, "updated": 0, "skipped": 0, "failed": 0 },
"pending_remaining": 2,
"poll_url": "https://api.apipay.kz/api/v1/catalog/batches/8f14e45f-ceea-467e-9a2c-0000000000aa",
"created_at": "2026-07-11T10:00:00+00:00",
"completed_at": null
}
}
Сохраните batch_id — он ключ к прогрессу и к разбору ошибок этого залива. data содержит
принятые позиции (новые — со status: pending), rejected — позиции, не прошедшие
структурную валидацию (например без name), их можно исправить и переотправить.
Ставьте
external_ref(вашу ссылку 1С) у каждой позиции — это ключ маппинга и якорь идемпотентности. Почему не штрихкод — см. Каталог для 1С.
Шаг 2. Следим за очередью и ETA
Большой каталог обрабатывается ровным потоком — примерно 200 позиций в минуту. Чтобы
показать клиенту прогресс, читайте 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-07-11T10:00:00+05:00" }
],
"total": 1234,
"queue": {
"state": "draining",
"ahead_in_cashier_queue": 2200,
"eta_minutes": 11,
"throttle_retry_in_seconds": null
}
}
total— сколько ваших позиций ещё ждёт обработки.queue.eta_minutes— оценка времени до конца очереди в минутах. Приходит целым числом только когда очередь реально двигается (state: draining); в остальных состояниях —null(не показывайте «0 минут»).queue.ahead_in_cashier_queue— сколько позиций стоит впереди с учётом всех организаций одного кассира. Если у кассира несколько организаций, они делят одну очередь — ETA это уже учитывает.queue.state— почему очередь в текущем состоянии:
state |
Что значит | Что делать |
|---|---|---|
draining |
очередь двигается | показывать eta_minutes |
paused_throttle |
пауза из-за ограничения частоты обработки | подождать throttle_retry_in_seconds секунд, обработка возобновится сама |
paused_hold |
накопление приостановлено владельцем (hold-режим) | снять hold в кабинете, чтобы очередь пошла |
direct |
приём в очередь выключен — позиции обрабатываются напрямую | это не ошибка; ETA неприменим |
not_connected |
у организации не подключён кассир Kaspi | подключить/переавторизовать кассира |
sandbox |
режим песочницы | позиции активируются мгновенно, без очереди |
Поллите очередь раз в 5–15 секунд — лимит 600 запросов/мин на ключ это позволяет.
Останавливайте поллинг, когда total дошёл до нуля.
Шаг 3. Итог конкретного залива
Прогресс именно вашего батча — GET /catalog/batches/{id} (используйте poll_url из
ответа POST):
curl https://api.apipay.kz/api/v1/catalog/batches/8f14e45f-ceea-467e-9a2c-0000000000aa \
-H "X-API-Key: YOUR_API_KEY"
{
"batch_id": "8f14e45f-ceea-467e-9a2c-0000000000aa",
"status": "partially_failed",
"totals": { "total": 6870, "created": 5868, "updated": 3, "skipped": 996, "failed": 3 },
"pending_remaining": 0,
"poll_url": "https://api.apipay.kz/api/v1/catalog/batches/8f14e45f-ceea-467e-9a2c-0000000000aa",
"created_at": "2026-07-11T10:00:00+00:00",
"completed_at": "2026-07-11T10:12:00+00:00"
}
status:accepted→processing→completed(всё ОК) либоpartially_failed(есть упавшие).pending_remaining— сколько позиций залива ещё не финализировано (0= готово).totalsпо завершении:total = created + updated + skipped + failed.skipped— это позиции, которые уже были в каталоге без изменений (идемпотентный пропуск) или совпали внутри батча; в Kaspi по ним обращений нет.
Шаг 4. Один вебхук на весь залив
Если у вас есть публичный URL для вебхуков, не ждите тысячи per-item уведомлений: когда
все позиции залива финализированы, ApiPay шлёт один вебхук catalog.batch_processed
(HMAC-подпись, как у остальных событий):
{
"event": "catalog.batch_processed",
"batch_id": "8f14e45f-ceea-467e-9a2c-0000000000aa",
"status": "partially_failed",
"totals": { "total": 6870, "created": 5868, "updated": 3, "skipped": 996, "failed": 3 },
"sample_failed": [ { "external_ref": "1C-000456", "error_code": "catalog_item_duplicate" } ],
"poll_url": "https://api.apipay.kz/api/v1/catalog/batches/8f14e45f-ceea-467e-9a2c-0000000000aa",
"timestamp": "2026-07-11T10:12:00+00:00"
}
sample_failed— до 50 упавших позиций (external_ref+error_code), для быстрой реакции. Полный список ошибок — черезGET /catalog/errors?batch_id=....- Вебхук может прийти повторно (доставка «хотя бы один раз») — дедуплицируйте по паре
(batch_id, status). - Пока идёт батч-заливка, отдельные
catalog.item_processedпо её позициям не приходят — их заменяет этот агрегат. Финал по каждой позиции берите изGET /catalog/batches/{id}или из журнала ошибок.
Шаг 5. Разбор ошибок
Упавшие позиции залива читайте через GET /catalog/errors, фильтруя по batch_id:
curl -G https://api.apipay.kz/api/v1/catalog/errors \
-H "X-API-Key: YOUR_API_KEY" \
--data-urlencode "batch_id=8f14e45f-ceea-467e-9a2c-0000000000aa"
{
"current_page": 1,
"data": [
{ "id": 456, "external_ref": "1C-000456", "name": "Фильтр воздушный",
"barcode": "4600000000001", "ntin": "00000000000001",
"error_code": "catalog_item_duplicate",
"error_message": "Такая позиция уже есть в каталоге.",
"queued_at": "2026-07-11T10:00:00+05:00",
"failed_at": "2026-07-11T10:03:00+05:00",
"batch_id": "8f14e45f-ceea-467e-9a2c-0000000000aa" }
],
"total": 3
}
Без batch_id эндпоинт отдаёт ошибки за последние 7 дней; явные from/to (по
времени постановки в очередь) снимают это ограничение. Причину всегда определяйте по
error_code, а не по тексту error_message (текст обезличен). Что делать с типовыми
кодами:
error_code |
Что значит | Что делать |
|---|---|---|
catalog_item_duplicate |
Kaspi считает позицию дублем уже заведённого товара (похожее наименование) | Найдите товар через GET /catalog и правьте существующий PATCH /catalog/{id}; проверьте, нет ли в вашем каталоге двух позиций с одинаковым именем |
barcode_too_long |
штрихкод длиннее 32 символов (лимит Kaspi) | Обрежьте/исправьте штрихкод и переотправьте позицию |
catalog_item_invalid |
Kaspi отклонил данные позиции (наименование/цена/поля) | Проверьте наименование, цену и штрихкод; исправьте и переотправьте |
Исправив вход, переотправьте только упавшие позиции — по их external_ref.
Шаг 6. Повторная заливка идемпотентна
Гоняйте синк по расписанию без страха дублей:
- Повтор запроса с тем же
Idempotency-Keyвозвращает уже принятый батч — позиции не создаются заново. - Повтор каталога: неизменённые позиции ApiPay пропускает по внутреннему фингерпринту
(они попадают в
skipped), обращений в Kaspi по ним нет. Изменённые — обновляются, новые — создаются. - Ключ сопоставления —
external_ref. Пока он стабилен, повторная заливка находит те же товары и не плодит «мёртвых» строк.
Так заливка 25 000 позиций в первый раз занимает ~2 часа, а последующие синки проходят почти мгновенно — работает только дельта.
Частые ошибки
- Батч больше 100 позиций.
POST /catalogпринимает до 100 items за запрос. Бейте каталог на батчи по 100 (или меньше) и лейте их подряд. - Заливка в много потоков ради скорости. Частые параллельные запросы на один кассир не ускоряют, а вызывают паузы обработки (
state: paused_throttle). Лейте батчами по 100 последовательно — ровный поток надёжнее. - Маппинг по штрихкоду вместо
external_ref. По одному штрихкоду у мерчанта бывает несколько SKU — сверка по штрихкоду путает id. Ключ — толькоexternal_ref. - Ожидание
activeв ответеPOST.202— это «принято», а не «готово». Финал берите изGET /catalog/batches/{id}, вебхука или поллинга. - Показ «0 минут» при
eta_minutes: null.null— легитимно (очередь не в состоянииdraining). Показывайте прочерк, а не ноль. - Отсутствие дедупа батч-вебхука.
catalog.batch_processedдоставляется «хотя бы один раз» — дедуплицируйте по(batch_id, status).
Частые вопросы
Какого размера делать батчи?
До 100 позиций за один POST /catalog. Больше — запрос отклонится валидацией. Лейте батчи подряд, ровным потоком.
Зачем нужен Idempotency-Key, если каталог и так идемпотентен?
Match-and-merge защищает от дублей на уровне товаров, а Idempotency-Key — от повторной обработки того же запроса при обрыве сети: вы можете безопасно повторить POST, не зная, дошёл ли он.
Сколько времени займёт заливка 25 000 позиций?
Около 2 часов при темпе ≈200 позиций/мин на кассира. Точную оценку остатка показывает eta_minutes в GET /catalog/queue.
Очередь показывает state: direct — это ошибка?
Нет. Это значит, что приём в очередь для организации сейчас работает в прямом режиме: позиции обрабатываются, просто без ETA. Дождитесь финала через GET /catalog/batches/{id}.
Как узнать, какие позиции упали при большой заливке?
GET /catalog/errors?batch_id=<batch_id> вернёт упавшие позиции этого залива с error_code. Чините вход по коду и переотправляйте только упавшие.
Придёт ли per-item вебхук catalog.item_processed при батч-заливке?
Нет — на время заливки его заменяет один агрегат catalog.batch_processed. Одиночные позиции (не в батче) шлют per-item вебхук как раньше.
Что дальше
- Логика без дублей и
external_ref: Каталог для 1С: синхронизация без дублей. - Базовые поля позиции, корзина и Нацкаталог: Каталог, корзина и Нацкаталог.