Правило №1: маппинг по `external_ref`, не по штрихкоду
Проставляйте у каждого товара external_ref — вашу ссылку в 1С (код номенклатуры, GUID, артикул, ≤191 символа). Это единственный надёжный якорь:
external_refUNIQUE в пределах организации — один товар 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. Разбивайте сверку на батчи (например по 100external_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: ведёте каталог руками — статья Как заполнить каталог.