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

# Возврат по QR: покупатель подтверждает возврат в Kaspi

**TL;DR.** Kaspi изменил механику возврата: теперь деньги возвращаются только после того, как **покупатель подтвердит возврат со своего телефона** — отсканирует возвратный QR тем Kaspi, которым платил. Только после этого вы видите список его покупок и выбираете нужную. Обычный возврат `POST /invoices/{id}/refund` никуда не делся и работает как раньше — новая ветка нужна, когда он не проходит. В кабинете это раздел **«Возврат с подтверждением»** на странице возвратов, в API — `/qr-refunds`.

## Коротко

| Вопрос | Ответ |
|---|---|
| Когда нужен | Когда обычный возврат по счёту не проходит или оплата шла мимо ваших счетов |
| Кто подтверждает | Покупатель — сканирует возвратный QR в приложении Kaspi |
| Чем сканировать | Тем же Kaspi, которым оплачивал: с другого аккаунта покупка не найдётся |
| Сколько живёт ссылка | Минуты — точный срок приходит в `expires_at` |
| Второй срок | После подтверждения время на выбор покупки тоже ограничено |
| Частичный возврат | Да: суммой (`amount`) либо по товарам (`items`) — взаимоисключимо |
| Как узнать итог | Ответ `execute` приходит сразу; плюс вебхуки `qr_refund.*` |

## Как это выглядит для продавца

1. Продавец начинает возврат и получает **ссылку для покупателя**.
2. Отправляет её покупателю — в WhatsApp, Telegram или любым другим способом.
3. Покупатель открывает ссылку и подтверждает возврат в Kaspi.
4. Продавец видит список покупок этого покупателя, выбирает нужную и возвращает деньги — целиком или частично.

Схема:

```
Продавец              ApiPay                  Покупатель
   │  начать возврат    │                          │
   ├───────────────────►│  ссылка + QR             │
   │◄───────────────────┤                          │
   │  отправляет ссылку ─────────────────────────► │
   │                    │◄── подтверждает в Kaspi ─┤
   │  статус: покупатель опознан                   │
   │◄───────────────────┤                          │
   │  список покупок    │                          │
   │◄──────────────────►│                          │
   │  вернуть выбранную │                          │
   ├───────────────────►│ ─── деньги покупателю ──►│
```

## Флоу в API

Все запросы — с вашим ключом в заголовке `X-API-Key`.

### 1. Начать возврат

```bash
curl -X POST https://api.apipay.kz/api/v1/qr-refunds \
  -H "X-API-Key: ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{}'
```

В ответе — ссылка и картинка QR для покупателя, срок действия и рекомендованный интервал опроса:

```json
{
  "id": 42,
  "status": "awaiting_scan",
  "qr_token_url": "https://qr.kaspi.kz/return/…",
  "qr_image_url": "https://…/storage/qr/uuid.png",
  "expires_at": "2026-07-27T17:27:09+05:00",
  "poll_interval_seconds": 3,
  "scan_wait_timeout_seconds": 90
}
```

Если у организации несколько активных касс, укажите `kaspi_connection_id` — иначе придёт `422 connection_ambiguous`.

### 2. Дождаться подтверждения

```bash
curl https://api.apipay.kz/api/v1/qr-refunds/42 -H "X-API-Key: ВАШ_КЛЮЧ"
```

Опрашивайте с интервалом из `poll_interval_seconds`, пока статус не станет `customer_identified` (покупатель подтвердил) или `expired` (срок вышел, нужна новая сессия). Вместо опроса можно слушать вебхуки `qr_refund.identified`, `qr_refund.completed`, `qr_refund.expired`.

⚠️ **Не прекращайте опрос после `customer_identified`.** У сессии два срока: сам QR живёт минуты, и отдельно ограничено время на выбор покупки после подтверждения. Если продавец задумался, сессия истечёт — и запрос на возврат вернёт `409 qr_refund_expired` уже после того, как человек выбрал покупку и ввёл сумму.

### 3. Посмотреть покупки

```bash
curl https://api.apipay.kz/api/v1/qr-refunds/42/operations -H "X-API-Key: ВАШ_КЛЮЧ"
```

```json
{
  "client_name": "Иван И.",
  "operations": [
    { "ref": "…", "amount": 500, "date": "2026-07-25T16:12:52+05:00", "returnable": "full" },
    { "ref": "…", "amount": 10,  "date": "2026-07-25T16:16:54+05:00", "returnable": "partial" },
    { "ref": "…", "amount": 10,  "date": "2026-06-30T20:19:31+05:00", "returnable": "none" }
  ],
  "has_more": false,
  "next_cursor": null,
  "remaining_count": 0
}
```

