Каталог для 1С в ApiPay: синхронизация без дублей

Обновлено 11 июля 2026 · Справочник · Версия в Markdown
Содержание
  1. Правило №1: маппинг по `external_ref`, не по штрихкоду
  2. Match-and-merge: как ведёт себя `POST /catalog`
  3. Подтверждение заливки: вебхук ИЛИ поллинг
  4. Статусная модель и `default = active`
  5. Лимиты, троттлы и таймауты
  6. Ошибки и что делать
  7. Обезличенный кейс: несколько SKU под одним штрихкодом
  8. Частые вопросы
  9. Что дальше

Правило №1: маппинг по `external_ref`, не по штрихкоду

Проставляйте у каждого товара external_ref — вашу ссылку в 1С (код номенклатуры, GUID, артикул, ≤191 символа). Это единственный надёжный якорь:

  • external_ref UNIQUE в пределах организации — один товар ApiPay на одну вашу ссылку.
  • Задаётся при создании; в PATCH /catalog/{id} не принимается (менять якорь нельзя).
  • Сверять по нему точечно: GET /catalog?external_refs[]=1c-000123.

Почему не штрихкод: Kaspi разрешает один товар на штрихкод/НТИН, а в 1С под одним штрихкодом могут висеть несколько SKU (разные фасовки). При сверке по штрихкоду вы не знаете, какой из id ваш. external_ref снимает неоднозначность полностью.

Match-and-merge: как ведёт себя `POST /catalog`

При создании ApiPay синхронно сопоставляет каждую позицию батча с существующими активными товарами организации. Ярусы (по приоритету):

Ярус Совпадение Что происходит
1 external_ref совпал Полный update: имя + цена, дозаполнение пустых ntin/gtin/barcode. Вернётся живой id
2 штрихкод или НТИН совпал и имя совпало Update цены + дозаполнение пустых полей (в т.ч. НТИН — лечит «штрихкод есть, НТИН пуст»)
3 штрихкод или НТИН совпал, имя другое Матч без перезаписи имени/цены, маркер name_differs: true. Правьте имя явным PATCH /catalog/{id}
  • При матче ответ несёт matched_existing: true и живой id существующего товара — новая строка не создаётся, id стабилен.
  • external_ref существующего товара никогда не перетирается.
  • Повторная заливка того же каталога идемпотентна: все позиции вернутся matched_existing: true, ноль новых строк, обращений в Kaspi по неизменным полям нет.
  • Дозаполненные/изменённые поля (имя, цена, НТИН/GTIN/штрихкод) уезжают в Kaspi асинхронно.

Переиздание удалённого товара: если external_ref указывает на ранее удалённый (deleted) товар — та же строка (тот же id) автоматически возвращается в pending и заново отправляется в Kaspi. Если external_ref попал в failed-товар — вернётся его строка со status: failed без авто-повтора: исправьте данные и переотправьте через PATCH /catalog/{id}.

Пример: заливка батча

curl -X POST https://api.apipay.kz/api/v1/catalog \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -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 (новые позиции — pending, сматченные — с их текущим статусом):

{
  "data": [
    { "id": 5001, "external_ref": "1c-000123", "name": "Ручка гелевая синяя 0.5",
      "barcode": "4870000000001", "ntin": null, "status": "pending",
      "matched_existing": false, "name_differs": false, "ntin_missing": true },
    { "id": 4980, "external_ref": "1c-000124", "name": "Тетрадь 48 листов клетка",
      "barcode": "4870000000002", "ntin": null, "status": "active",
      "matched_existing": true, "name_differs": false, "ntin_missing": true }
  ]
}

Здесь первая позиция создана заново (pending), вторая сматчена с существующим товаром (matched_existing: true, живой id 4980). ntin_missing: true — у обеих есть штрихкод, но пустой НТИН (в чек как маркировка не уйдёт).

Подтверждение заливки: вебхук ИЛИ поллинг

Ответ 202 означает «принято в обработку», а не «готово». Финальный статус (active/failed) приходит асинхронно. Два равноправных способа его узнать:

