Массовая заливка каталога 1С в ApiPay: очередь, ETA, ошибки

Обновлено 26 августа 2026 · Справочник · Версия в Markdown
Содержание
  1. Шаг 1. Заливка пачками по 100 с `Idempotency-Key`
  2. Шаг 2. Следим за остатком очереди
  3. Шаг 3. Сверяем конкретные позиции
  4. Шаг 4. Вебхук по каждой позиции
  5. Шаг 5. Разбор отказов
  6. Шаг 6. Повторная заливка идемпотентна
  7. Если ваш код писался под «партии»
  8. Частые вопросы
  9. Что дальше

Шаг 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.

Что дальше

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

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

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он настроит интеграцию за вас: получит доступы, создаст тестовый счёт в песочнице и подключит вебхуки. Техническую часть делает ИИ, а не вы.

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