`returnable` говорит, что Kaspi разрешает по этой покупке: `full` — только целиком, `partial` — можно частично, `none` — возврат недоступен. Если покупок больше, передавайте `next_cursor` в `?cursor=`.

⚠️ **`ref` непрозрачен и привязан к сессии.** Не разбирайте его на части, не показывайте покупателю и не сохраняйте между сессиями — просто возвращайте обратно как есть.

### 4. Детали покупки

```bash
curl https://api.apipay.kz/api/v1/qr-refunds/42/operations/REF -H "X-API-Key: ВАШ_КЛЮЧ"
```

Приходят доступная к возврату сумма, уже возвращённое, ссылка на чек и позиции корзины — у каждой свой `ref` и свой остаток.

### 5. Вернуть деньги

```bash
# целиком
curl -X POST https://api.apipay.kz/api/v1/qr-refunds/42/execute \
  -H "X-API-Key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
  -d '{"operation_ref":"REF"}'

# частично, суммой
  -d '{"operation_ref":"REF","amount":7}'

# частично, по товарам
  -d '{"operation_ref":"REF","items":[{"ref":"ITEM_REF","amount":5}]}'
```

`amount` и `items` **взаимоисключимы** — вместе их присылать нельзя. У позиции `amount` необязателен: без него вернётся вся её доступная сумма.

Запрос синхронный, итог приходит сразу:

```json
{
  "id": 42,
  "status": "completed",
  "refunded_amount": "500.00",
  "receipt_url": "https://receipt.kaspi.kz/…",
  "client_name": "Иван И.",
  "completed_at": "2026-07-27T17:20:11+05:00"
}
```

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

| Код | Что означает | Что делать |
|---|---|---|
| `qr_refund_not_identified` | Покупатель ещё не подтвердил возврат | Дождитесь `customer_identified` |
| `qr_refund_expired` | Срок сессии истёк | Начните новый возврат и отправьте свежую ссылку |
| `qr_refund_completed` | Возврат по этой сессии уже выполнен | Не повторяйте: запросите статус и покажите результат оттуда |
| `operation_not_returnable` | Kaspi не разрешает возврат по этой покупке | Выберите другую покупку |
| `refund_amount_exceeds_available` | Сумма больше доступной к возврату | Проверьте `available_for_refund` в деталях покупки |
| `partial_refund_requires_return_items` | Эту покупку Kaspi возвращает только по товарам | Пришлите `items` вместо `amount` |
| `connection_ambiguous` | У организации несколько активных касс | Укажите `kaspi_connection_id` при старте |

При ошибках `422` сессия остаётся живой — можно повторить с другой покупкой или суммой, не начиная заново.

## Как проверить без реального покупателя

В песочнице реальный QR никому не уходит, а шаги покупателя двигаются вручную:

```bash
# покупатель подтвердил
curl -X POST https://api.apipay.kz/api/v1/qr-refunds/42/simulate \
  -H "X-API-Key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
  -d '{"event":"identified"}'

# срок истёк
  -d '{"event":"expired"}'
```

После `identified` отдаются три покупки: 500 ₸ (возврат целиком), 10 ₸ (только по товарам) и одна невозвратная. Неуспех возврата форсируется полем `simulate` в теле `execute`:

```json
{ "operation_ref": "REF", "simulate": { "status": "failed", "error_code": "refund_amount_exceeds_available" } }
```

Вне песочницы поле `simulate` даёт `403 not_sandbox`.

## Частые вопросы

**Обычный возврат перестанет работать?**
Нет. `POST /invoices/{id}/refund` не менялся и работает как раньше. Возврат по QR — запасной путь для случаев, когда обычный не проходит.

**Покупатель может подтвердить с чужого телефона?**
Открыть ссылку — да, но сканировать нужно **тем Kaspi, которым он оплачивал**. С другого аккаунта его покупка просто не найдётся, и список придёт пустым.

**Сколько живёт ссылка?**
Минуты — точное значение в `expires_at`. Поэтому не создавайте сессию заранее: сначала убедитесь, что покупатель у телефона.

**Что будет, если продавец нажмёт «вернуть» дважды?**
Второй запрос вернёт `qr_refund_completed`. Это не ошибка возврата: деньги уже вернулись, просто запросите статус сессии и покажите результат оттуда.

**Можно вернуть несколько покупок за одну сессию?**
Нет. Одна сессия — один возврат. Для второй покупки начните новый возврат.

**Приходят ли вебхуки?**
Да: `qr_refund.identified` при подтверждении покупателем, `qr_refund.completed` при успешном возврате и `qr_refund.expired` при истечении срока.

---

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