Для крупных заливок этих двух путей мало — нужен прогресс по всему заливу. Тогда отправляйте позиции с заголовком Idempotency-Key, берите batch_id из ответа и следите за очередью: GET /catalog/queue показывает остаток и ETA в минутах, GET /catalog/batches/{id} — итог залива (created/updated/skipped/failed), а один вебхук catalog.batch_processed заменяет лавину per-item уведомлений. Разбор упавших позиций — GET /catalog/errors?batch_id=.... Подробный плейбук — Массовая заливка каталога 1С.

Путь A — вебхук catalog.item_processed (SaaS с публичным URL)

На каждый обработанный товар ApiPay шлёт вебхук (HMAC-подпись, как у остальных событий):

{
  "event": "catalog.item_processed",
  "catalog_item": {
    "id": 5001,
    "external_ref": "1c-000123",
    "kaspi_item_id": "90210",
    "name": "Ручка гелевая синяя 0.5",
    "barcode": "4870000000001",
    "ntin": null,
    "gtin": null,
    "status": "active",
    "error_code": null,
    "error_message": null,
    "ntin_missing": true
  },
  "timestamp": "2026-07-09T10:00:00Z"
}

Сверяйте по external_ref — это ваш ключ 1С. status: active — товар в Kaspi; status: failed — смотрите error_code/error_message.

Проверить, что вебхуки реально ушли (и с каким кодом ответа), можно read-only логом доставок: GET /catalog/webhook-logs (фильтры status=success|failed, catalog_item_id, created_after; плоская пагинация {current_page, data, total}). Это отдельный лог именно каталожного события — не путать с GET /webhook-logs (доставки по счетам). Логи каталожных вебхуков хранятся 3 дня — забирайте оперативно, для длительного аудита складывайте у себя.

Путь B — поллинг targeted-GET (1С/on-prem, дёшево)

Если публичного URL для вебхука нет — просто перечитайте залитые позиции точечно по external_ref:

curl -G https://api.apipay.kz/api/v1/catalog \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  --data-urlencode "external_refs[]=1c-000123" \
  --data-urlencode "external_refs[]=1c-000124"
{
  "data": [
    { "id": 5001, "external_ref": "1c-000123", "status": "active",
      "kaspi_item_id": "90210", "ntin_missing": true },
    { "id": 4980, "external_ref": "1c-000124", "status": "active",
      "kaspi_item_id": "90188", "ntin_missing": true }
  ]
}

Для 1С поллинг дешевле вебхука (не нужно открывать сервис наружу). Оба пути дают один результат — выбирайте по инфраструктуре.

Статусная модель и `default = active`

GET /catalog по умолчанию отдаёт только активные товары — в любом режиме (offset/keyset/incremental/targeted). Чтобы увидеть другие статусы, передавайте statuses[] явно:

  • ?statuses[]=active&statuses[]=failed — активные + провалившиеся.
  • Зеркалирование удалений (чтобы гасить в 1С то, что удалили в ApiPay): ?updated_after=<iso>&statuses[]=active&statuses[]=deleted — инкремент, включающий удалённые.
  • «Призраки» (deleted без kaspi_item_id — позиции, которых никогда не было в Kaspi) не отдаются никогда, даже при ?statuses[]=deleted. Настоящие удаления (deleted с kaspi_item_id) через statuses[]=deleted доступны.

Лимиты, троттлы и таймауты

Параметр Значение
Батч POST /catalog до 100 позиций за запрос
Общий лимит ключа 200 запросов/мин (при 429 — заголовок Retry-After)
Targeted-вход GET /catalog суммарно ≤200 значений по ntins[]+barcodes[]+ids[]+external_refs[]
Targeted-выход 1000 строк соответствий; превышение любого → 422 catalog_match_overflow
POST /catalog/scan 30/мин + 2000/сутки на ключ + circuit-breaker ~90с при троттлинге частоты
GET /invoices/{id} 1000/мин (для поллинга без вебхуков)
Штрихкод 32 символа (лимит Kaspi)
  • Таргетед больше не усекается молча. Раньше ответ обрезался на 200 строках; теперь превышение входа (>200 значений) или выхода (>1000 строк) — явная 422 catalog_match_overflow. Разбивайте сверку на батчи (например по 100 external_ref).
  • Рекомендуемый клиентский read-timeout ≥ 15 секунд. Синхронный матчинг на create и точечное чтение больших батчей укладываются в этот бюджет; на слабом канале не ставьте таймаут в 3–5 с.
  • Не распараллеливайте заливку в много потоков на один кассир. Частые параллельные запросы вызывают паузы обработки — лейте батчами по 100 последовательно. Ровный поток надёжнее.

