Коротко
Создание счёта — это один запрос:
POST https://api.qut.kz/api/v1/invoices
X-API-Key: qp_live_…
Content-Type: application/json
В ответе приходит статус 201 и payUrl — отправьте покупателя по этому адресу. Обязательное поле всего одно: amount.
Старый путь POST /api/v1/orders тоже принимается, это алиас того же эндпоинта.
Поля запроса
| Поле | Тип | Обязательно | Описание | |
|---|---|---|---|---|
amount | number | ✅ | Тенге. В QR-счёте не более 2 знаков после запятой, в счёте по телефону — целое | |
kind | qr \ | phone | — | qr — по умолчанию: QR-код и ссылка. phone — push в приложение Kaspi покупателя |
description | string | — | Видит покупатель. QR — 100 символов, phone — 60 | |
externalOrderId | string | — | Ваш номер заказа. Возвращается в вебхуке | |
customer.name | string | — | Имя покупателя | |
customer.phone | string | ✅ для kind: phone | В формате 7XXXXXXXXXX, 11 цифр | |
customer.email | string | — | Если указан, покупателю может уйти письмо с чеком | |
successUrl | url | — | Куда вернуть покупателя после успешной оплаты. Только http(s) | |
failUrl | url | — | Куда вернуть, если оплата не прошла. Только http(s) | |
metadata | object | — | Любой JSON. Хранится без изменений и возвращается в вебхуке |
Заголовки:
| Заголовок | Обязателен | Зачем |
|---|---|---|
X-API-Key | ✅ | qp_live_… или qp_test_… |
Content-Type: application/json | ✅ | Тело в JSON |
Idempotency-Key | — | Защита от дублей, см. ниже |
Если передать Idempotency-Key, повторный запрос с тем же ключом не создаст новый счёт: вернётся прежний, с HTTP 200 и полем idempotentReplay: true. Подробнее: Идемпотентность.
Разница между qr и phone
qr | phone | |
|---|---|---|
| Что видит покупатель | QR-код или ссылку на оплату | Push-счёт в приложении Kaspi |
customer.phone | Не обязателен | Обязателен |
| Сумма | До 2 знаков после запятой | Только целые тенге |
description | 100 символов | 60 символов |
| Когда удобно | Сайт, офлайн-точка, экран | Продажа по телефону, удалённо |
Полное сравнение: QR-счёт или счёт по телефону.
Ответ
HTTP 201 и тело такого вида:
| Поле | Что это |
|---|---|
id | Идентификатор счёта, inv_… |
status | При создании pending |
payUrl | Страница оплаты, сюда отправляйте покупателя |
qrUrl | Содержимое QR-кода |
deepLink | Ссылка, открывающая приложение Kaspi |
qrImageUrl | Картинка QR, можно вывести на своей странице |
expiresAt | После этого времени счёт недействителен |
Окно сканирования QR — примерно три минуты, его задаёт Kaspi. Не зашивайте это число в код: всегда берите expiresAt из ответа.
Пример на curl
curl -X POST https://api.qut.kz/api/v1/invoices \
-H 'X-API-Key: qp_live_…' \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-1001' \
-d '{
"amount": 2500,
"kind": "qr",
"description": "Заказ №1001",
"externalOrderId": "1001",
"customer": { "name": "Айгуль", "phone": "77010000000" },
"successUrl": "https://site.kz/ok",
"failUrl": "https://site.kz/fail",
"metadata": { "branch": "almaty-1", "cart": 17 }
}'
Пример на Node
const res = await fetch('https://api.qut.kz/api/v1/invoices', {
method: 'POST',
headers: {
'X-API-Key': process.env.QUTPAY_API_KEY,
'Content-Type': 'application/json',
'Idempotency-Key': `order-${order.id}`,
},
body: JSON.stringify({
amount: order.total,
kind: 'qr',
description: `Заказ №${order.id}`,
externalOrderId: String(order.id),
successUrl: 'https://site.kz/ok',
metadata: { orderId: order.id },
}),
});
if (!res.ok) {
const err = await res.json();
console.error('qutpay', res.status, err.error, err.message);
throw new Error(err.error);
}
const invoice = await res.json();
redirect(invoice.payUrl);
При ошибке смотрите на код в поле error, а не на текст message: текст может измениться, код — нет.
Алиас /orders
Для старых интеграций сохранён путь POST /api/v1/orders. Там дополнительно принимаются поля merchantRef (то же, что externalOrderId) и method: "invoice". Если пишете новый код, используйте /api/v1/invoices.
Частые ошибки
| Код | HTTP | Что случилось | Решение |
|---|---|---|---|
invalid_amount | 422 | Суммы нет или это не число | Передайте положительное число |
amount_must_be_whole_tenge | 422 | Переданы тиыны | Передайте целые тенге |
amount_too_small / amount_too_large | 422 | Сумма вне допустимого диапазона | Исправьте сумму |
invalid_phone | 422 | Неверный формат телефона | 7XXXXXXXXXX, 11 цифр, без + и пробелов |
phone_required | 422 | kind: phone, но телефона нет | Добавьте customer.phone |
invalid_kind | 422 | Неизвестный тип | qr или phone |
invalid_url | 422 | Неверный successUrl/failUrl | Укажите полный http(s)-адрес |
unauthorized | 401 | Ключ не передан или недействителен | Проверьте заголовок X-API-Key |
insufficient_scope | 403 | У ключа нет invoices:write | Добавьте право в кабинете |
kaspi_session_expired | 409 | Привязка кассира оборвалась | Переподключите кассира |
tariff_limit_reached | 429 | Закончился месячный лимит | Тарифы и лимиты |
invoice_create_failed | 502 | Kaspi не принял счёт | Повторите с растущей паузой |
Полный список: Каталог ошибок.
Пограничные случаи
- Тиыны. QR-счёт принимает два знака после запятой, счёт по телефону — только целые тенге. Если в сумме есть тиыны, округлите до отправки с
kind: phone. - Длинное описание. Текст сверх лимита у покупателя может обрезаться, поэтому не превышайте 100 символов для QR и 60 для phone.
- Несколько счетов на один заказ. Двойное нажатие кнопки «оплатить» создаст два счёта. Ставьте
Idempotency-Key. - Нужно много счетов сразу. До 100 счетов можно создать одним запросом: Массовое создание счетов.
- Не дождались ответа. При таймауте счёт мог быть создан. Повторите с тем же
Idempotency-Key— новый не появится.
Вопросы и ответы
Чем payUrl отличается от deepLink? payUrl — страница оплаты в браузере, работает на любом устройстве. deepLink открывает приложение Kaspi напрямую, удобен на телефоне.
Как узнать статус счёта? Через вебхук или запросом GET /api/v1/invoices/{id}. Что когда: Вебхук или опрос статуса.
Что можно класть в metadata? Любой JSON: номер точки, идентификатор корзины, номер ячейки. Он возвращается и в вебхуке, и в ответе на запрос статуса.
Можно изменить сумму уже созданного счёта? Нет. Отмените счёт и создайте новый.
В песочнице те же поля? Да, полностью. Разница только в том, что Kaspi не вызывается и оплату вы симулируете сами.