# Приём оплаты в мобильном приложении

> Правильный порядок: приложение → ваш сервер → Qut Pay → Kaspi. Почему API-ключ не должен попадать в APK и IPA, что происходит при открытии payUrl и правила App Store и Play Market.

## Коротко

Мобильное приложение **не обращается в Qut Pay напрямую**. Порядок всегда из четырёх звеньев: приложение → ваш сервер → Qut Pay → Kaspi. Причина одна: API-ключ должен жить только на сервере, а не внутри APK или IPA — собранное приложение может распаковать кто угодно и достать ключ. Сервер создаёт счёт и возвращает приложению `payUrl` или `deepLink`, приложение его открывает, а после подтверждения оплаты на сервер приходит вебхук.

## Как это работает

| Шаг | Кто | Что делает |
|---|---|---|
| 1 | Приложение | Пользователь жмёт «Оплатить», приложение идёт на ваш сервер |
| 2 | Ваш сервер | Проверяет заказ и **сам считает сумму** |
| 3 | Ваш сервер | `POST /api/v1/invoices` — `X-API-Key` используется только здесь |
| 4 | Ваш сервер | Отдаёт приложению только `id` и `payUrl` (или `deepLink`) |
| 5 | Приложение | Открывает ссылку — запускается приложение Kaspi |
| 6 | Покупатель | Подтверждает оплату в Kaspi |
| 7 | Qut Pay | Отправляет на ваш сервер `invoice.paid` |
| 8 | Ваш сервер | Закрывает заказ и сообщает приложению пушем или статусом |

**Не берите сумму из приложения.** Запрос легко подменить и прислать 100 тенге. Сумму сервер должен посчитать сам, по содержимому заказа.

## Про API-ключ

Это самая важная часть статьи.

- **Ключ никогда не лежит внутри приложения.** APK и IPA — это обычные архивы, их можно распаковать. Не храните ключ ни в коде, ни в ресурсах, ни в `strings.xml`, ни в `Info.plist`, ни в обфусцированном виде.
- **Ключ лежит в переменных окружения вашего сервера.** В репозиторий он не коммитится.
- **Приложение ходит на ваш сервер со своей авторизацией** (сессия пользователя, JWT — что у вас принято). Ключ Qut Pay там не фигурирует вовсе.
- **Если ключ утёк**, сразу удалите его в кабинете и создайте новый: старый перестаёт работать в тот же момент.
- Выдавайте ключу только нужные права: `invoices:write`, `invoices:read`, и `refunds:write`, если делаете возвраты.

Об устройстве сервиса в целом: [Безопасно ли подключать и как всё устроено](/kb/ru/is-it-safe).

## Какой метод API используется

На сервере:

```
POST https://api.qut.kz/api/v1/invoices
X-API-Key: qp_live_…
Idempotency-Key: app-order-8841

{
  "amount": 12900,
  "kind": "qr",
  "description": "Заказ №8841",
  "externalOrderId": "8841",
  "successUrl": "https://moe-app.kz/pay/ok?order=8841",
  "failUrl": "https://moe-app.kz/pay/fail?order=8841",
  "metadata": { "platform": "ios", "user_id": "u_512" }
}
```

Не отдавайте приложению весь ответ — достаточно `id` и `payUrl` (или `deepLink`).

Остальное: `GET /api/v1/invoices/{id}` — узнать статус, когда пользователь вернулся в приложение, `POST /api/v1/invoices/{id}/cancel`, `POST /api/v1/invoices/{id}/refund`.

Если номер покупателя известен (он зарегистрирован в приложении), подойдёт и `kind: "phone"`: в Kaspi прилетит пуш. Тогда `customer.phone` в формате `7XXXXXXXXXX`, сумма — целые тенге, описание — до 60 символов.

## Что происходит при открытии payUrl

Когда приложение открывает `payUrl`, на телефоне запускается приложение Kaspi, и покупатель подтверждает оплату там. Вам нужны две вещи.

**1. Проверяйте статус при возвращении.** Поймайте момент возврата в приложение (`applicationDidBecomeActive`, `onResume`) и спросите у своего сервера статус заказа. Пользователь мог вернуться, ничего не оплатив, — «вернулся» само по себе не значит «оплатил».

**2. Источник истины — вебхук.** Переводите заказ в «оплачен» только на сервере и только по событию `invoice.paid`. Словам приложения верить нельзя: его поведение можно подменить.

Лучше держать оба механизма: вебхук — как основной, запрос из приложения — чтобы быстро обновить интерфейс.

`successUrl` и `failUrl` принимают только http(s). Удобно вести их на страницы вашего домена, а уже оттуда возвращать пользователя в приложение.

## Правила App Store и Play Market

Это не техническое, а магазинное ограничение, но от него зависит, пройдёте ли вы модерацию.

| Что продаётся | Внешняя оплата |
|---|---|
| Цифровой товар: подписка, премиум-доступ, игровая валюта, контент внутри приложения | Нельзя — нужно использовать оплату самого магазина |
| Физический товар: одежда, еда, книги, покупка в магазине | Можно |
| Доставка, курьер, такси | Можно |
| Услуги: ремонт, консультация, обучение, салон | Можно |
| Бронирование: столик, номер, билет, время | Можно |

То есть Qut Pay уместен для **реальных товаров и услуг**, но не для цифрового контента внутри приложения. Правила магазинов меняются — перед публикацией сверьтесь с их актуальной редакцией.

## На что обратить внимание

- **Идемпотентность.** Мобильная сеть нестабильна, повторная отправка запроса — обычное дело. Передавайте номер заказа в `Idempotency-Key`: при повторе с тем же ключом новый счёт не создаётся, возвращается прежний.
- **Окно сканирования QR — около трёх минут.** Показывайте таймер и кнопку «Новый счёт». Не зашивайте время в код — берите `expiresAt` из ответа.
- **Поздняя оплата.** Если деньги придут на истёкший счёт, событие придёт с признаком `late: true`. Не закрывайте такой заказ автоматически.
- **Обработчик вебхука должен быть идемпотентным** — пара `(invoice.id, status)` обрабатывается один раз.
- **Сначала песочница.** Прогоните всю цепочку ключом `qp_test_…`: счёт → `simulate` → вебхук → статус в приложении обновился.
- **Если получаете 401**, чаще всего дело в несовпадении режима или неверном заголовке: [API отвечает 401](/kb/ru/api-401).

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

**У меня нет сервера, только приложение. Как быть?** Понадобится небольшой сервер: вся его работа — создавать счета и принимать вебхуки, это буквально пара эндпоинтов. Без сервера ключ неизбежно окажется внутри приложения, а так делать нельзя.

**А если обфусцировать ключ?** Не помогает. Обфускация не прячет ключ, а лишь усложняет поиск. Достаточно посмотреть трафик собранного приложения.

**Нужно ли показывать картинку QR в приложении?** Сканировать QR на том же телефоне неудобно. Лучше открывать Kaspi через `payUrl` или `deepLink`. QR пригодится для случая, когда платят с другого телефона.

**Что если покупатель оплатил, а приложение закрылось?** Ничего не теряется. Оплата проходит в Kaspi, вебхук приходит на сервер, заказ закрывается. При следующем запуске приложение увидит готовый статус.

**У меня есть ещё веб-версия — можно один ключ на оба?** Можно, но удобнее разные: если один утечёт, вы удалите только его, не трогая второй.