Ошибки и что делать

Ошибка Где Что делать
barcode_too_long статус товара failed Штрихкод длиннее 32 символов. Обрежьте/исправьте штрихкод, переотправьте позицию
catalog_item_duplicate статус товара failed Остаточный отказ Kaspi (похожее наименование). Найдите товар через GET /catalog и правьте существующий PATCH /catalog/{id} — с match-and-merge такое встречается редко
catalog_match_overflow 422 на GET /catalog Слишком много значений (>200) или строк соответствий (>1000) в точечном запросе. Разбейте сверку на меньшие батчи
catalog_busy 409 на POST /catalog Каталог занят другой операцией (не взят per-org lock за 5с). Повторите запрос через несколько секунд
sandbox_catalog_limit 400 на POST /catalog Лимит песочницы — 1000 позиций. Тестируйте на меньшем объёме или подключите платный тариф
Сессия кассира недействительна 400 на POST /catalog/scan Требуется переподключить кассира Kaspi
kaspi_throttled 429 на POST /catalog/scan Ограничена частота обработки (Retry-After) — подождите и повторите

Причину провала товара определяйте по error_code, а не по тексту error_message.

Обезличенный кейс: несколько SKU под одним штрихкодом

Интегратор заливал каталог канцтоваров, маппя товары по штрихкоду. У нескольких позиций один и тот же штрихкод стоял на разных фасовках — например, «Ручка гелевая синяя 0.5» и «Ручка гелевая синяя 0.7» помечены одним штрихкодом 4870000000001. Так как Kaspi держит один товар на штрихкод, вторая позиция отбивалась, а при сверке по штрихкоду 1С получала несколько id и брала не тот — товар показывался «удалённым», хотя в Kaspi был активен.

Решение: маппить по external_ref, а не по штрихкоду. Каждая позиция 1С получает свой 1c-000123/1c-000124; при сверке GET /catalog?external_refs[]=1c-000123 возвращает ровно один товар с однозначным id. Дублирующиеся штрихкоды при этом — не проблема: match-and-merge вернёт живой товар, а не породит «мёртвую» строку.

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

По какому полю маппить товары между 1С и ApiPay?

По external_ref — вашей ссылке на номенклатуру. Не по штрихкоду и не по имени: по одному штрихкоду бывает несколько связанных позиций, а имена меняются. external_ref уникален в пределах организации.

Что вернёт повторная заливка того же каталога?

Все позиции придут с matched_existing: true и живыми id, новые строки не создаются, по неизменным полям в Kaspi обращений нет. Заливка идемпотентна — можно гонять её по расписанию.

Как узнать, что товар реально доехал до Kaspi?

Ответ 202 — это «принято». Финал (active/failed) узнаёте вебхуком catalog.item_processed (если есть публичный URL) или поллингом GET /catalog?external_refs[]=.... Оба пути равноправны.

Пришёл matched_existing: true с name_differs: true — что делать?

Штрихкод/НТИН совпали с существующим товаром, но имя другое (для Kaspi это тот же товар). Имя мы не перезаписали. Если ваше имя правильное — отправьте его явным PATCH /catalog/{id}.

Targeted-запрос вернул 422 catalog_match_overflow.

Вы прислали больше 200 значений суммарно или наткнулись на >1000 строк соответствий. Разбейте сверку на батчи (например по 100 external_ref) и повторите. Тихого усечения теперь нет — это защита от потери строк.

Можно распараллелить заливку в 10 потоков для скорости?

Нет. Частые параллельные запросы вызывают паузы обработки — лейте батчами по 100 последовательно. Ровный поток надёжнее гонки потоков.

Что дальше

  • Массовая заливка: тысячи позиций из 1С — Массовая заливка каталога 1С (батчи по 100, очередь с ETA, батч-вебхук, разбор ошибок).
  • Трек D: отдайте вашему ИИ-ассистенту ссылку apipay.kz/for-ai — он соберёт цикл scan → bulk POST с external_ref → подтверждение → идемпотентный повтор. Общая интеграция с 1С — Интеграция ApiPay с 1С.
  • Трек M: ведёте каталог руками — статья Как заполнить каталог.

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

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

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

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