# Каталог ошибок ApiPay

> Источник: https://apipay.kz/errors · Обновлено: 2026-10-27 · apipay.kz
> Постоянные якоря: код из поля error_code в ответе API открывается по адресу https://apipay.kz/errors#{error_code}.

Ошибки приходят в нескольких формах — не путайте их между собой:

- **HTTP-статус** (`400`, `401`, `429` …) — общий класс ошибки, присутствует всегда.
- **Поле `error`** в теле ответа — конкретная причина синхронной ошибки. Значения бывают
  двух видов: машинные коды в snake_case (`organization_required`,
  `kaspi_session_not_configured`) и английские фразы целиком
  (`Organization not found or not verified`, `Invoice cannot be cancelled`).
  **Не сравнивайте текст `error` в коде** — для ветвления используйте `error_code`.
- **Поле `error_message`** — человекочитаемый текст асинхронной ошибки Kaspi (счёт создан
  со статусом `processing` и позже перешёл в `error`). Фиксированных кодов у Kaspi нет.
- **Поле `error_code`** (новое) — стабильный snake_case-код из фиксированного каталога.
  Стройте switch-логику по нему, а не по тексту `error`/`error_message`.

В колонке «Код» ниже значения сгруппированы по тому, где именно они появляются.

## HTTP-статусы (общие для всех эндпоинтов)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `400` (#400) | 400 | Bad Request — некорректный запрос или недопустимое состояние. Точная причина — в поле message или error |
| `401` (#401) | 401 | Unauthorized — API-ключ отсутствует, неверен, истёк или не привязан к организации; либо аккаунт деактивирован |
| `403` (#403) | 403 | Forbidden — организация заморожена (suspended) или не верифицирована для рабочего режима |
| `tariff_inactive` (#tariff_inactive) | 403 | Нет действующей подписки на ApiPay — оплатите тариф в кабинете. Закрывает все платные операции: создание, отмену и возврат счёта, чеки, изменяющий каталог, создание, изменение и возобновление подписок, проверку номера клиента. Грейса нет — блокировка сразу после expires_at (это поле приходит в теле ответа; null, если тариф не оформлялся). Чтение (GET), оплата тарифа, настройки, подключение кассира, а также приостановка и отмена подписок продолжают работать. В песочнице тариф не требуется |
| `404` (#404) | 404 | Not Found — ресурс не найден или принадлежит другой организации |
| `422` (#422) | 422 | Validation Error (машинный код validation_error) — ошибка валидации полей; детали в объекте errors. Например, POST /catalog/image без файла image отвечает именно так. Повтор того же запроса даст тот же результат — правьте запрос. |
| `429` (#429) | 429 | Too Many Requests — превышен общий лимит Public API (200 запросов/мин на API-ключ); смотрите заголовок Retry-After. Отдельно: POST /clients/check ограничен 60 запросами/мин и 10 000 запросами/сутки на API-ключ, а POST /invoices/qr — 200 QR/мин на организацию (на этом 429 заголовка Retry-After нет) |
| `500` (#500) | 500 | Server Error — внутренняя ошибка сервера |
| `502` (#502) | 502 | Bad Gateway — ошибка на стороне Kaspi API |
| `503` (#503) | 503 | Service Unavailable — сессия Kaspi недействительна или истекла |
| `organization_archived` (#organization_archived) | 403 | Организация, к которой привязан ключ, отправлена в архив. Отказ приходит на ЛЮБУЮ операцию этого ключа. Сам ключ остаётся активным, и перевыпуск ничего не меняет — доступ возвращает владелец аккаунта. Не путайте с 401: там ключ неверен, истёк или деактивирован, и перевыпуск как раз помогает. Что именно случилось с организацией, ответ не раскрывает. |
| `api_key_not_accepted` (#api_key_not_accepted) | 401 | Ключ API отправлен на путь кабинета `/api/*` вне `/api/v1/`: такие пути ключ не принимают. Сам ключ действует — перейдите на соответствующий путь `/api/v1/…`. Повтор того же запроса бессмыслен. |
| `sandbox_resource_removed` (#sandbox_resource_removed) | 410 | Песочный ресурс удалён — при переводе организации в рабочий режим или при очистке песочницы. Приходит на GET /invoices/{id}, /invoices/{id}/refunds, /invoices/{id}/receipt, /subscriptions/{id} и /qr-refunds/{id} вместе с `removed_at`. Прекратите опрос этого id: повтор бессмыслен. |

## Права интеграции (ключи, выданные партнёром)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `grant_not_granted` (#grant_not_granted) | 403 | Операция вне набора прав, который мерчант выдал вашей интеграции. Один и тот же ответ приходит на три случая — права нет, операция интеграциям не выдаётся никогда, операции нет в каталоге прав. По ответу они не различаются. Повтор бессмысленен — набор меняет мерчант в своём кабинете. |
| `channel_required` (#channel_required) | 403 | Живого доступа к организации у интеграции нет: мерчант его отозвал. Ключ при этом продолжает существовать и не деактивируется — доступ восстанавливает мерчант. Отдельный код от grant_not_granted намеренно: «не хватает права» и «доступа нет вовсе» лечатся по-разному. |
| `grant_snapshot_unreadable` (#grant_snapshot_unreadable) | 403 | Права интеграции временно недоступны — это временный сбой на нашей стороне, а не отказ в праве. Повторите позже; если отказ повторяется — обратитесь в поддержку. |

## Поле «error» при создании счёта (POST /invoices, /invoices/qr)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `organization_required` (#organization_required) | 400 | Организация не подключена — создайте sandbox-организацию для тестов или подключите кассира Kaspi |
| `Organization not found or not verified` (#organization-not-found-or-not-verified) | 400 | Рабочий режим: организация не верифицирована. Дождитесь верификации или тестируйте в песочнице |
| `kaspi_session_not_configured` (#kaspi_session_not_configured) | 400 | Кассир Kaspi не подключён. Подключите его в кабинете (Настройки → Авторизация Kaspi) или через поддержку (WhatsApp +7 700 307 65 12) |
| `kaspi_session_invalid` (#kaspi_session_invalid) | 503 | Сессия кассира Kaspi истекла или сброшена. Переподключите кассира — запросите новый SMS-код |
| `connection_ambiguous` (#connection_ambiguous) | 422 | У организации несколько активных касс, основная не выбрана — передайте kaspi_connection_id |
| `sandbox_invoice_limit` (#sandbox_invoice_limit) | 400 | Достигнут лимит тестовых счетов (1000 на организацию) — очистите песочницу в кабинете |
| `duplicate_idempotency_key` (#duplicate_idempotency_key) | 409 | Идемпотентность: активный счёт с таким external_order_id_idempotency уже существует — повторный POST /invoices с тем же ключом не создаёт дубликат (в ответе invoice_id и status существующего счёта). Перевыставление возможно, только если предыдущий счёт с этим external_order_id_idempotency находится в статусе expired, cancelled или error |
| `amount_must_be_whole_tenge` (#amount_must_be_whole_tenge) | 422 | Сумма счёта на номер телефона должна быть целой: тиыны Kaspi по такому счёту не принимает. Проверяется и голая amount, и итог корзины после скидок. Округлите сумму или выставьте счёт через POST /invoices/qr — там суммы с тиынами принимаются (но со скидкой позиции с ценой в тиынах QR не принимает — qr_discount_unsupported). В POST /invoices/bulk приходит по позиции: она попадает в invoices[] как failed, остальные создаются |
| `invoices_disabled` (#invoices_disabled) | 503 | Приём новых счетов временно приостановлен — идут технические работы. Счёт не создан, повторите позже. |

## Поле «error» только для QR-счёта (POST /invoices/qr)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `qr_discount_unsupported` (#qr_discount_unsupported) | 422 | Скидку на QR к этой корзине применить нельзя — отказ приходит до обращения в Kaspi, счёт не создаётся (в песочнице так же). Приходит на POST /invoices/qr (в том числе static: true) и POST /static-qr. Причины: цена позиции задана в тиынах (в cart_items[].price или в каталоге); скидка обнуляет позицию — на дешёвой позиции скидка округляется вверх до целого тенге (1 ₸ со скидкой 10 % это скидка 1 ₸). Процент скидки принимается только целый (1–99); дробный процент отбивается отдельно — 422 с errors.discount_percentage, а не этим кодом. Что делать: задайте целые цены в тенге и целый процент; позицию, которую скидка обнуляет, сделайте дороже или уберите скидку. Счёт по номеру телефона (POST /invoices) подойдёт, только если итог после скидки выходит в целых тенге — иначе 422 amount_must_be_whole_tenge |
| `qr_rate_limit` (#qr_rate_limit) | 429 | Слишком много QR-запросов для организации (лимит 200/мин) — подождите минуту. Заголовок Retry-After на этом 429 не возвращается (в отличие от общего лимита Public API 200/мин) |
| `qr_render_failed` (#qr_render_failed) | 500 | Не удалось сформировать изображение QR-кода — повторите запрос позже |
| `kaspi_error` (#kaspi_error) | 502 | Kaspi API вернул ошибку при создании QR-токена — повторите позже |
| `idempotency_conflict` (#idempotency_conflict) | 409 | Одноразовая платёжная ссылка с таким external_order_id_idempotency уже выпущена (POST /invoices/qr с static=true). Дубликат не создаётся: в ответе приходят static_qr_id и payment_url уже существующей ссылки — отправьте покупателю их. Ключ закрепляется за ссылкой в отдельном пространстве организации: в счёт он не переносится и не освобождается, когда истекает QR этой ссылки. У обычного QR-счёта дубль ключа приходит другим кодом — duplicate_idempotency_key. Доставка: sync HTTP 409 |
| `static_qr_disabled` (#static_qr_disabled) | 403 | Режим одноразовой платёжной ссылки (`static=true`) недоступен — ссылка не создана. Повтор не поможет, и для отдельной организации режим не включается: используйте статический QR (`POST /static-qr`) — этим кодом он не отбивается. Доставка: sync HTTP 403 |
| `sandbox_mode_mismatch` (#sandbox_mode_mismatch) | 409 | Значение is_sandbox расходится с режимом организации: запрос заявляет песочницу, а организация работает в боевом режиме, или наоборот. Ничего не создано — ни ссылка на оплату (POST /invoices/qr с static: true), ни лист статического QR (POST /static-qr): боевую ссылку нельзя выпустить под видом тестовой, и наоборот. Повтор того же запроса не поможет. Что делать: сверьте режим организации и передайте is_sandbox, который ему соответствует. Поле необязательное — без него режим не сверяется. Доставка: sync HTTP 409 |
| `recipient_name_unavailable` (#recipient_name_unavailable) | 422 | Боевой лист статического QR (POST /static-qr) или ссылка на оплату (POST /invoices/qr с static: true) не выпущены: у организации нет юридического названия, которое покупатель видит на странице оплаты как получателя платежа. Тело — message и error_code, без errors: это ошибка организации, а не полей запроса, и повтор её не исправит, в том числе с тем же ключом идемпотентности. На уже выпущенных листах и ссылках такой организации покупатель не получит нового QR и оплаты по номеру, а оплаченное и начатую оплату по-прежнему видит. Песочницы не касается. Что делать: не повторяйте запрос автоматически — обратитесь в поддержку ApiPay. Доставка: sync HTTP 422 |

## Поле «error» при отмене и возврате

| Код | HTTP | Что это и что делать |
|---|---|---|
| `Invoice cannot be cancelled` (#invoice-cannot-be-cancelled) | 400 | Отменить можно только счёт в статусе pending или processing |
| `Invoice is not refundable` (#invoice-is-not-refundable) | 400 | Возврат возможен только по оплаченному счёту, ещё не возвращённому полностью |
| `Refund amount exceeds available amount` (#refund-amount-exceeds-available-amount) | 400 | Сумма возврата больше доступной — смотрите available_for_refund в GET /invoices/{id}/refunds (в GET /invoices/{id} этого поля нет) |
| `qr_cancel_unsupported` (#qr_cancel_unsupported) | 409 | QR-счёт (is_qr_token: true) отменить нельзя — отмена для него не поддерживается. Статус счёта не меняется, в Kaspi ничего не уходит. В теле ответа есть expires_at — момент, после которого QR перестанет быть оплачиваемым; дождитесь статуса expired или выставьте новый счёт |

## Поле «error» при работе с подписками

| Код | HTTP | Что это и что делать |
|---|---|---|
| `sandbox_subscription_limit` (#sandbox_subscription_limit) | 400 | Достигнут лимит тестовых подписок (10 на организацию) — очистите песочницу |
| `Organization not verified` (#organization-not-verified) | 403 | Подписки в рабочем режиме доступны только верифицированной организации |
| `subscription_not_active` (#subscription_not_active) | 409 | Пропустить период можно только у подписки в статусе active: у подписки на паузе, отменённой или истёкшей пропускать нечего. Приходит на POST /subscriptions/{id}/skip-period и /skip-period/preview; поля error и error_code одинаковы. Повтор того же запроса не поможет. |
| `subscription_cycles_exhausted` (#subscription_cycles_exhausted) | 409 | По подписке уже получены все оплаты total_cycles — пропускать нечего, возобновить её тоже нельзя. Приходит на POST /subscriptions/{id}/skip-period и /skip-period/preview. Повтор не поможет: если услуга продолжается, создайте новую подписку. |
| `subscription_busy` (#subscription_busy) | 409 | Прямо сейчас по подписке выставляется счёт — период не пропущен, ничего не изменено. Повторите POST /subscriptions/{id}/skip-period примерно через минуту с тем же billing_period_start. |
| `skip_period_changed` (#skip_period_changed) | 409 | Пока человек подтверждал пропуск, период сменился — например, вышел новый счёт. Этот запрос ничего не пропустил. В теле ответа — свежий объект skip: покажите человеку, какой период будет пропущен теперь, и после подтверждения повторите POST /subscriptions/{id}/skip-period с новым billing_period_start. Вслепую не повторяйте. Повтор с уже пропущенным периодом — не этот отказ, а 200 с skip.replayed: true. |

## Корзина подписки: итог товаров (POST /subscriptions, PUT /subscriptions/{id})

| Код | HTTP | Что это и что делать |
|---|---|---|
| `cart_total_out_of_range` (#cart_total_out_of_range) | 422 | Итог корзины cart_items по текущим ценам каталога меньше 100 ₸ или больше 1 000 000 ₸. Подписка не создаётся и не меняется. Текст с суммой — в errors.cart_items. Что делать: измените количество позиций или цены в каталоге; автоповтор не поможет |
| `cart_total_not_whole_tenge` (#cart_total_not_whole_tenge) | 422 | Итог корзины cart_items по текущим ценам каталога получился с тиынами. Списание по подписке выставляется счётом на номер телефона, а он принимает только целые тенге. Подписка не создаётся и не меняется, текст с суммой — в errors.cart_items. Что делать: измените количество позиций или цены в каталоге так, чтобы итог был в целых тенге |

## Асинхронные ошибки Kaspi: status=error / поле error_message (HTTP-кода нет)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `status=error` (#status-error) | — | Счёт создан (201, статус processing), но Kaspi не смог его обработать — статус сменился на error. Причина — стабильный код в поле error_code (GET /invoices/{id} и вебхук invoice.status_changed): ветвитесь по нему, например client_not_found или network_unavailable. Поле error_message — тот же смысл текстом, только для показа человеку |
| `error_message: номер не в Kaspi` (#error-message-nomer-ne-v-kaspi) | — | «Этот номер телефона не зарегистрирован в Kaspi. Укажите номер с установленным приложением Kaspi.» — у клиента нет приложения Kaspi; попросите другой номер. В error_code — client_not_found |
| `error_message: сбой Kaspi` (#error-message-sboy-kaspi) | — | «Ошибка обработки платежа. Обратитесь в поддержку» или «Не удалось обработать счёт после нескольких попыток» — временный сбой Kaspi; повторите создание счёта позже |

## Поле error_code — стабильный машинный код (новое, рекомендуется)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `error_code (новое поле)` (#error-code-novoe-pole) | — | Стабильный snake_case-код ошибки из фиксированного каталога. Присутствует в JSON-ответах об ошибках и в webhook-объектах invoice (для status=error) и refund (для status=failed). Поля message/error сохранены без изменений. Определяйте тип ошибки по error_code, текст — для показа пользователю. У каждого кода ниже указана «Доставка» — приходит ли он асинхронно (в webhook) или синхронно (HTTP-ответ с кодом) |
| `network_unavailable` (#network_unavailable) | — | Сервис временно недоступен (сбой сети/Kaspi). Можно повторить позже. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `session_transient` (#session_transient) | — | Временные проблемы авторизации Kaspi. Можно повторить попытку. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `client_not_found` (#client_not_found) | — | Номер телефона не зарегистрирован в Kaspi. Не повторяемая — попросите другой номер. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `kaspi_throttled` (#kaspi_throttled) | — | Kaspi ограничил частоту запросов. Повторите через 2–3 минуты. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `refund_window_expired` (#refund_window_expired) | — | Срок возврата истёк или возврат уже сделан (часто для refund status=failed). Доставка: async — приходит в invoice.refunded (refund.error_code) |
| `refund_rejected_by_kaspi` (#refund_rejected_by_kaspi) | — | Kaspi отклонил возврат по этой операции. Отказ может быть временным — попробуйте позже ещё раз. Если не получится, выполните возврат вручную в приложении Kaspi Pay — но сначала проверьте весь счёт: отзовите все живые ссылки по счёту, включая выпущенные без source_refund_id (DELETE /qr-refunds/links/{id}), и дождитесь конца открытого окна сканирования — отзыв его не прерывает. Затем в GET /invoices/{id}/refunds: у ссылок в qr_refund_sessions только completed, expired или failed (ссылка без source_refund_id своего operation_state не имеет), у возвратов счёта operation_state не processing, buyer_link_active или uncertain, сам возврат не completed. Иначе покупатель получит деньги второй раз. Сервис сам этот возврат не повторяет: строка сразу финальная, повтор — ваш новый запрос на возврат. Доставка: async — приходит в invoice.refunded (refund.error_code) |
| `refund_execution_uncertain` (#refund_execution_uncertain) | — | Результат денежного запроса не подтверждён. Возврат остаётся processing, повтор и переключение способа запрещены. Исход разбирает владелец или разработчик организации в кабинете ApiPay по истории операций Kaspi — после разбора у возврата будет код refund_resolved_not_executed или refund_resolved_executed. Доставка: GET /invoices/{id}/refunds, refund.error_code; до разбора failed-вебхук не отправляется. |
| `refund_resolved_not_executed` (#refund_resolved_not_executed) | — | Возврат с неизвестным исходом разобран: владелец или разработчик организации отметил в кабинете ApiPay, что по истории операций Kaspi он не исполнен и деньги не ушли. До разбора возврат держал счёт занятым (operation_state: uncertain), теперь счёт свободен. Что делать: если возврат всё ещё нужен, оформите его заново — новым запросом с новым ключом идемпотентности: с прежним ключом вернётся этот же возврат в статусе failed. Доставка: async — приходит в invoice.refunded (refund.error_code), виден и в GET /invoices/{id}/refunds |
| `refund_resolved_executed` (#refund_resolved_executed) | — | Возврат с неизвестным исходом разобран: владелец или разработчик организации подтвердил в кабинете ApiPay, что в Kaspi он исполнен и деньги ушли. Синхронизация с Kaspi уже завела эту операцию отдельной записью возврата — её id приходит в resolved_refund_id, и деньги учтены ею один раз. status: failed у исходного возврата здесь не отказ: в GET /invoices/{id}/refunds у него operation_state: completed. Что делать: не оформляйте возврат заново — деньги уже вернулись; в учёте опирайтесь на запись resolved_refund_id. Доставка: GET /invoices/{id}/refunds, refund.error_code; вебхук invoice.refunded по этому разбору не отправляется — о деньгах уже объявила синхронизация |
| `refund_requires_buyer_confirmation` (#refund_requires_buyer_confirmation) | — | Kaspi требует подтверждение покупателя. Создайте QR-возврат через POST /api/v1/qr-refunds/links и попросите покупателя отсканировать QR-код. Доставка: async — приходит в invoice.refunded (refund.error_code) |
| `invoice_already_paid` (#invoice_already_paid) | — | Счёт уже оплачен. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `invoice_already_cancelled` (#invoice_already_cancelled) | — | Счёт уже отменён. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `invoice_not_found_in_kaspi` (#invoice_not_found_in_kaspi) | — | Счёт не найден в Kaspi. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `organization_not_configured` (#organization_not_configured) | — | Организация не настроена (нет рабочей привязки Kaspi). Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `unknown_error` (#unknown_error) | — | Непредвиденная ошибка обработки — обратитесь в поддержку. Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `qr_render_failed` (#qr_render_failed-2) | 500 | Не удалось сформировать изображение QR-кода — повторите запрос. Доставка: sync HTTP 500 + async (webhook invoice.status_changed, status=error) |
| `kaspi_session_invalid` (#kaspi_session_invalid-2) | 503 | Сессия кассира Kaspi истекла или сброшена — переподключите кассира. Доставка: sync HTTP 503 + async (webhook invoice.status_changed, status=error) |
| `kaspi_session_unavailable` (#kaspi_session_unavailable) | 503 | Не удалось проверить сессию Kaspi — попробуйте позже. Доставка: sync HTTP 503; на GET /invoices/{id}/receipt этот код приходит с HTTP 409 |
| `manager_throttled` (#manager_throttled) | 429 | Слишком много операций — попробуйте позже. Доставка: sync HTTP 429 |
| `whatsapp_otp_throttled` (#whatsapp_otp_throttled) | 429 | Слишком частые запросы кода WhatsApp — попробуйте позже. Доставка: sync HTTP 429 |
| `whatsapp_gateway_error` (#whatsapp_gateway_error) | 500 | Не удалось отправить код WhatsApp — попробуйте ещё раз. Доставка: sync HTTP 500 |
| `subscription_payment_failed` (#subscription_payment_failed) | 502 | Не удалось создать счёт для оплаты подписки — попробуйте позже. Доставка: sync HTTP 502 |
| `kaspi_error` (#kaspi_error-2) | 502 | Kaspi API вернул ошибку — повторите позже. Текст message содержит конкретную причину от Kaspi. Доставка: sync HTTP 502 + async (для QR-счетов: webhook invoice.status_changed, status=error) |
| `trial_daily_limit` (#trial_daily_limit) | 429 | Антифрод/триал: на пробном тарифе превышен дневной лимит создания счетов (50 счетов/сутки). Дождитесь следующего дня или оформите подписку. Доставка: sync HTTP 429 + заголовок Retry-After |
| `tariff_limit_reached` (#tariff_limit_reached) | 429 | Исчерпан лимит счетов оплаченного тарифа (Старт 30, Бизнес 100, Про 300, Про Макс 600 в сутки). Считаются только счета, созданные через API; счета из кабинета и песочницы в лимит не входят. Разовое превышение не блокирует: отказ приходит при систематическом превышении лимита либо при исчерпанном бюджете у организаций на помесячном подсчёте. meta.mode различает окна (daily — расчётные сутки, monthly — блок 30 дней), meta.limit — потолок, meta.used — израсходовано, meta.reset_at — момент обнуления счётчика; повторять запрос раньше бессмысленно. Ограничение снимается переходом на тариф выше сразу после оплаты. Автосписания по подпискам, созданным через API, при действующем ограничении пропускают цикл, дата следующего списания не сдвигается. В POST /invoices/bulk отказ приходит поэлементно и несёт только error_code и message. Доставка: sync HTTP 429 + заголовок Retry-After |
| `outstanding_recipient_limit` (#outstanding_recipient_limit) | 429 | Антифрод: слишком много неоплаченных (outstanding) счетов на одного получателя. Дождитесь оплаты или истечения ранее выставленных счетов. Доставка: sync HTTP 429 + заголовок Retry-After |
| `outstanding_org_limit` (#outstanding_org_limit) | 429 | Антифрод: слишком много неоплаченных (outstanding) счетов по организации в целом. Дождитесь оплаты или истечения ранее выставленных счетов. Доставка: sync HTTP 429 + заголовок Retry-After |
| `recipient_fanout_exceeded` (#recipient_fanout_exceeded) | 429 | Антифрод: превышен темп рассылки счетов по разным получателям (fan-out). Времени повтора в ответе нет — ожидание само по себе ограничение не снимает, причину разбирают через поддержку. Доставка: sync HTTP 429, без Retry-After и retry_after_seconds |
| `content_rejected` (#content_rejected) | 422 | Антифрод: содержимое счёта (например текст описания) отклонено проверкой. Исправьте текст и повторите. Доставка: sync HTTP 422 |
| `description_too_long` (#description_too_long) | 422 | Описание счёта длиннее, чем Kaspi показывает покупателю: видны только первые 60 символов, остальное Kaspi отбрасывает. Укоротите описание — вынесите в начало то, по чему плательщик узнает платёж. Повтор с тем же телом бесполезен. Доставка: sync HTTP 422 |
| `field_too_long` (#field_too_long) | 422 | Значение поля слишком длинное. Имя поля лежит ключом в errors — укоротите именно его; повтор с тем же телом бесполезен. Страховочный код: обычно слишком длинное значение отсекается обычной ошибкой валидации, форма которой не менялась. В POST /invoices/bulk приходит не на весь запрос, а строкой в invoices[] у конкретной позиции: ответ остаётся 201, соседние счета созданы, повторить нужно только эту позицию. Доставка: sync HTTP 422 |
| `sandbox_simulated_error` (#sandbox_simulated_error) | — | Симулированная ошибка в песочнице: POST /invoices/{id}/simulate-status со status=error присваивает счёту этот error_code (с опциональным error_message до 255 символов) и шлёт вебхук invoice.status_changed как при реальной ошибке. Только sandbox (в проде simulate-status недоступен). Доставка: async — приходит в invoice.status_changed (invoice.error_code) |
| `cart_items_mismatch` (#cart_items_mismatch) | — | Зарезервирован — сейчас не используется бэкендом (такие случаи приходят как kaspi_error). Доставка: — |
| `image_upload_failed` (#image_upload_failed) | — | Зарезервирован — сейчас не используется бэкендом. Доставка: — |
| `catalog_item_not_found` (#catalog_item_not_found) | — | Зарезервирован — сейчас не используется бэкендом. Доставка: — |

## Каталог: чтение списка и ошибок (GET /catalog, GET /catalog/errors)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `catalog_batch_filter_removed` (#catalog_batch_filter_removed) | 422 | Фильтр по партии (batch_id) больше не поддерживается: агрегат партий удалён. Отбивается по самому наличию параметра, поэтому пустой batch_id тоже вернёт 422 — это сделано намеренно, чтобы старый интегратор увидел отказ, а не молча получил не тот набор. Подтверждайте свой набор точечным запросом GET /catalog?external_refs[]=..., остаток работы смотрите в GET /catalog/queue, отказы — в GET /catalog/errors с окном from и to по моменту отказа, push — событие catalog.item_processed. Доставка: sync HTTP 422 |

## Каталог: резолв штрихкода (POST /catalog/scan)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `kaspi_session_expired` (#kaspi_session_expired) | 400 | Сессия Kaspi мерчанта истекла — нужна переавторизация кассира Kaspi. Доставка: sync HTTP 400 |
| `kaspi_throttled` (#kaspi_throttled-2) | 429 | Kaspi троттлит сессию при сканировании. Повторить после паузы (тело retry_after_seconds, заголовок Retry-After). Circuit-breaker: ~90 с сразу отдаёт 429 без обращения к Kaspi. Доставка: sync HTTP 429. Примечание: отличается от async error_code kaspi_throttled по счетам |
| `kaspi_scan_unavailable` (#kaspi_scan_unavailable) | 503 | Нацкаталог Kaspi временно недоступен — повторить позже. Доставка: sync HTTP 503 |

## Каталог: приём позиций (POST /catalog)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `catalog_item_invalid` (#catalog_item_invalid) | — | Позиция не прошла валидацию: имя, цена, единица измерения или другое поле. Конкретика — в error_message и карте errors по позиции. Доставка: per-item в rejected[] ответа POST /catalog |
| `catalog_item_duplicate` (#catalog_item_duplicate) | — | Kaspi отклонил товар как похожий на уже существующий в вашем каталоге. Проверьте, нет ли позиции с тем же названием и штрихкодом, — если товар действительно новый, измените название так, чтобы оно отличалось |
| `barcode_too_long` (#barcode_too_long) | — | Штрихкод длиннее допустимого — не более 32 символов. Обрежьте значение или передайте позицию без штрихкода |
| `catalog_delivery_incomplete` (#catalog_delivery_incomplete) | — | Позицию не удалось довезти до Kaspi — Kaspi её ни разу не видел и данные не отвергал. Чинить в позиции нечего: переотправьте её тем же POST /catalog с тем же external_ref, данные менять не нужно. Если вы шлёте Idempotency-Key, повтор должен идти с НОВЫМ ключом, иначе ответ вернётся из кеша идемпотентности и работа по строке не откроется. Раньше этот случай приходил под кодом catalog_item_invalid. Доставка: error_code строки каталога (GET /catalog/errors, событие catalog.item_processed) |
| `catalog_create_unresolved` (#catalog_create_unresolved) | — | Запрос на создание ушёл в Kaspi, но подтверждения по позиции так и не пришло, и попытки прекращены. Данные позиции ни при чём. Прежде чем заводить товар заново, посмотрите каталог в приложении Kaspi Pay: он мог всё-таки создаться, и повторное заведение даст две одинаковые позиции. Дожать строку — PATCH /catalog/{id} («Повторить» в кабинете); дословный повтор POST /catalog такую строку не возобновляет. Доставка: error_code строки каталога (GET /catalog/errors, событие catalog.item_processed) |
| `catalog_create_blocked` (#catalog_create_blocked) | — | Записывать каталог от имени организации было невозможно слишком долго — например, подключение кассира перестало быть активным, — и заявка на создание закрыта по сроку. Данные позиции ни при чём: сначала восстановите подключение, затем дожмите строку через PATCH /catalog/{id} («Повторить» в кабинете). Дословный повтор POST /catalog такую строку не возобновляет. Доставка: error_code строки каталога (GET /catalog/errors, событие catalog.item_processed) |
| `catalog_delete_abandoned` (#catalog_delete_abandoned) | — | Удаление позиции не удалось довести до конца: установить, что стало с позицией в каталоге Kaspi, не получилось. Обратитесь в поддержку. Повторный DELETE /catalog/{id} запускает проверку заново |

## Каталог: массовое удаление (POST /catalog/bulk-delete)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `catalog_delete_scope_required` (#catalog_delete_scope_required) | 422 | Не задан ни ids[], ни external_refs[], либо заданы оба списка сразу. Передайте ровно один список — сервер не додумывает, что именно вы хотите удалить |
| `catalog_match_overflow` (#catalog_match_overflow) | 422 | В списке слишком много значений — за один запрос принимается не более 200 ids или external_refs. Разбейте выборку на части |
| `catalog_bulk_delete_mismatch` (#catalog_bulk_delete_mismatch) | 409 | Переданный expected_count разошёлся с фактом: каталог изменился между разведкой и командой. В теле придёт actual_count — повторите dry_run и убедитесь, что удаляете то, что хотели. Поле expected_count необязательное, но при расхождении не удаляется ничего |
| `catalog_multi_tradepoint` (#catalog_multi_tradepoint) | 409 | У организации несколько торговых точек — массовое удаление для неё закрыто. Обратитесь в поддержку. У одиночного DELETE /catalog/{id} этот же код синхронно больше не приходит: запрос принимается с 202, а код появляется на самой позиции в error_code |
| `catalog_busy` (#catalog_busy) | 409 | Каталог занят другой операцией — повторите запрос через несколько секунд |
| `idempotency_key_conflict` (#idempotency_key_conflict) | 409 | Переданный Idempotency-Key уже занят другим телом либо другой каталожной операцией — пространство ключей общее. Возьмите новый ключ. Точный повтор ТОГО ЖЕ тела с тем же ключом конфликтом не считается: приходит 200 с idempotent_replay true, и ничего не удаляется повторно |
| `request_rate_limited` (#request_rate_limited) | 429 | Превышен лимит эндпоинта — 10 запросов в минуту. Разведка (dry_run) расходует тот же лимит. Пауза — в поле retry_after_seconds и в заголовке Retry-After |

## Каталог: изменение и снятие позиции (PATCH / DELETE /catalog/{id})

| Код | HTTP | Что это и что делать |
|---|---|---|
| `catalog_item_foreign_channel` (#catalog_item_foreign_channel) | 409 | Позиция заведена другой интеграцией этой организации: изменить и снять её можно только со стороны той интеграции либо из кабинета мерчанта. Отказ именно 409, а не 404, потому что позиция видна: в списке она приходит с source: other. ⚠️ Повторная заливка ошибки не даёт: POST /catalog при совпадении external_ref, barcode или ntin вернёт её id с matched_existing: true и НЕ обновит ни имя, ни цену — прежде чем считать позицию синхронизированной, сверяйте source. Позиций, у которых интеграции-автора нет (source: shared), это не касается. |

## Тариф: индивидуальные условия

| Код | HTTP | Что это и что делать |
|---|---|---|
| `custom_tariff_locked` (#custom_tariff_locked) | 409 | У мерчанта индивидуальные условия тарифа: смена тарифа не самообслуживаемая и оформляется поддержкой. Продление ТОГО ЖЕ тарифа не блокируется — это оплата, а не смена условий. Определяйте состояние заранее: is_custom в GET /api/v1/tariff и can_change_tier в каталогах планов; повтор запроса не поможет |

## Каталог: паритет has_catalog ↔ cart_items

| Код | HTTP | Что это и что делать |
|---|---|---|
| `catalog_requires_cart_items` (#catalog_requires_cart_items) | 422 | У организации включён каталог товаров: счёт должен нести состав покупки. Передайте cart_items[] — идентификаторы позиций берутся из GET /catalog, цена строки при необходимости переопределяется полем price. Приходит на POST /invoices/qr и POST /static-qr. У подписки (POST /subscriptions) этого кода нет: она принимает amount без корзины, а без amount и без cart_items отвечает 422 по полю amount. Доставка: sync HTTP 422 (без ключа errors) |
| `catalog_not_supported` (#catalog_not_supported) | 422 | У организации каталог товаров выключен, а в запросе передана корзина. Уберите cart_items[] и передайте amount. Если каталог вам нужен — он включается вместе с Kaspi Кассой (ОФД), напишите нам. Доставка: sync HTTP 422 (без ключа errors), а в POST /invoices/bulk — per-item в разборе позиций |

## Подключение кассира (POST /connections/{connection}/auth/*)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `duplicate_cashier_confirm` (#duplicate_cashier_confirm) | 409 | Этот номер кассира уже подключён к другой организации того же владельца. Это не блокировка: повторите тот же запрос send-phone с confirm_duplicate: true. В теле — can_override и existing_organization с полями id, name, connection_status и invoices_count; connection_status относится к подключению, а не к организации |
| `entrance_auth_disabled` (#entrance_auth_disabled) | 503 | Подключение кассира временно недоступно — идут технические работы. Повторите позже, данные подключения не пострадали |
| `cashier_change_requires_merchant` (#cashier_change_requires_merchant) | 403 | Сменить кассира у этой организации может только сам мерчант. Отказ приходит на send-phone, когда ключ выдан партнёром на организацию, аккаунтом которой партнёр не владеет, и присланный номер отличается от телефона текущего кассира. Первое подключение (кассира ещё нет) и переавторизация того же кассира проходят. Повтор не поможет: смену проводит мерчант — своей дверью либо по ссылке-приглашению. |
| `context_expired` (#context_expired) | 409 | Процесс авторизации кассира закрыт: контекст истёк либо Kaspi его потерял. Исход терминальный — повторять send-phone или verify-otp бесполезно, начните заново с POST /connections/{connection}/auth/init. Уже подтверждённая ранее привязка не сбрасывается. |
| `kaspi_busy` (#kaspi_busy) | 503 | Kaspi временно не принимает этот шаг. Приходит на шагах send-phone и verify-otp; в заголовке Retry-After — сколько секунд ждать. Сессия авторизации при этом закрыта: после паузы нужен новый POST /connections/{connection}/auth/init, а не повтор того же шага. |
| `sms_failed` (#sms_failed) | 502 | Kaspi не отправил кассиру SMS с кодом. Шаг send-phone можно повторить. Если отказ повторяется, проверьте, что номер заведён кассиром в Kaspi Pay. |
| `invalid_phone` (#invalid_phone) | 422 | Номер кассира не принят на шаге send-phone: Kaspi ответил, что номер не принадлежит ни одному оператору РК, — чаще всего это опечатка в цифрах. Код приходит в поле error. Повтор с тем же номером не поможет: попросите проверить номер и отправьте исправленный. |
| `kyc_required` (#kyc_required) | 403 | Анкета «Расскажите о бизнесе» ещё не одобрена — подключить кассира можно после одобрения. На init и send-phone SMS кассиру не отправляется вовсе; на verify-otp отказ терминальный (одобрение перестало действовать между шагами): Kaspi код уже принял, но подключение не создаётся — после одобрения нужен новый init. Повтор до одобрения бесполезен: решение принимает человек, Retry-After не отдаётся. Переподключение уже привязанного кассира (тот же номер) этим кодом не отбивается. Доставка: sync HTTP 403 |
| `http_error` (#http_error) | 502 | Сервис авторизации Kaspi временно недоступен. Может прийти на любом шаге подключения. Повторите шаг позже. |

## Подключение кассира: сверка организации Kaspi

| Код | HTTP | Что это и что делать |
|---|---|---|
| `organization_identity_conflict` (#organization_identity_conflict) | 409 | Подключаемый кассир принадлежит другой организации Kaspi. Организация закрепляется за первой привязкой и подключением нового кассира не переносится. Подключайте кассира той организации, за которой эта уже закреплена, либо заводите отдельную организацию. В теле приходят только error и message: идентификаторов и названия другой организации в ответе нет. Если владелец бизнеса действительно сменился, напишите в поддержку 77003076512. Доставка: sync HTTP 409 |
| `organization_identity_unavailable` (#organization_identity_unavailable) | 502 | Kaspi не вернул достоверные данные организации, и сверить её в этой попытке не удалось. Попытка входа закрыта: начните заново с POST /connections/{connection}/auth/init, повторная отправка кода на закрытом процессе вернёт no_process. Уже подтверждённая ранее привязка не сбрасывается и не блокируется. В теле приходят только error и message, отдельного поля error_code нет — разбирайте error. Доставка: sync HTTP 502 |
| `connection_identity_unverified` (#connection_identity_unverified) | 422 | Основным нельзя назначить подключение, которое ещё не подтвердило организацию или заблокировано. Проведите вход кассира до конца и повторите. В теле приходят только error и message, отдельного поля error_code нет — разбирайте error. Доставка: sync HTTP 422 |

## KYC: верификация бизнеса и webhook-домен (новое)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `kyc_daily_limit_reached` (#kyc_daily_limit_reached) | 429 | Молодая организация: пока анкета о бизнесе не одобрена, боевые счета не выставляются вовсе — meta.limit = 0 (песочница не затронута, интеграцию можно полностью отладить там). Чтобы снять ограничение — заполните короткую анкету «Расскажите о бизнесе» в кабинете (/business-profile), одобрение обычно за несколько часов. Повторять запрос до одобрения бесполезно. Доставка: sync HTTP 429 (meta.reset_at — когда лимит сбросится, meta.kyc_status — текущий статус) |
| `kyc_rejected` (#kyc_rejected) | 403 | Приём платежей недоступен по итогам проверки бизнеса (статус организации blocked). Не повторяемая — напишите в поддержку, если считаете это ошибкой. Доставка: sync HTTP 403 при создании счёта (POST /invoices, POST /invoices/qr) |
| `webhook_url_requires_domain` (#webhook_url_requires_domain) | 422 | Адрес webhook должен быть на вашем домене — IP-адреса не принимаются. Действует для ещё не одобренных организаций в рабочем режиме; в песочнице правило мягче. Укажите публичный HTTPS-адрес на домене. Доставка: sync HTTP 422 при сохранении webhook-URL |
| `webhook_url_tunnel_forbidden` (#webhook_url_tunnel_forbidden) | 422 | Туннели (ngrok и подобные) нельзя использовать для рабочих webhook — они временны и отключатся, уведомления перестанут приходить. Укажите адрес на вашем домене. Действует для ещё не одобренных организаций в рабочем режиме; в песочнице туннель для локального теста допустим. Доставка: sync HTTP 422 при сохранении webhook-URL |

## Фискальные чеки (Kaspi OFD)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `fiscal_receipts_disabled` (#fiscal_receipts_disabled) | 403 | Исторический код: с 06.09.2026 не приходит. Может встретиться в старых логах и записях доставки вебхуков. Ветку обработки можно оставить — она больше не срабатывает |
| `not_sandbox` (#not_sandbox) | 403 | Поле simulate прислано боевой организацией — форсировать исход чека можно только в песочнице. Чек не создан. Уберите simulate из тела запроса. Доставка: sync HTTP 403 (POST /receipts) |
| `kaspi_session_not_configured` (#kaspi_session_not_configured-2) | 409 | К организации не привязан кассир Kaspi Pay — выбить чек не через кого. Подключите кассира в кабинете (Настройки → Подключение кассира). Доставка: sync HTTP 409 |
| `duplicate_client_operation_id` (#duplicate_client_operation_id) | 409 | Чек с таким client_operation_id уже выбивался — второй раз он не пробьётся (идемпотентность). В теле ответа приходит receipt_id существующего чека: опросите его через GET /receipts/{id}. Повтор после failed разрешён, но с НОВЫМ ключом. Доставка: sync HTTP 409 |
| `connection_ambiguous` (#connection_ambiguous-2) | 422 | К организации привязано несколько кассиров — непонятно, через какого выбивать чек. Передайте kaspi_connection_id явно. Доставка: sync HTTP 422 |
| `receipt_preview_unavailable` (#receipt_preview_unavailable) | 503 | Предпросмотр чека временно недоступен (POST /receipts/preview). Повторяемая — попробуйте позже; на выбивание самого чека не влияет. Доставка: sync HTTP 503 |
| `receipt_not_found` (#receipt_not_found) | 404 | Чек не найден или принадлежит другой организации. Доставка: sync HTTP 404 (GET /receipts/{id}) |
| `shift_closed` (#shift_closed) | — | Смена на кассе закрыта — чек выбить нельзя. Откройте смену в приложении Kaspi Pos и повторите с новым client_operation_id. В песочнице воспроизводится через simulate. Доставка: async, status=failed (GET /receipts/{id}, вебхук receipt.failed) |
| `item_not_fiscal` (#item_not_fiscal) | — | Позицию нельзя выбить в чек: её нет в каталоге организации; в бою она ещё не синхронизирована с Kaspi (in_kaspi_catalog=false — в песочнице это поле всегда false, и чек выбивается); или у неё есть штрихкод, но нет НТИН (ntin_missing=true) — дорезолвите НТИН через POST /catalog/scan и сохраните его в позиции (PATCH /catalog/{id}) и повторите чек. Несинхронизированную позицию повторите, когда in_kaspi_catalog станет true (GET /catalog или вебхук catalog.item_processed). Позиция без НТИН и без штрихкода (например, услуга) этим кодом не отказывает — она выбивается в чек без маркировки Нацкаталога. Доставка: async, status=failed |
| `receipt_vat_not_supported` (#receipt_vat_not_supported) | — | В чеке есть позиция со ставкой НДС — такие чеки не выбиваются. Фискальный документ не создан. Уберите позицию со ставкой НДС из состава чека и выбейте чек заново. Не повторяемая: тот же состав чека вернёт тот же отказ. Доставка: async, status=failed (GET /receipts/{id}, вебхук receipt.failed) |
| `rfo_missing` (#rfo_missing) | — | Не определён код торговой точки — Kaspi не может зарегистрировать чек. Подключите Kaspi Kassa (ОФД) к точке в приложении Kaspi Pay, затем переподключите кассира в ApiPay. Доставка: async, status=failed |
| `receipt_kaspi_error` (#receipt_kaspi_error) | — | Kaspi отклонил выбивание чека; подробности — в поле error_message чека. Повтор возможен с НОВЫМ client_operation_id. В песочнице воспроизводится через simulate. Доставка: async, status=failed |
| `receipt_dispatch_error` (#receipt_dispatch_error) | — | Не удалось отправить чек в Kaspi (сеть или временный сбой). Повторяемая — попробуйте ещё раз с новым client_operation_id. Доставка: async, status=failed |
| `receipt_ofd_token_revoked` (#receipt_ofd_token_revoked) | — | Фискальная привязка кассы отозвана — мерчанту нужно перепривязать ОФД в приложении Kaspi. Приём оплат при этом работает: счета и QR продолжают создаваться, встают только чек и изменяющий каталог. Не путайте с kaspi_session_invalid — платёжная сессия жива, переподключение кассира не поможет. Повтор имеет смысл только ПОСЛЕ перепривязки ОФД, временем не лечится. Доставка: async, status=failed |

## Чек Kaspi по счёту (GET /invoices/{id}/receipt)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `receipt_not_available_for_status` (#receipt_not_available_for_status) | 409 | Чек есть только у оплаченного или частично возвращённого счёта. Проверьте status счёта перед запросом. Тот же код приходит, если у оплаченного счёта ещё нет числового идентификатора Kaspi — чек по такому счёту получить нельзя. Не повторяемая. Доставка: sync HTTP 409 |
| `receipt_requires_kaspi_kassa` (#receipt_requires_kaspi_kassa) | 409 | Фискальный чек Kaspi формирует только при подключённой Kaspi Кассе. Не повторяемая, Retry-After нет. Счёт и оплата не затронуты. Доставка: sync HTTP 409 |
| `kaspi_session_expired` (#kaspi_session_expired-2) | 409 | Кассир, через которого прошла оплата, требует переподключения — чек получить нельзя, пока кассир не подключён заново. Счёт и оплата не затронуты. Доставка: sync HTTP 409 |
| `kaspi_session_unavailable` (#kaspi_session_unavailable-2) | 409 | Кассир по счёту сейчас недоступен — повторите позже; если повторяется, проверьте подключение кассира в кабинете. Доставка: sync HTTP 409 |
| `receipt_rate_limited` (#receipt_rate_limited) | 429 | Запросов чеков слишком много — у ручки отдельный лимит. Повторите через минуту. Доставка: sync HTTP 429 |
| `receipt_unavailable` (#receipt_unavailable) | 503 | Чек получить не удалось. Повторяемая — попробуйте через минуту. Счёт и оплата не затронуты. Доставка: sync HTTP 503 |

## Возврат по QR (клиент подтверждает сканированием)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `qr_refund_not_identified` (#qr_refund_not_identified) | 409 | Покупатель ещё не отсканировал возвратный QR. Приходит на GET /qr-refunds/{id}/operations и на execute. Не ошибка интеграции — дождитесь статуса customer_identified (поллинг GET /qr-refunds/{id} или вебхук qr_refund.identified). Сессия жива. |
| `qr_refund_expired` (#qr_refund_expired) | 409 | Срок сессии истёк. У сессии ДВА срока: окно скана QR — не более 90 секунд (expires_at), и отдельно ограничено время на выбор операции после подтверждения покупателем. Возврат не сделан — начните новую сессию. У ссылки «Возврат ApiPay» дополнительно действует link_expires_at (24 часа); пропущенное окно скана её не завершает — покупатель может открыть новое окно той же ссылкой. Не повторяемая. |
| `qr_refund_completed` (#qr_refund_completed) | 409 | Возврат по этой сессии уже выполнен. Повторный execute не пройдёт (идемпотентность). Не считайте это отказом: запросите GET /qr-refunds/{id} и возьмите refunded_amount и receipt_url оттуда. |
| `operation_not_returnable` (#operation_not_returnable) | 422 | Kaspi не разрешает возврат по выбранной операции (returnable: none) — срок возврата истёк либо деньги уже возвращены. Сессия остаётся живой: выберите другую покупку. |
| `refund_amount_exceeds_available` (#refund_amount_exceeds_available) | 422 | Сумма больше доступной к возврату. Актуальное значение — в available_for_refund из GET /qr-refunds/{id}/operations/{ref}. Сессия жива, можно повторить с корректной суммой. |
| `refund_insufficient_funds` (#refund_insufficient_funds) | 422 | На счёте Kaspi Pay мерчанта не хватает денег на возврат. Отказ приходит ДО отправки денег: ничего не списано, сессия остаётся customer_identified. Пополните счёт Kaspi Pay — и повторите возврат на той же сессии, пока не истёк срок идентификации покупателя; после него понадобится новая ссылка. Второй выход — вернуть покупателю наличными. Раньше этот отказ приходил как 502 kaspi_error, и повтор без пополнения счёта давал тот же результат. Тот же код приходит асинхронно в refund.error_code вебхука invoice.refunded (status=failed) для POST /invoices/{id}/refund. |
| `partial_refund_requires_return_items` (#partial_refund_requires_return_items) | 422 | Эту покупку Kaspi возвращает только по позициям: пришлите items вместо amount. Напоминание: amount и items взаимоисключимы. |
| `not_found` (#not_found) | 404 | Сессия не найдена или принадлежит другой организации — код ответа одинаков в обоих случаях (не оракул чужих сессий). Начните новую сессию. |
| `not_sandbox` (#not_sandbox-2) | 403 | Поле simulate или POST /qr-refunds/{id}/simulate прислан боевой организацией — форсировать шаги покупателя можно только в песочнице. |
| `qr_refund_execution_uncertain (202 своему запросу / 409 повтору)` (#qr-refund-execution-uncertain-202-svoemu-zaprosu-409-povtoru) | 202 | ⛔ Самый важный код группы. Денежный запрос ушёл, а ответ Kaspi не доказал ни успех, ни отказ — возврат мог быть применён. Своему запросу приходит 202 со снимком сессии в статусе execution_uncertain, повтору того же execute — 409. Сессия терминальна для автоматики. Повторять execute НЕЛЬЗЯ ни при каких условиях: второй запрос — это второй возврат живых денег покупателю. Retry-After не отдаётся. Разбирается вручную — обратитесь в поддержку. До ответа поддержки не проводите этот возврат ни повторным execute, ни вручную в приложении Kaspi Pay — деньги могли уже уйти. Параллельно приходит вебхук qr_refund.execution_uncertain. Доставка: sync HTTP 202 и HTTP 409 (POST /qr-refunds/{id}/execute) |
| `qr_refund_execution_result_unavailable` (#qr_refund_execution_result_unavailable) | 202 | ⛔ Денежный запрос ушёл, а его исход не доказан — деньги могли уйти. Снимка сессии в этом ответе нет: не ждите её полей и не выводите статус из их отсутствия. Повторять execute НЕЛЬЗЯ: попытка уже потрачена. Retry-After не отдаётся. Разбирается вручную — обратитесь в поддержку. Доставка: sync HTTP 202 (POST /qr-refunds/{id}/execute) |
| `qr_refund_execution_in_progress` (#qr_refund_execution_in_progress) | 409 | ⛔ Возврат по этой сессии уже идёт: его начал предыдущий запрос. Сам этот 409 денег не двигает, но повторять execute НЕЛЬЗЯ — второй запрос рискует стать вторым возвратом. Ретрай на этот код не строить. Дождитесь терминального состояния через GET /qr-refunds/{id} или вебхук. Доставка: sync HTTP 409 (POST /qr-refunds/{id}/execute) |
| `qr_refund_execution_attempts_exhausted` (#qr_refund_execution_attempts_exhausted) | 409 | Число денежных попыток по одной сессии возврата исчерпано. Kaspi при этом не вызывается вовсе: деньги не двигались, сессия остаётся customer_identified. Повторять execute на этой сессии бесполезно — выпустите новую ссылку возврата и получите новое подтверждение покупателя. Автоповтор execute не зацикливайте: на 422 refund_insufficient_funds и 502 kaspi_error повтор имеет смысл по действию человека (например, после пополнения счёта Kaspi Pay), но окно подтверждения покупателя короткое: на той же сессии успевает лишь тот, у кого деньги появляются сразу, — иначе тоже новая сессия. А на этом коде — только новая сессия. Доставка: sync HTTP 409 (POST /qr-refunds/{id}/execute) |
| `kaspi_error` (#kaspi_error-3) | 502 | Kaspi недоступен либо ответил неразбираемо до отправки денег. Деньги не двигались, сессия остаётся customer_identified. Повтор здесь допустим: попробуйте ещё раз, пока жива сессия. Доставка: sync HTTP 502 (POST /qr-refunds/{id}/execute) |
| `qr_refund_execution_context_unavailable` (#qr_refund_execution_context_unavailable) | 503 | Условия возврата не удалось проверить достоверно, поэтому денег мы не трогали: ни одного вызова Kaspi не сделано. Сессия остаётся customer_identified и жива. Повторить execute можно — попробуйте позже, пока не истёк срок сессии. Доставка: sync HTTP 503 (POST /qr-refunds/{id}/execute) |
| `qr_refund_execution_disabled` (#qr_refund_execution_disabled) | 503 | Денежное выполнение возвратов сейчас закрыто. Отказ происходит до отправки денег: ни одного вызова Kaspi не сделано, деньги не двигались, сессия остаётся customer_identified. Повторить execute можно позже; если код держится, обратитесь в поддержку. Доставка: sync HTTP 503 (POST /qr-refunds/{id}/execute) |
| `qr_refund_actor_no_longer_authorized` (#qr_refund_actor_no_longer_authorized) | 403 | Доступ, которым начат возврат, к моменту отправки денег уже не действует: ключ отозван или просрочен, канал закрыт, доступ к организации потерян. Это отдельный код, а не tariff_inactive: у организации право может быть в полном порядке, вопрос к конкретному ключу. Отказ до отправки денег — деньги не двигались, сессия жива. Перевыпустите ключ или восстановите права и повторите execute. Доставка: sync HTTP 403 (POST /qr-refunds/{id}/execute) |
| `qr_refund_temporarily_unavailable` (#qr_refund_temporarily_unavailable) | 503 | Операция временно недоступна: приходит на выпуск ссылки POST /qr-refunds/links, на её отзыв и на старт POST /qr-refunds. Ни одного вызова Kaspi не сделано, ничего не создано и не списано. Повторить можно позже. Доставка: sync HTTP 503 |
| `qr_refund_link_limit_reached` (#qr_refund_link_limit_reached) | 429 | У организации уже максимум неиспользованных ссылок на возврат — до 20 одновременно. Ссылка не создана, Kaspi не вызывался, ничего не потрачено. Дождитесь, пока покупатели откроют выпущенные ссылки, либо отзовите лишние через DELETE /qr-refunds/links/{id} — и повторите выпуск. Доставка: sync HTTP 429 (POST /qr-refunds/links) |
| `qr_refund_link_not_revocable` (#qr_refund_link_not_revocable) | 409 | Отозвать нечего: покупатель уже открыл ссылку либо она терминальна по другой причине. Это не 404 — ссылка существует и принадлежит вам. Повтор отзыва результата не изменит; текущее состояние читайте через GET /qr-refunds/{id}. Активированная сессия завершится сама по своему окну скана. Доставка: sync HTTP 409 (DELETE /qr-refunds/links/{id}) |
| `qr_refund_link_invoice_busy` (#qr_refund_link_invoice_busy) | 409 | По счёту уже выполняется обычный или QR-возврат, включая неизвестный денежный результат. Новый возврат не создан. Дождитесь результата. Неактивированную ссылку можно явно отозвать, если сервер допускает отзыв. Доставка: HTTP 409, POST /invoices/{id}/refund и POST /qr-refunds/links. |
| `invoice_not_refundable` (#invoice_not_refundable) | 422 | Этот счёт нельзя вернуть по ссылке: он не найден, принадлежит не вашей организации, не оплачен, уже возвращён целиком или остатка к возврату по нему нет — причины снаружи намеренно не различаются. Ссылка не создана, Kaspi не вызывался, деньги не двигались. Повтор с тем же invoice_id результата не изменит: проверьте id счёта и его состояние через GET /invoices/{id}, а если возврат всё равно нужен — выпустите ссылку без привязки к счёту. Тот же код приходит и в вебхуке qr_refund.failed, если счёт стал недоступен к возврату к моменту автопроведения; тогда нужна новая ссылка. Доставка: sync HTTP 422 (POST /qr-refunds/links), вебхук qr_refund.failed |
| `qr_refund_operation_not_found` (#qr_refund_operation_not_found) | — | Оплата этого счёта не найдена среди операций покупателя, который подтвердил себя по ссылке: например, код отсканировал другой человек. HTTP-отказом не приходит: это пара error_code/error_message в снимке сессии, статус сессии — failed, приходит вебхук qr_refund.failed после qr_refund.identified. Возврат не сделан, деньги не двигались. Повторять нечего — выпустите новую ссылку и убедитесь, что её открывает и сканирует сам покупатель, которому возвращаете деньги. Доставка: GET /qr-refunds/{id}, вебхук qr_refund.failed |
| `qr_refund_link_revoked` (#qr_refund_link_revoked) | — | Продавец сам отозвал ссылку на возврат через DELETE /qr-refunds/links/{id}. HTTP-отказом не приходит: это пара error_code/error_message в снимке сессии, статус сессии — expired. Возврат не сделан и по этой ссылке уже не будет. Нужен возврат — выпустите новую ссылку. Доставка: GET /qr-refunds/{id}, ответ DELETE /qr-refunds/links/{id} |
| `qr_refund_link_expired` (#qr_refund_link_expired) | — | Неактивированная ссылка не была использована до своего срока: link_expires_at — 24 часа с выпуска, и он не продлевается ничем, ни открытием страницы, ни перезагрузкой. HTTP-отказом не приходит: пара error_code/error_message в снимке сессии, статус — expired. Возврат не сделан, Kaspi не вызывался. Выпустите новую ссылку. Доставка: GET /qr-refunds/{id} |
| `qr_return_scan_timeout` (#qr_return_scan_timeout) | 409 | Окно скана закрылось раньше, чем возвратный QR был выдан покупателю. Окно — не более 90 секунд, действующее значение приходит в scan_wait_timeout_seconds. Возврат не сделан, деньги не двигались, сессия терминальна. Повторять тот же запрос нечем — выпустите новую ссылку POST /qr-refunds/links. Доставка: sync HTTP 409, а также error_code в снимке сессии |
| `qr_return_identity_timeout` (#qr_return_identity_timeout) | — | Покупатель подтверждение прошёл, но возврат не был выполнен в отведённое время — здесь не успел продавец, в отличие от qr_return_scan_timeout. HTTP-отказом не приходит: пара error_code/error_message в снимке сессии, статус — expired. Деньги не двигались. Начните возврат заново — новой ссылкой. Доставка: GET /qr-refunds/{id}, вебхук qr_refund.expired |
| `qr_return_not_found` (#qr_return_not_found) | — | Kaspi больше не видит эту операцию возврата. HTTP-отказом не приходит: пара error_code/error_message в снимке сессии, статус — failed. Возврат не сделан, деньги не двигались. Повторять нечего — создайте новую ссылку на возврат. Доставка: GET /qr-refunds/{id}, вебхук qr_refund.failed |
| `qr_refund_activation_in_progress` (#qr_refund_activation_in_progress) | 202 | Запрос на выдачу возвратного QR принят и уже ушёл, а исход ещё не записан. Это не ошибка: единственная попытка активации потрачена. Повторять запрос нельзя — ни ссылка, ни старт второй попытки не делают. Состояние читайте через GET /qr-refunds/{id} или ждите вебхук. Доставка: sync HTTP 202 (POST /qr-refunds) |
| `qr_refund_activation_failed` (#qr_refund_activation_failed) | 502 | Начать подтверждение не удалось: возвратный QR выдан не был. Статус сессии — failed, возврат не начинался и деньги не двигались. Попытка потрачена: повторять тот же запрос нельзя, нужна НОВАЯ сессия — новая ссылка POST /qr-refunds/links либо новый POST /qr-refunds. Доставка: sync HTTP 502, вебхук qr_refund.failed |
| `organization_not_verified` (#organization_not_verified) | 400 | Организация не верифицирована — возврат по QR ей недоступен. Код приходит на POST /qr-refunds, POST /qr-refunds/links и POST /qr-refunds/{id}/execute. Отказ происходит до обращения в Kaspi: деньги не двигались. Пройдите верификацию в кабинете и повторите. |

## Касса: смены, наличные, сверка (/cashbox/*)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `cashbox_disabled` (#cashbox_disabled) | 403 | Кассовые операции для организации сейчас недоступны. В песочнице этот отказ не приходит. |
| `cashbox_kkm_unknown` (#cashbox_kkm_unknown) | 409 | Номер кассы (ККМ) для организации неизвестен. Он есть только у организаций с подключённой кассой Kaspi (ОФД) — той же, что включает каталог товаров. Если продажи идут через Kaspi Pos без ОФД, ждать нечего: кассовых смен у такой организации не существует. Если Kaspi Касса подключена, проверьте, что кассир подключён и его сессия активна; при нескольких кассах укажите kaspi_connection_id нужной точки. Если код продолжает приходить — напишите в поддержку. |
| `rfo_missing` (#rfo_missing-2) | 409 | Код торговой точки Kaspi не определён: этим кодом отвечают GET /cashbox/summary и оба тумблера настроек. Чаще всего это значит, что к аккаунту Kaspi Pay не подключена касса Kaspi (ОФД) — тогда кассовых смен у организации не существует. Если Kaspi Касса подключена, при нескольких кассах передавайте kaspi_connection_id нужной точки. Код торговой точки ApiPay получает при подключении кассира. Если Kaspi Кассу подключили позже, переподключите кассира или нажмите в настройках кабинета «Обновить информацию об организации». ⚠️ Оба действия могут включить каталог товаров: после него POST /invoices/qr и POST /static-qr без cart_items отвечают 422 catalog_requires_cart_items, а напечатанные QR-листы без состава перестают работать. Тот же код приходит и в фискальном контуре, по той же причине: чек не выбивается, пока код торговой точки не определён. |
| `cashbox_no_open_shift (см. operation.error_code)` (#cashbox-no-open-shift-sm-operation-error-code) | — | Открытой смены на кассе нет — закрывать нечего. |
| `cashbox_shift_already_closed` (#cashbox_shift_already_closed) | — | Смена уже закрыта. Для операции закрытия это успешный исход: целевое состояние достигнуто. |
| `cashbox_shift_not_found` (#cashbox_shift_not_found) | 404 | Смена с таким id недоступна. Возьмите id из GET /cashbox/shifts. |
| `cashbox_operation_not_found` (#cashbox_operation_not_found) | 404 | Операция не найдена или принадлежит другой организации — код ответа одинаков в обоих случаях. |
| `cashbox_duplicate_operation` (#cashbox_duplicate_operation) | 409 | client_operation_id уже использован. В теле ответа приходит operation_id принятой операции — по нему продолжайте поллинг; для нового закрытия возьмите новый ключ. Ключ не освобождается даже после failed. |
| `cashbox_busy (см. operation.error_code)` (#cashbox-busy-sm-operation-error-code) | — | Касса занята другой операцией. Повторите позже новым client_operation_id. |
| `cashbox_operation_failed (см. operation.error_code)` (#cashbox-operation-failed-sm-operation-error-code) | — | Kaspi не выполнил операцию. При resolution.safe_to_retry=false автоматический повтор небезопасен — решение оставьте человеку. |
| `cashbox_unavailable` (#cashbox_unavailable) | 503 | Касса Kaspi временно недоступна, данные получить не удалось. Повторите позже. |
| `kaspi_session_expired` (#kaspi_session_expired-3) | 409 | Кассир подключён, но его сессия Kaspi не авторизована — переавторизуйте кассира в кабинете ApiPay. Не повторяемая, Retry-After нет: повтор не поможет, пока кассир не переавторизован. Синхронно приходит на GET /cashbox/summary и GET /cashbox/shifts, при закрытии смены — в operation.error_code. Временный сбой Kaspi отвечает 503 cashbox_unavailable. |
| `cashbox_report_unavailable` (#cashbox_report_unavailable) | 503 | Отчёт по смене получить не удалось. Повторите позже. |
| `cashbox_toggle_in_progress` (#cashbox_toggle_in_progress) | 503 | Переключение тумблера уже выполняется. Повторите позже. |
| `cashbox_toggle_unavailable` (#cashbox_toggle_unavailable) | 503 | Текущее значение на кассе проверить не удалось — переключение не выполнено. Повторите позже. |
| `cashbox_settings_owner_key_required` (#cashbox_settings_owner_key_required) | 403 | Тумблеры настроек кассы переключает только ключ, выпущенный владельцем организации: PUT /cashbox/settings/auto-close и PUT /cashbox/settings/auto-withdrawal. Ключ сотрудника получает этот отказ, и повтор не поможет — перевыпустите ключ от имени владельца. Чтение GET /cashbox/settings доступно любому ключу организации. |

## Песочница: симуляции, состояние и очистка

| Код | HTTP | Что это и что делать |
|---|---|---|
| `sandbox_scenario_unknown` (#sandbox_scenario_unknown) | 422 | Заголовок X-Sandbox-Scenario несёт сценарий, которого нет в списке. Поле scenarios в ответе перечисляет известные. Исправьте значение заголовка — повтор с тем же значением даст тот же ответ. У боевой организации заголовок не действует: запрос выполняется по-настоящему, ответ обычный, плюс заголовок X-Sandbox-Scenario-Ignored: live_organization. |
| `run_event_mismatch` (#run_event_mismatch) | 409 | Счёт привязан к проверке, заявленной на другой исход (поле expected_event). Приходит на POST /invoices/{id}/simulate-status и POST /static-qr/{id}/simulate. Переход не выполнен — симулируйте заявленный исход. |
| `qr_refund_simulation_not_applicable` (#qr_refund_simulation_not_applicable) | 409 | Это событие нельзя симулировать в текущем состоянии ссылки возврата: например, identified у ссылки, которую покупатель ещё не открыл (сначала activated). Приходит на POST /qr-refunds/{id}/simulate. Состояние не меняется — сначала симулируйте событие, которое ведёт в нужное состояние. |
| `refund_effect_in_progress` (#refund_effect_in_progress) | 409 | По этой ссылке возврата сейчас идёт другая операция. Приходит на POST /qr-refunds/{id}/simulate вместе с retry_after_seconds. Переход не выполнен — повторите через указанное время. |
| `subscription_simulation_not_applicable` (#subscription_simulation_not_applicable) | 409 | Подписка сейчас не в том состоянии, чтобы симулировать списание или окончание льготного периода. Причину называет текст ответа: календарный автоплатёж, исчерпанные циклы, неоплаченный счёт уже выставлен, списание отложено. Приходит на POST /subscriptions/{id}/simulate-billing и POST /subscriptions/{id}/simulate. Ничего не записано — сначала приведите подписку в нужное состояние. |
| `static_qr_unavailable` (#static_qr_unavailable) | 409 | Печатный лист сейчас не выставляет счёт, поэтому скан или оплату симулировать нельзя. Поле reason — машинная причина (например, link_settled, link_closed, sandbox_limit_reached). Приходит на POST /static-qr/{id}/simulate. Повтор имеет смысл только после того, как причина устранена. |
| `refund_not_uncertain` (#refund_not_uncertain) | 409 | Исход этого возврата уже известен — разбирать нечего. Приходит на POST /invoices/{invoice}/refunds/{refund}/simulate-resolve. Не повторяйте — перечитайте счёт. |
| `refund_not_held` (#refund_not_held) | 409 | У этого возврата нет удержания. Приходит на POST /invoices/{invoice}/refunds/{refund}/simulate-release-hold. Повтор по уже снятому удержанию — не ошибка: ответ 200 с тем же возвратом. |
| `refund_hold_link_active` (#refund_hold_link_active) | 409 | По этому возврату открыта ссылка покупателю, поэтому удержание снять нельзя. Приходит на POST /invoices/{invoice}/refunds/{refund}/simulate-release-hold. Повторите, когда ссылка завершится, истечёт или будет отозвана. |
| `clear_cooldown` (#clear_cooldown) | 429 | Песочницу уже очищали недавно. Приходит на POST /sandbox/clear; поле retry_after — через сколько секунд можно повторить. |
| `operation_in_progress` (#operation_in_progress) | 409 | Очистка песочницы уже идёт. Приходит на POST /sandbox/clear. Повторите через несколько секунд. |

## Журнал доставок: повтор и тест вебхука

| Код | HTTP | Что это и что делать |
|---|---|---|
| `webhook_log_not_retryable` (#webhook_log_not_retryable) | 422 | Эта доставка не повторяется: строка не failed (в том числе skipped — событие не отправлялось из-за паузы), у ключа нет действующего одобренного адреса, ключ отключён, событие не повторяется или строка доставлена через apipay-cli listen (`via: cli_listen`) — повторите событие в песочнице заново, а обработку повторной доставки проверьте командой apipay-cli trigger --duplicate. Приходит на POST /webhook-logs/{id}/retry (только песочница). Тот же запрос получит тот же ответ: строку skipped и успешную доставку повторить нельзя. Если причина в адресе или ключе — сначала почините их, затем повторите failed-строку. |
| `webhook_circuit_open` (#webhook_circuit_open) | 409 | Отправка вебхуков этого ключа на паузе — повтор её не обходит. Приходит на POST /webhook-logs/{id}/retry (только песочница). Повторите, когда GET /webhook-logs/delivery-state покажет state: open. |
| `webhook_already_delivered` (#webhook_already_delivered) | 409 | Это событие ключ уже принял — повторять нечего. Приходит на POST /webhook-logs/{id}/retry (только песочница) для события чека (receipt.*) и события подписки по счёту (subscription.* с invoice_id), которое уже доставлено, в том числе автоматическим повтором: второй экземпляр означал бы вторую печать чека или второе зачисление оплаты. Тот же запрос получит тот же ответ. Остальные события, в том числе события подписки без invoice_id, повторяются как обычно. |
| `sandbox_effect_in_progress` (#sandbox_effect_in_progress) | 409 | Организация сейчас меняет режим, повтор доставки не отправлялся. Приходит на POST /webhook-logs/{id}/retry вместе с retry_after_seconds — повторите через указанное время. |
| `webhook_pending_review` (#webhook_pending_review) | 409 | Адрес вебхука на проверке. Приходит на POST /webhook-test. Тестовый вебхук не отправлен — повторите запрос после одобрения адреса. |

## Привязка устройства

| Код | HTTP | Что это и что делать |
|---|---|---|
| `pairing_not_found` (#pairing_not_found) | 404 | Заявки с таким `device_code` нет: её не создавали, срок вышел и её убрали, или код искажён. Приходит на POST /device-pairings/token. Создайте новую заявку (apipay-cli pair). |
| `slow_down` (#slow_down) | 429 | Заявку опрашивают чаще, чем interval из ответа на её создание. Приходит на POST /device-pairings/token вместе с retry_after_seconds. Подождите указанное время и повторите: заявка при этом действует. |
| `pairing_kind_mismatch` (#pairing_kind_mismatch) | 409 | Кабинет: заявку на перевод в рабочий режим (`kind: production`) открыли на экране выпуска ключа. Ключ не выпускается — откройте ссылку из помощника: перевод подтверждается на отдельном экране. |
| `setup_mismatch` (#setup_mismatch) | 422 | Кабинет: на экране перевода в рабочий режим выбрана не та интеграция, что в заявке помощника (`setup_id` заявки `kind: production`). Перевод не выполнен — откройте ссылку из помощника заново. |

## Песочница без публичного адреса (listen)

| Код | HTTP | Что это и что делать |
|---|---|---|
| `listen_sandbox_only` (#listen_sandbox_only) | 403 | Сессия приёма уведомлений открывается только для организации в песочнице. Приходит на POST /webhook-listen/sessions. В рабочем режиме уведомления идут на публичный адрес вебхука. |
| `listen_not_available` (#listen_not_available) | 403 | Для этого ключа сессия приёма уведомлений недоступна: ключом управляет подключённая интеграция. Приходит на POST /webhook-listen/sessions. Используйте ключ своего приложения. |
| `listen_session_not_found` (#listen_session_not_found) | 404 | У этого ключа нет сессии с таким id. Приходит на запросы к /webhook-listen/sessions/{session}. Откройте новую сессию. |
| `listen_session_closed` (#listen_session_closed) | 409 | Сессия закрыта, истекла или организация вышла из песочницы. Приходит на GET /webhook-listen/sessions/{session}/events. Откройте новую сессию; в рабочем режиме уведомления приходят только на публичный адрес вебхука. |
| `listen_delivery_closed` (#listen_delivery_closed) | 409 | Событие не найдено или срок ответа по нему вышел. Приходит на POST /webhook-listen/sessions/{session}/events/{delivery}/result. Повторно сообщать результат не нужно: доставку повторит обычная сетка повторов. |

---

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