Коротко
При создании счёта у поля 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.
Если в одном коде используются оба типа, безопаснее обрезать текст заранее:
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 в ответе.
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 и в итоге истечёт.
В такой ситуации:
- Создайте этому покупателю счёт
qr. - Отправьте ему
payUrlв WhatsApp или SMS — ссылка открывается и в браузере. - Либо дайте постоянную ссылку на оплату.
Подробнее: У покупателя нет приложения Kaspi.
Формат номера
Для phone номер указывается в формате 7XXXXXXXXXX, 11 цифр: 77011234567. Варианты с +7, 8, пробелами и дефисами не принимаются — придёт invalid_phone.
Нормализуйте номер перед отправкой:
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.