> Источник: https://apipay.kz/guides/katalog-dlya-integratorov-1c · Обновлено: 2026-07-11 · apipay.kz
> ApiPay — независимый сервис приёма платежей поверх вашего Kaspi Pay.

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

**TL;DR.** Ключ маппинга товара между 1С и ApiPay — **`external_ref`** (код/GUID номенклатуры), а не штрихкод и не имя. По одному штрихкоду у мерчанта бывает несколько связанных позиций, поэтому сверка по штрихкоду путает id. `POST /catalog` работает в режиме **match-and-merge**: если позиция совпала с существующим товаром, возвращается **живой id** существующего товара с маркером `matched_existing: true` — дубли и «мёртвые» строки не создаются, **повторная заливка идемпотентна**. Итог заливки подтверждается двумя равноправными путями: **вебхук `catalog.item_processed`** (для SaaS с публичным URL) или **поллинг** `GET /catalog?external_refs[]=...` (дёшево для 1С/on-prem). Батч — до **100** позиций; точечное чтение — до **200** значений суммарно, иначе `422 catalog_match_overflow`. Для больших каталогов (тысячи позиций) есть отдельный операционный плейбук с очередью, ETA и разбором ошибок залива — [Массовая заливка каталога 1С](/guides/massovaya-zagruzka-kataloga-iz-1c).

> Статья про публичный API мерчанта с заголовком `X-API-Key`. Базовый адрес — всегда `https://api.apipay.kz/api/v1`. Базовую механику полей разбирает [Каталог, корзина и Нацкаталог](/guides/katalog-korzina-nackatalog).

## Правило №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}`.

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

```bash
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`, сматченные — с их текущим статусом):

```json
{
  "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С](/guides/massovaya-zagruzka-kataloga-iz-1c).

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

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

```json
{
  "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`:

```bash
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"
```

```json
{
  "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С](/guides/massovaya-zagruzka-kataloga-iz-1c) (батчи по 100, очередь с ETA, батч-вебхук, разбор ошибок).
- **Трек D:** отдайте вашему ИИ-ассистенту ссылку apipay.kz/for-ai — он соберёт цикл scan → bulk POST с `external_ref` → подтверждение → идемпотентный повтор. Общая интеграция с 1С — [Интеграция ApiPay с 1С](/guides/integraciya-apipay-s-1c).
- **Трек M:** ведёте каталог руками — статья [Как заполнить каталог](/guides/zapolnenie-kataloga-apipay).

---

ApiPay — независимый сервис и не аффилирован с АО «Kaspi Bank». Kaspi и Kaspi Pay — товарные знаки их правообладателя.
База знаний: https://apipay.kz/guides
