Блог
возвраты Kaspi Pay API refund учёт платежей

Возвраты в Kaspi Pay: как делать через API правильно

Как делать возвраты через Kaspi Pay API: частичные и полные возвраты, ограничение 90 дней, обработка ошибок, бухгалтерский учёт. С примерами кода.

Pay Bot 10 апреля 2026 г. 4 мин чтения

Возврат — самая нелюбимая часть платёжной интеграции. Деньги идут не туда, куда привыкли, и любая ошибка в логике превращается в недовольного клиента и спор с банком. Эта статья — про то, как делать возвраты в 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"
# }

Статус возврата проходит цепочку: pendingprocessingcompleted (или 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"
  }
}

В обработчике стоит:

  1. Обновить заказ — пометить позицию как возвращённую.
  2. Записать в учёт: дата, сумма, причина, исходный платёж.
  3. Уведомить клиента — «деньги придут в 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 за вас.