Возврат — самая нелюбимая часть платёжной интеграции. Деньги идут не туда, куда привыкли, и любая ошибка в логике превращается в недовольного клиента и спор с банком. Эта статья — про то, как делать возвраты в Kaspi Pay API спокойно и без сюрпризов.
Что Kaspi умеет, а что нет
Базовые правила, заложенные на уровне самого Kaspi:
- Возврат возможен до 90 дней с момента платежа. После — только через ручное оформление в Kaspi Business.
- Только на ту же карту, с которой пришёл платёж. Никаких выводов на чужие реквизиты.
- Можно частично. Возврат меньше суммы платежа — нормальный кейс. Например, при возврате одного товара из заказа.
- Можно несколько раз, пока сумма всех возвратов не превысит исходную.
- Деньги возвращаются за минуты, обычно 1–3 минуты. Иногда — до 24 часов.
API-вызов: одна строка
import requests
resp = requests.post(
f"https://api.paybot.kz/me/payments/{operation_id}/refund",
headers={"Authorization": f"Bearer {jwt_token}"},
json={
"amount": 5000, # сумма в тенге; не больше остатка
"reason": "Возврат товара по запросу клиента",
},
).json()
# resp:
# {
# "refund_id": 142,
# "kaspi_refund_id": "ref_8a3f...",
# "amount": 5000,
# "status": "pending"
# }
Статус возврата проходит цепочку: pending → processing → completed (или failed при отказе Kaspi). Финальный статус приходит webhook'ом payment.refunded.
Когда возврат провалится
Самые частые причины — стоит обрабатывать явно:
| Код | Причина | Что делать |
|-----|---------|------------|
| refund_window_expired | Прошло > 90 дней | Только ручной возврат в Kaspi |
| refund_amount_exceeds | Сумма больше остатка | Уменьшить amount |
| payment_not_completed | Платёж ещё pending | Дождаться paid |
| kaspi_unavailable | Kaspi не отвечает | Retry через минуту |
| refund_in_progress | Параллельный возврат | Дождаться завершения первого |
try:
refund = api.refund(op_id, amount=5000, reason="…")
except ApiError as e:
if e.code == "refund_window_expired":
notify_finance("Ручной возврат: 90 дней истекли")
elif e.code == "kaspi_unavailable":
retry_in(60)
else:
log.error("Refund failed: %s", e.message)
Частичные возвраты: тонкости
Допустим, клиент купил три товара по 1 000 ₸ (всего 3 000 ₸), и вернул один.
Простой вариант — один возврат на 1 000:
api.refund(op_id, amount=1000, reason="Возврат: товар X")
После него:
paid_amount: 3 000 ₸refunded_amount: 1 000 ₸refundable_balance: 2 000 ₸statusплатежа:paid(не меняется, пока не возвращена вся сумма)
Если потом клиент вернёт ещё один товар — делаете второй возврат на 1 000. И т.д.
Можно ли сразу возвратить два товара одним вызовом на 2 000? Да. Но если хотите видеть в отчётах каждую возвращённую позицию отдельно — лучше делать по одному запросу. У каждого возврата свой refund_id и свой comment в учёте.
Идемпотентность возвратов
Возвраты тоже принимают Idempotency-Key. Это критично — если ваш повторный POST из-за сетевого сбоя пройдёт без ключа, деньги ушли клиенту дважды.
api.refund(
op_id,
amount=5000,
reason="...",
idempotency_key=f"refund:{order_id}:{returned_item_id}",
)
Простое правило для ключа: что-то, что естественно уникально на стороне вашего бизнеса. Например, refund:order_42:item_7. Pay Bot хранит идемпотентность 24 часа — ровно столько, сколько разумно для одной операции.
Webhook payment.refunded
Когда Kaspi подтвердил возврат, прилетает webhook:
{
"id": "evt_...",
"event": "payment.refunded",
"data": {
"operation_id": "op_...",
"refund_id": 142,
"amount": 5000,
"total_refunded": 5000,
"refundable_balance": 0,
"kaspi_refund_id": "ref_...",
"refunded_at": "2026-04-10T11:23:45Z"
}
}
В обработчике стоит:
- Обновить заказ — пометить позицию как возвращённую.
- Записать в учёт: дата, сумма, причина, исходный платёж.
- Уведомить клиента — «деньги придут в Kaspi в течение нескольких минут».
Если возврат частичный, поле refundable_balance подскажет, сколько ещё можно вернуть.
Бухгалтерский аспект
Несколько практических замечаний для бухгалтерии:
- В отчёте «Платежи» Pay Bot возвраты показываются как отдельные строки с типом
refund. Сумма исходного платежа не меняется. - В выгрузке Kaspi Business возврат пройдёт как операция со знаком «−».
- Дата для учёта —
refunded_at, неcreated_at. У возврата, инициированного сегодня, дата исполнения может быть завтрашней при ночной обработке. - НДС, если он есть в вашей категории, считается от чистой суммы:
платёж − возвраты.
CSV-экспорт и сверка
В разделе «Платежи» в кабинете есть кнопка «Экспорт CSV». Файл содержит колонки operation_id, amount, status, refunded_amount, created_at, paid_at. Удобно для месячной сверки с вашей бухгалтерской системой.
curl https://api.paybot.kz/me/payments/export \
-H "Authorization: Bearer $TOKEN" \
-o payments.csv
Если нужен фильтр по дате — добавьте ?date_from=2026-04-01&date_to=2026-04-30.
Подводные камни
Три кейса, на которых обычно спотыкаются впервые:
Возврат запросили, но статус «failed». В 90% случаев — Kaspi временно недоступен. Повторите через 30–60 секунд, обычно проходит. Если 3 раза подряд — пишите нам, проверим со стороны Kaspi.
Возврат сделали, но webhook не пришёл.
Webhook на payment.refunded нужно подключить отдельно — он не идёт автоматически с payment.completed. Проверьте подписку в кабинете.
Возвращаем 0.01 ₸ для проверки. Не работает. Минимальная сумма возврата — 1 ₸ (это уже на уровне Kaspi). Для теста используйте Sandbox.
Когда стоит делать «отмену», а не «возврат»
Если платёж ещё pending (клиент получил QR, но не оплатил), не надо его возвращать — отмените:
api.cancel(op_id, reason="Передумали")
Отмена pending-платежа бесплатна и мгновенна. Возврат имеет смысл только для paid-операций.
Чек-лист продакшна
Перед запуском возвратов в live стоит проверить:
- [ ] Webhook
payment.refundedподключён. - [ ] Обработчик идемпотентен (дедуп по
event.id). - [ ] Каждый возврат идёт с
Idempotency-Key. - [ ] Ошибка
refund_window_expiredуведомляет финансистов. - [ ] Возвраты экспортируются в учётную систему.
- [ ] Клиент получает уведомление с ожидаемым сроком зачисления.
Если хотя бы один пункт пропущен — рано или поздно сюрприз.
Итого
Возвраты в Kaspi Pay устроены проще, чем в классическом эквайринге: одна сумма, один статус, один webhook. Главное — обработать редкие ошибки (особенно «90 дней истекли»), сделать запросы идемпотентными и правильно отразить в учёте.
Если нужен лайв-разбор вашей конкретной интеграции — оставьте заявку или напишите в @paybotkz_bot.
Готовы попробовать Pay Bot?
Зарегистрируйтесь в кабинете — Sandbox доступен сразу. Или оставьте заявку, и менеджер настроит подключение Kaspi за вас.