# QR-счёт или счёт по телефону — что выбрать

> Полное сравнение двух типов счёта: значение kind, что делает покупатель, нужен ли номер, ограничения описания и суммы, срок жизни и таблица сценариев с рекомендацией по каждому.

## Коротко

При создании счёта у поля `kind` два значения:

- **`qr`** (по умолчанию) — создаётся QR-код и ссылка на оплату. Покупатель сканирует QR или открывает ссылку.
- **`phone`** — счёт приходит пушем прямо в приложение Kaspi покупателя. Поле `customer.phone` обязательно.

Самое короткое правило: **покупатель смотрит на ваш экран — берите `qr`; покупатель далеко, а номер известен — берите `phone`**.

## Полное сравнение

| | QR-счёт | Счёт по телефону |
|---|---|---|
| Значение `kind` | `qr` (по умолчанию) | `phone` |
| Что делает покупатель | Сканирует QR или открывает `payUrl` | Открывает уведомление в Kaspi и подтверждает |
| Нужен ли номер телефона | Нет | Да, `customer.phone` обязателен |
| Ограничение описания | 100 символов | 60 символов |
| Сумма | Максимум 2 знака после запятой | Только целые тенге |
| Срок | Окно сканирования QR ограничено, берите из `expiresAt` | Уведомление лежит в приложении, окно длиннее |
| Приложение Kaspi у покупателя | Не обязательно: ссылка открывается и в браузере | **Обязательно** |
| Поля ответа | `payUrl`, `qrUrl`, `qrImageUrl`, `deepLink` | `payUrl` тоже есть, но основной канал — пуш |
| Где удобно | Касса, витрина, вендинг, сайт, соцсети | После звонка, чат, подписка, массовая рассылка |

## Ограничение описания

Поле `description` видит покупатель, но предел у двух типов разный: **100 символов у QR и 60 у phone**.

Если в одном коде используются оба типа, безопаснее обрезать текст заранее:

```js
const LIMIT = { qr: 100, phone: 60 };

function invoiceBody(kind, amount, text, phone) {
  return {
    kind,
    amount,
    description: text.slice(0, LIMIT[kind]),
    ...(kind === 'phone' ? { customer: { phone } } : {}),
  };
}
```

Слишком длинный текст может привести к отказу при создании счёта, а в массовой рассылке это заметно не сразу — поэтому обрезайте в коде.

## Разница в сумме

| Тип | Допустимая сумма |
|---|---|
| `qr` | До двух знаков после запятой: `1250.50` пройдёт |
| `phone` | Только целые тенге: `1250` пройдёт, `1250.50` — нет |

На тиыны в счёте по телефону придёт ошибка `amount_must_be_whole_tenge`. Поэтому, если в вашей системе расчёта есть копейки, **заранее решите правило округления** для `phone` — вверх или вниз. Обычно округляют в одну сторону последовательно, чтобы разницу можно было объяснить покупателю одинаково в любом случае.

У QR два знака допустимы, но на практике круглая сумма всё равно понятнее покупателю.

## Срок: про окно QR

Окно сканирования QR **ограничено, и задаёт его Kaspi**. Не записывайте его длительность в код константой — берите из поля **`expiresAt`** в ответе.

```js
const inv = await createInvoice({ amount: 2500, kind: 'qr' });
const msLeft = new Date(inv.expiresAt) - Date.now();
showCountdown(msLeft);          // по истечении сами прячьте QR
```

Если покупатель отсканирует просроченный QR, Kaspi покажет ему сообщение в духе «попробуйте позже». Это не сбой — просто истёк срок счёта. Решение одно: **создать новый счёт**.

Поэтому там, где QR долго висит на экране (вендинг, турникет, витрина), ставьте обратный отсчёт и автоматически обновляйте код по истечении окна.

У счёта по телефону уведомление лежит в приложении покупателя, и решение он принимает без спешки. Это главное преимущество типа `phone`.

## Если у покупателя нет приложения Kaspi

Счёт `phone` уходит в приложение Kaspi покупателя. Если приложения нет или его Kaspi привязан к другому номеру, **уведомление не придёт никуда** — счёт останется в `pending` и в итоге истечёт.

В такой ситуации:

1. Создайте этому покупателю счёт `qr`.
2. Отправьте ему `payUrl` в WhatsApp или SMS — ссылка открывается и в браузере.
3. Либо дайте постоянную [ссылку на оплату](/kb/ru/payment-links).

Подробнее: [У покупателя нет приложения Kaspi](/kb/ru/customer-no-kaspi).

## Формат номера

Для `phone` номер указывается в формате `7XXXXXXXXXX`, 11 цифр: `77011234567`. Варианты с `+7`, `8`, пробелами и дефисами не принимаются — придёт `invalid_phone`.

Нормализуйте номер перед отправкой:

```js
const normalize = (raw) => {
  const d = String(raw).replace(/\D/g, '');
  if (d.length === 11 && d.startsWith('8')) return '7' + d.slice(1);
  if (d.length === 10) return '7' + d;
  return d;
};
```

## Какой тип под какой сценарий

| Сценарий | Что брать | Почему |
|---|---|---|
| Экран на кассе, витрина | `qr` | Покупатель рядом, номер не нужен |
| Вендинг, турникет, шлагбаум | `qr` | Отсканировал и сразу оплатил |
| Интернет-магазин, страница заказа | `qr` | Вы отправляете покупателя на `payUrl` |
| Instagram, TikTok, директ | `qr` или ссылка на оплату | Ссылку можно дать, не спрашивая номер |
| Счёт после телефонного разговора | `phone` | Покупатель далеко, номер известен |
| Очередной платёж по подписке | `phone` | Покупатель видит счёт в своём приложении |
| Массовая рассылка: водители, слушатели | `phone` | Номера уже в базе, рассылать ссылки не нужно |
| Оплата в момент доставки | `qr` | QR на телефоне курьера |

Типы можно комбинировать: отправить `phone`, а если покупатель не открыл его за пару минут — прислать в чат ещё и ссылку из `qr`-счёта. Приём распространённый и работает хорошо.

## Что у них общего

- Одинаковые статусы: `new` → `pending` → `paid` / `cancelled` / `expired`.
- Одинаковые события вебхуков: `invoice.paid` и остальные.
- `externalOrderId`, `metadata` и `Idempotency-Key` работают в обоих типах.
- Возврат делается одинаково.
- Деньги в обоих случаях приходят напрямую на ваш счёт Kaspi.
- В лимит тарифа оба типа считаются одинаково.

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

**Что будет, если не указать `kind`?** Применится значение по умолчанию — `qr`.

**Можно ли изменить тип у созданного счёта?** Нет. Отмените старый счёт и создайте новый.

**Есть ли `payUrl` у счёта `phone`?** Да, он приходит в ответе. Если покупатель не заметил пуш, эту ссылку можно отправить ему отдельно.

**Видит ли покупатель номер кассира?** Да, в уведомлении Kaspi номер кассира виден — это штатное поведение Kaspi, одинаковое для обоих типов.

**Нужно ли самому рисовать QR-код?** Нет: в ответе уже есть готовые `qrImageUrl` и `deepLink`.

**Нормально ли, что счёт долго висит в `pending`?** Это значит, что покупатель пока не оплатил. Когда счёт станет `expired`, видно в поле `expiresAt`: [Счёт завис в статусе pending](/kb/ru/invoice-stuck-pending).
