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

Обновлено 6 июля 2026 · Справочник · Версия в Markdown
Содержание
  1. Как сделать возврат: кабинет и API
  2. Частичный возврат: по сумме или по штукам?
  3. «Возврат создан, но не проходит» — что происходит?
  4. Почему статус счёта не меняется на «refunded»?
  5. Сроки: окно возврата и зачисление покупателю
  6. Частые ошибки
  7. Вопросы и ответы

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

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

Через API:

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».

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

  • Окно возврата — около 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.

Интеграция с ИИ

Не настраивайте вручную — поручите ИИ

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он подключит приём платежей примерно за 15 минут. Настраивает ИИ, а не вы.

Остались вопросы — напишите нам в WhatsApp: +7 708 516 74 89. Отвечаем быстро, без звонков.

Написать в WhatsApp

Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/vozvraty-kaspi-cherez-api.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.