> Источник: https://apipay.kz/guides/vozvraty-kaspi-cherez-api · Обновлено: 2026-10-24 · apipay.kz
> ApiPay — независимый сервис приёма платежей поверх вашего Kaspi Pay.

# Как сделать возврат Kaspi через API и почему он не проходит?

Если клиент платил вашему ТОО или ИП через Kaspi Pay, возврат делают двумя способами. Вручную — в приложении Kaspi Pay под аккаунтом владельца. Автоматически — через ApiPay: кнопкой возврата у оплаченного счёта в кабинете или запросом `POST /api/v1/invoices/{id}/refund`, полностью или частично, а итог приходит вебхуком `invoice.refunded`.

## Вы покупатель?

Эта статья — для продавцов. С возвратом покупки обращайтесь к продавцу, у которого покупали; по покупкам в Kaspi Магазине, в рассрочку и возвратам после экспертизы — в поддержку Kaspi ([справка Kaspi](https://guide.kaspi.kz/)). ApiPay — инструмент для бизнеса, который принимает оплату через Kaspi Pay. Если вы продавец — читайте дальше.

## Главное о возвратах Kaspi

Возвращать можно только оплаченные счета. Окно возврата задаёт Kaspi — ориентир около 14 дней с оплаты. Возврат асинхронный: заявка принимается сразу, итог приносит вебхук `invoice.refunded`. Kaspi удерживает свою комиссию при оплате, поэтому частая причина отказа — на Kaspi-счёте не хватает денег на полную сумму возврата. Если Kaspi требует подтверждения покупателя, отправьте ему ссылку-возврат (`POST /api/v1/qr-refunds/links`). Статуса `refunded` у счёта нет: полностью возвращённый счёт остаётся `paid` с флагом `is_fully_refunded: true`.

## Коротко

| Вопрос | Ответ |
|---|---|
| Какие счета можно вернуть | Только оплаченные (`paid` / `partially_refunded`) и не возвращённые полностью |
| Вручную или через ApiPay | Вручную — в приложении Kaspi Pay под аккаунтом владельца. Через ApiPay — кнопка в кабинете или `POST /invoices/{id}/refund` |
| Кассир отключён | Через ApiPay возврат невозможен — только вручную в приложении Kaspi Pay |
| Кассир сменён на другой номер | Возврат по оплате прежнего кассира может не пройти: обычный придёт `failed`, по ссылке — `qr_refund_operation_not_found`. Тогда вручную в приложении Kaspi Pay |
| Частичный возврат | Да: по сумме (`amount`) или по позициям (`return_items[]`: штуки `count` либо сумма `amount`). По корзинному счёту — только через `return_items[]` |
| Как узнать итог | Вебхук `invoice.refunded` (приходит и на `completed`, и на `failed`) |
| Kaspi требует подтверждения покупателя | Ссылка-возврат: `POST /qr-refunds/links`, покупатель подтверждает возврат в своём Kaspi |

## Как сделать возврат клиенту, который платил через Kaspi Pay?

**Из кабинета (без кода):** apipay.kz → раздел «Счета» → найдите оплаченный счёт → кнопка возврата справа → укажите сумму (по умолчанию — вся доступная) → подтвердите. Дальше всё автоматически.

**Через API:**

```bash
# Полный возврат (вся доступная сумма)
curl -X POST "https://api.apipay.kz/api/v1/invoices/{id}/refund" \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Idempotency-Key: refund-order-123-1" \
  -H "Content-Type: application/json"

# Частичный возврат по сумме
curl -X POST "https://api.apipay.kz/api/v1/invoices/{id}/refund" \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Idempotency-Key: refund-order-124-1" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1500}'
```

**Повтор после таймаута — только с тем же `Idempotency-Key`.** Ключ (до 191 символа) задаёте вы, один на каждую просьбу о возврате, и сохраняете рядом с заказом до отправки запроса. Если ответ потерялся, повторите запрос с тем же ключом и тем же телом: придёт `200` с `idempotent_replay: true` и тот же возврат в текущем статусе, второго возврата не будет. Без ключа повтор не распознаётся: частичный возврат после завершения первого пройдёт второй раз. Тот же ключ с другим телом — `409 idempotency_key_conflict`.

Ответ — `201` со статусом возврата `pending`. Это **не** «деньги вернулись», это «заявка принята»: дальше система асинхронно проводит возврат в Kaspi и присылает вебхук `invoice.refunded` с итоговым статусом `completed` или `failed`. Жизненный цикл возврата: `pending → processing → completed | failed`. Полный справочник запросов — в [документации API](/docs).

Синхронные отказы при создании возврата:

| Код | Когда | Что делать |
|---|---|---|
| `400` | Счёт не оплачен или уже возвращён полностью | Неоплаченный счёт отменяют через `POST /api/v1/invoices/{id}/cancel`, а не возвращают |
| `400` | Сумма больше `available_for_refund` | Прочитать остаток из `GET /invoices/{id}/refunds` и повторить с ним |
| `409 qr_refund_link_invoice_busy` | По счёту уже идёт обычный или QR-возврат либо его исход неизвестен | Дождаться итога прежнего возврата, второй не создавать |
| `422 partial_refund_requires_return_items` | Частичный возврат корзинного счёта отправлен плоским `amount` | Передать `return_items[]` |
| `403 tariff_inactive` | Не оплачена подписка ApiPay | Оплатить тариф — без него возвраты закрыты |

Истёкший срок возврата синхронным отказом не приходит: возврат создаётся, а затем приходит вебхук `invoice.refunded` со `status: failed` и `error_code: refund_window_expired` (тот же итог виден в `GET /invoices/{id}/refunds`). Повторять не нужно — возвращайте вне ApiPay по своим правилам торговли.

Список возвратов: `GET /api/v1/invoices/{id}/refunds` и общий `GET /api/v1/refunds`.

## Частичный возврат: по сумме или по штукам?

Три способа, выбирайте один:

- **Просто сумма** — `{"amount": 1500}`: вернуть часть денег без привязки к товарам. Подходит счетам без корзины.
- **По позициям штуками** — `return_items: [{"catalog_item_id": 11, "count": 2}]`: вернуть 2 штуки конкретного товара из корзины.
- **По позиции суммой** — `return_items: [{"catalog_item_id": 15, "amount": 900}]`: частичный возврат неделимой позиции (например, вернуть 900 ₸ из услуги за 5 000 ₸).

Правило API: в каждой позиции `return_items[]` указывается **ровно одно** из `count` (целые штуки) или `amount` (сумма), иначе `422`.

⚠️ **Частичную сумму по корзинному счёту передавайте только через `return_items[]`.** Тот же `error_code` приходит в вебхуке `invoice.refunded` со `status: failed`, если синхронная проверка не сработала.

В вебхуке частичного возврата по сумме `refund.items[].count` может быть `0` — возврат был по сумме, а не по штукам.

Сколько ещё можно вернуть по счёту, видно в `GET /invoices/{id}/refunds` (блок `invoice`) и в ответе на сам возврат: поле `available_for_refund` (число). Оно учитывает и незавершённые возвраты, поэтому бывает меньше, чем `amount − total_refunded`. В `GET /invoices/{id}` этого поля нет — там только `total_refunded` и `is_fully_refunded`.

## Почему возврат Kaspi создан, но не проходит?

Симптом: возврат создан, итог `failed`, Kaspi отвечает «Недостаточно денег на счёте. Пополните счёт или сделайте возврат наличными». Возврат уходит на **полную** сумму покупки, а на счёт организации она пришла за вычетом комиссии Kaspi — с пустого счёта полной суммы не хватит.

Такой ответ Kaspi терминален: возврат сразу получает `failed`, уходит вебхук `invoice.refunded` с `refund.error_code`. Автоповторов на него нет, ждать бесполезно.

**Что делать вам:**

1. Пополните Kaspi-счёт организации (или дождитесь следующих оплат).
2. Создайте возврат заново — той же кнопкой или тем же запросом с **новым** `Idempotency-Key`: прежний ключ вернёт старый `failed`. После `failed` сумма освобождается.

⚠️ Пока предыдущий возврат висит в `pending`/`processing`, второй возврат по этому счёту не создаётся: API ответит `409 qr_refund_link_invoice_busy`. Дождитесь итога первого.

⛔ **`processing` с `refund.error_code: refund_execution_uncertain`** — исход денежного запроса не подтверждён, деньги могли уйти. Не повторяйте возврат ни через ApiPay, ни вручную в приложении Kaspi Pay и не выпускайте ссылку-возврат. Исход отмечает владелец или разработчик организации в карточке счёта в кабинете ApiPay, сверившись с историей операций в Kaspi Pay; если не уверены — напишите в поддержку. Пока исход не отмечен, вебхука нет, а в `GET /invoices/{id}/refunds` у возврата `operation_state: uncertain`. После разбора возврат получает `failed` с `refund_resolved_not_executed` (деньги не ушли, придёт `invoice.refunded`) или `refund_resolved_executed` (деньги ушли и учтены в возврате `resolved_refund_id`; вебхука по исходному возврату нет — статус читайте в `GET /invoices/{id}/refunds`). После `refund_resolved_executed` новый возврат не создавайте: деньги покупателю уже вернулись.

Другие причины в `refund.error_code`:

- `refund_rejected_by_kaspi` — Kaspi отклонил возврат по этой операции. Выпустите ссылку-возврат с `source_refund_id` этого возврата — покупатель подтвердит его сам. Если возврат создавался без `hold_for_buyer_confirmation`, его можно и повторить позже с новым `Idempotency-Key`. Вручную — в приложении Kaspi Pay; перед этим пройдите проверку «Перед ручным возвратом» ниже.
- `refund_requires_buyer_confirmation` — Kaspi ждёт подтверждения покупателя. Отправьте ему ссылку-возврат (раздел ниже).

**Перед ручным возвратом** в приложении Kaspi Pay проверьте весь счёт в `GET /invoices/{id}/refunds`: деньги могут уйти и по ссылке, выпущенной без `source_refund_id`, — своего `operation_state` у неё нет, она видна только в `qr_refund_sessions`.

1. Отзовите все живые ссылки по счёту: в кабинете — кнопка «Отозвать ссылку» в разделе «Возвраты», по API — `DELETE /qr-refunds/links/{id}`. Отзыв не прерывает уже открытое окно сканирования: если покупатель успеет подтвердить, деньги уйдут. Дождитесь, пока окно закроется: оно длится не больше 90 секунд, после него статус ссылки сменится (шаг 2).
2. В `qr_refund_sessions` у ссылок должны остаться только статусы `completed`, `expired` или `failed`. `completed` значит, что деньги по этой ссылке уже вернулись: вручную возвращайте только остаток, если он ещё нужен, — его показывает `available_for_refund` в блоке `invoice`. `execution_uncertain` значит, что деньги, возможно, уже ушли: вручную не возвращайте и обратитесь в поддержку ApiPay — этот статус сам не изменится.
3. У каждого возврата счёта `operation_state` — `completed`, `failed`, `awaiting_buyer` или `released`. У самого отклонённого возврата — только `failed`, `awaiting_buyer` или `released`: если у него `completed`, деньги уже вернулись, в том числе по ссылке, и вручную возвращать не нужно. При `processing`, `buyer_link_active` или `uncertain` — ждите итога.

Только после этого возвращайте вручную, иначе покупатель получит деньги второй раз. `409 qr_refund_link_not_revocable` значит, что ссылку уже нельзя отозвать: покупатель подтвердил возврат или ссылка завершилась. Решайте по тем же полям.

Диагностика по нехватке денег, сроку возврата и корзине — в статье «[Возврат Kaspi не проходит](/guides/vozvrat-ne-prohodit)».

**Отключаете или меняете кассира на другой номер?** После отключения возвраты по ранее оплаченным счетам через ApiPay недоступны — их придётся делать вручную в приложении Kaspi Pay. После смены на другой номер успех возврата по оплатам прежнего кассира не гарантирован: обычный возврат может прийти со статусом `failed`, возврат по ссылке — с ошибкой `qr_refund_operation_not_found`. Поэтому возвраты делайте до смены. Если по счетам последних двух недель возможны возвраты, меняйте или отключайте кассира после закрытия окна возврата ([Смена и отключение кассира](/guides/smena-kassira-nomera-vladeltsa)).

## Почему статус счёта не меняется на «refunded»?

Такого статуса нет: счёт и возврат — разные объекты. **Счёт** фиксирует факт продажи и остаётся `paid`; при частичном возврате переходит в `partially_refunded`, при полном получает флаг **`is_fully_refunded: true`**. **Возврат** — отдельная операция со своими статусами (`pending → processing → completed | failed`).

Для интеграции это значит: не ждите вебхук `invoice.status_changed` со статусом «refunded» — его не будет никогда. Итог возврата приносит событие **`invoice.refunded`** (и на успех, и на отказ). Дедуплицируйте его на своей стороне по паре `(refund.id, refund.status)`; `refund.error_message` в вебхуке отсутствует намеренно — детали смотрите в кабинете или через `GET /invoices/{id}/refunds`. Настройка вебхуков — «[Настройка вебхуков ApiPay](/guides/nastroyka-webhookov-apipay)».

## Какой срок возврата у Kaspi?

- **Окно возврата задаёт Kaspi** — ориентир около 14 дней с момента оплаты, точную границу держит Kaspi. После окна возврат откажет с кодом `refund_window_expired`. Если окно истекло, деньги покупателю возвращают вне ApiPay — переводом или наличными по вашим правилам торговли.
- **Зачисление покупателю** после успешного возврата происходит на стороне Kaspi.

Возвраты, сделанные кассиром вручную в приложении Kaspi Pay, ApiPay подтягивает автоматически: они появятся в кабинете и придут тем же вебхуком `invoice.refunded`. Двойного учёта не будет.

## Что делать, если Kaspi требует подтверждения покупателя — ссылка-возврат

Вторая механика возврата — с подтверждением покупателя. Вы выпускаете одноразовую ссылку и отправляете её покупателю. Он открывает её, нажимает кнопку и сканирует возвратный QR в том Kaspi, которым платил. Применяйте её, когда возврат по счёту упирается в отказ Kaspi или когда оплата шла мимо ваших счетов.

- **В кабинете:** у отказанного возврата — кнопка «Создать ссылку для клиента», либо раздел «Возврат с подтверждением» на странице возвратов.
- **В API:** `POST /api/v1/qr-refunds/links`. С `invoice_id` ссылка привязана к оплаченному счёту и сама проводит возврат, как только покупатель подтвердит себя; итог — вебхуки `qr_refund.*`.
- **Два срока.** Ссылка ждёт покупателя 24 часа (`link_expires_at`). Окно скана открывается только после его нажатия и длится не более 90 секунд (`expires_at`).
- **Покупателю нужен второй экран:** ссылку он открывает на компьютере или другом телефоне, а QR сканирует в приложении Kaspi.
- ⛔ Итог `qr_refund.execution_uncertain` означает, что деньги могли уйти. Не повторяйте возврат ни новой ссылкой, ни через `POST /invoices/{id}/refund`, ни вручную в приложении Kaspi Pay — обратитесь в поддержку.
- ⛔ `customer_url` приходит **ровно один раз**. Кто открыл ссылку, тот и подтверждает возврат: отправляйте её только этому покупателю. Потеряли адрес — отзовите ссылку (`DELETE /api/v1/qr-refunds/links/{id}`) и выпустите новую.

Немедленный старт `POST /qr-refunds` (QR у кассы) в API помечен как устаревший: строить новую интеграцию на нём не нужно. Флоу, коды ошибок и песочница — в статье «[Возврат по QR: покупатель подтверждает возврат в Kaspi](/guides/vozvrat-po-qr-cherez-api)».

## Вопросы и ответы

**Я покупатель и хочу вернуть деньги за покупку в Kaspi Магазине или после экспертизы — это сюда?**
Нет. Эта статья — для бизнеса, который принимает оплату через Kaspi Pay. С возвратом покупки обращайтесь к продавцу, у которого покупали; по покупкам в Kaspi Магазине, в рассрочку и после экспертизы — в поддержку Kaspi.

**Можно ли сделать частичный возврат Kaspi?**
Да: по сумме (`amount`) или по позициям корзины (`return_items[]`). По счёту с корзиной — только через `return_items[]`, иначе `422 partial_refund_requires_return_items`.

**Повторяет ли система возврат автоматически?**
Отказ Kaspi по существу фиксируется как `failed` сразу, автоповторов на него нет. Повторную попытку система делает только при временном сбое связи или сессии кассира. После `failed` устраните причину и создайте новый возврат с новым `Idempotency-Key`. Исключение — `failed` с `refund_resolved_executed`: деньги уже вернулись покупателю, новый возврат не нужен.

**Можно ли запретить возвраты совсем?**
Серверного «запрета возвратов» не существует: возврат — штатная функция Kaspi. Контролируйте это на своей стороне — просто не вызывайте возврат из своей интеграции; в кабинете операция доступна владельцу.

**Кто напишет интеграцию возвратов?**
Код по этой статье может написать ваш ИИ-агент — дайте ему [apipay.kz/for-ai](/for-ai) и [документацию API](/docs).

Смотрите также: [Возврат Kaspi не проходит](/guides/vozvrat-ne-prohodit) · [Как создать счёт Kaspi по номеру](/guides/kak-sozdat-schet-kaspi-po-nomeru) · [Настройка вебхуков ApiPay](/guides/nastroyka-webhookov-apipay) · [Тарифы и комиссия ApiPay](/guides/tarify-i-komissiya-apipay) · «[Как принимать оплату Kaspi через API](/kaspi-api)».

---

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