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

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

**TL;DR.** Возврат делается в два клика из кабинета (раздел «Счета» → кнопка возврата) или запросом `POST /api/v1/invoices/{id}/refund`. Возвращать можно только оплаченные счета, окно возврата у Kaspi — около 14 дней. Возврат асинхронный: система делает до 3 попыток и присылает вебхук `invoice.refunded` с итогом. Самая частая причина отказа — **на Kaspi-счёте не хватает денег**: Kaspi удерживает свою комиссию (~0,95%) сразу при оплате, поэтому вернуть полную сумму без пополнения счёта нельзя. И важная деталь: статуса `refunded` у счёта не существует — полностью возвращённый счёт остаётся `paid` с флагом `is_fully_refunded: true`.

## Коротко

| Вопрос | Ответ |
|---|---|
| Какие счета можно вернуть | Только оплаченные (`paid` / `partially_refunded`) и не возвращённые полностью |
| Окно возврата | ~14 дней с оплаты; точный срок определяет Kaspi |
| Сколько попыток делает система | До 3, затем вебхук `completed` или `failed` |
| Частичный возврат | Да: по сумме (`amount`) или по позициям (`return_items[]`: штуки `count` либо сумма `amount`) |
| Главная причина отказа | Недостаточно средств на Kaspi-счёте (комиссия ~0,95% уже удержана) |
| Статус счёта после полного возврата | Остаётся `paid` + `is_fully_refunded: true`; статуса `refunded` не существует |
| Как узнать итог | Вебхук `invoice.refunded` (приходит и на `completed`, и на `failed`) |

## Как сделать возврат: кабинет и API

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

**Через API:**

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

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

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

Если счёт вернуть нельзя, API откажет сразу синхронно: `400 Invoice is not refundable` — возвращаются только оплаченные и не полностью возвращённые счета. Список возвратов: `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` (сумма). В вебхуке частичного возврата по сумме `refund.items[].count` может быть `0` — это нормально, значит возврат был по сумме, а не по штукам.

Практический совет для счетов с корзиной: если вы пробили весь заказ одной позицией («Услуги», 10 штук), Kaspi сможет вернуть только кратно цене штуки — для произвольной суммы внутри позиции используйте `amount` у этой позиции.

Сколько ещё можно вернуть по счёту, видно в `GET /invoices/{id}`: поля `total_refunded`, `is_fully_refunded` и `available_for_refund` (число).

## «Возврат создан, но не проходит» — что происходит?

Классическая картина: нажали возврат, статус «создан», деньги покупателю не пришли, статус счёта не изменился. В 9 случаях из 10 причина — **на Kaspi-счёте недостаточно средств**. Откуда нехватка, если оплата только что пришла?

Считаем на пальцах. Покупатель заплатил 2 000 ₸. Kaspi сразу удержал свою комиссию ~0,95% — на счёт зачислилось примерно 1 981 ₸. Вы делаете возврат на полные 2 000 ₸ — а их на счёте нет, если он был пустой. Kaspi отвечает: «Недостаточно денег на счёте. Пополните счёт или сделайте возврат наличными».

Что делает система в этот момент: автоматически повторяет возврат до 3 раз, после чего фиксирует `failed` и присылает вебхук. Дальше система **сама не повторяет** — и это правильно: без пополнения счёта четвёртая попытка кончится так же.

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

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

Комиссия 0,95% — это условия Kaspi, ApiPay на них не влияет; подробнее о том, кто и за что платит, — «Тарифы и комиссия ApiPay».

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

Потому что такого статуса **нет** — и это не баг, а модель данных. Счёт и возврат — разные объекты:

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

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

## Сроки: окно возврата и зачисление покупателю

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

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

## Частые ошибки

- **Возвращать полную сумму с пустого Kaspi-счёта** — упрётся в нехватку средств из-за комиссии ~0,95%. Пополните счёт и повторите.
- **Ждать статус счёта `refunded`** — его не существует. Смотрите `is_fully_refunded` и событие `invoice.refunded`.
- **Повторять возврат сразу после `failed` без пополнения** — результат не изменится: причина не в ApiPay, а в балансе.
- **Пытаться вернуть неоплаченный счёт** — API откажет (`400 Invoice is not refundable`); неоплаченный счёт нужно отменять, а не возвращать.
- **Не дедуплицировать `invoice.refunded`** — событие может прийти повторно; ключ дедупликации: `(refund.id, refund.status)`.
- **Тянуть с возвратом «до выходных»** — окно ~14 дней; чем ближе к краю, тем выше риск `refund_window_expired`.

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

**Как сделать возврат без программиста?**
В кабинете apipay.kz: раздел «Счета» → кнопка возврата справа у оплаченного счёта → сумма → подтвердить. Итог придёт вебхуком и будет виден в кабинете.

**Можно ли вернуть больше, чем осталось по счёту?**
Нет. Доступный остаток показывает поле `available_for_refund`; попытка вернуть больше отклоняется на валидации.

**Возврат прошёл частично — что со счётом?**
Счёт переходит в `partially_refunded`, в `GET /invoices/{id}` растёт `total_refunded`. Можно делать следующие частичные возвраты, пока не исчерпан остаток.

**Почему я не вижу причину отказа в вебхуке?**
`refund.error_message` в вебхук не включается намеренно. Причина отказа видна в кабинете и в `GET /invoices/{id}/refunds`; самая частая — недостаточно средств на счёте.

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

**Кассир сделал возврат в приложении Kaspi — ApiPay узнает?**
Да: такие возвраты импортируются автоматически и приходят тем же вебхуком `invoice.refunded`.

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

---

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