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

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

**TL;DR.** Большой каталог (тысячи и десятки тысяч позиций) заливайте в ApiPay **батчами
до 100 позиций** через `POST /catalog`, добавляя заголовок `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=...` по `error_code`. Темп обработки — **≈200 позиций/мин**,
поэтому 25 000 позиций проходят примерно за **2 часа**. Повторная заливка идемпотентна:
неизменённые позиции пропускаются по фингерпринту, а ключ маппинга — всегда `external_ref`.

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

## Коротко

| Факт | Значение |
|---|---|
| Размер батча `POST /catalog` | до **100** позиций за запрос |
| Идемпотентность запроса | заголовок `Idempotency-Key` (≤191) или body `idempotency_key` |
| Темп обработки | **≈200 позиций/мин** на кассира |
| 25 000 позиций | **≈2 часа** заливки |
| Прогресс очереди + ETA | `GET /catalog/queue` |
| Итог конкретного залива | `GET /catalog/batches/{id}` |
| Разбор ошибок | `GET /catalog/errors?batch_id=...` (окно по умолчанию — 7 дней) |
| Итоговый вебхук | `catalog.batch_processed` (один на весь залив) |
| Лимит поллинг-эндпоинтов | **600 запросов/мин** на ключ |

## Шаг 1. Заливка батчами по 100 с `Idempotency-Key`

Разбейте каталог на батчи **до 100 позиций** и отправляйте их по очереди. На каждый батч
ставьте свой `Idempotency-Key` — стабильную строку (например `upload-2026-07-11-part-042`).
Если сеть оборвалась и вы не знаете, дошёл ли запрос, повторите его **с тем же ключом** —
ApiPay вернёт уже принятый батч, а не создаст позиции второй раз.

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

```json
{
  "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С](/guides/katalog-dlya-integratorov-1c).

## Шаг 2. Следим за очередью и ETA

Большой каталог обрабатывается ровным потоком — примерно **200 позиций в минуту**. Чтобы
показать клиенту прогресс, читайте `GET /catalog/queue`:

```bash
curl -G https://api.apipay.kz/api/v1/catalog/queue \
  -H "X-API-Key: YOUR_API_KEY"
```

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

```bash
curl https://api.apipay.kz/api/v1/catalog/batches/8f14e45f-ceea-467e-9a2c-0000000000aa \
  -H "X-API-Key: YOUR_API_KEY"
```

```json
{
  "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-подпись, как у остальных событий):

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

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

```json
{
  "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С: синхронизация без
  дублей](/guides/katalog-dlya-integratorov-1c).
- **Базовые поля позиции, корзина и Нацкаталог:** [Каталог, корзина и
  Нацкаталог](/guides/katalog-korzina-nackatalog).

---

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