Коротко
В вендинге схема простая: покупатель выбирает товар → контроллер автомата сообщает вашему серверу в облаке → сервер создаёт счёт в Qut Pay → QR появляется на экране автомата → покупатель сканирует его в Kaspi → к вам приходит вебхук invoice.paid → сервер отправляет автомату команду на выдачу. Одинаково работает для автоматов воды, кофе и снеков. QR создаётся заново на каждую продажу — напечатанная и висящая на корпусе картинка не подойдёт.
Как это работает
| Шаг | Кто | Что делает |
|---|---|---|
| 1 | Покупатель | Выбирает товар на автомате (например, 5 литров воды) |
| 2 | Контроллер | Запрос на ваш сервер: номер автомата, ячейка, сумма |
| 3 | Ваш сервер | POST /api/v1/invoices, в metadata — номер автомата и ячейка |
| 4 | Ваш сервер | Возвращает контроллеру qrImageUrl (или qrUrl) и expiresAt |
| 5 | Автомат | Показывает на экране QR и таймер |
| 6 | Покупатель | Сканирует в приложении Kaspi и подтверждает |
| 7 | Qut Pay | Шлёт на сервер invoice.paid |
| 8 | Ваш сервер | Отправляет автомату команду: открыть ячейку / включить насос |
| 9 | Автомат | Выдаёт товар и возвращает экран в исходное состояние |
После подтверждения покупателем вебхук обычно приходит в пределах пяти секунд, так что человек может спокойно подождать у автомата.
Какой метод API используется
Основной — POST /api/v1/invoices с kind: "qr":
POST https://api.qut.kz/api/v1/invoices
X-API-Key: qp_live_…
Idempotency-Key: vm-014-1726300000
{
"amount": 250,
"kind": "qr",
"description": "Автомат №14, вода 5 л",
"externalOrderId": "vm-014-88231",
"metadata": { "machine": "VM-014", "slot": "A2", "address": "Абая 10" }
}
Из ответа нужны: id, qrImageUrl (картинка на экран), qrUrl, expiresAt (для таймера).
Дополнительно:
GET /api/v1/invoices/{id}— спросить статус, если вебхук задержалсяPOST /api/v1/invoices/{id}/cancel— покупатель ушёл, счёт закрываетсяPOST /api/v1/invoices/{id}/refund— товар не выдан, деньги возвращаются
Кладите номер автомата в metadata
Это основа всего сценария. В metadata положите как минимум три вещи: номер автомата, ячейку или код товара, адрес точки. Когда придёт вебхук, вы сразу знаете, какому автомату слать команду, и не ищете это по своей базе.
В externalOrderId удобно собирать номер автомата и внутренний номер продажи: vm-014-88231. По нему потом легко и отчитаться, и разобрать спорную ситуацию.
Если нужна раздельная отчётность по точкам, заведите на каждую точку свой API-ключ — фильтровать счета по источнику станет проще.
Что нужно со стороны железа
- Контроллер со связью. GSM-модем с SIM-картой или Wi-Fi-модуль, если на точке есть сеть. Без подключения схема не работает: счёт создаётся в облаке.
- Экран. Подойдёт любой, на котором можно показать QR. У части автоматов он уже есть, к остальным ставят небольшой дисплей.
- Сервер в облаке. Создаёт счета, принимает вебхуки, шлёт команды автоматам. API-ключ лежит только здесь, не в контроллере.
- Канал команд. Чаще всего MQTT: контроллер подписан на брокер, сервер публикует в его тему «открыть ячейку A2». Можно и по HTTP — контроллер сам опрашивает сервер, но MQTT быстрее и меньше нагружает связь.
Не кладите API-ключ в контроллер. Устройство внутри автомата можно вскрыть, а один ключ действует на всю сеть. Пусть контроллер ходит только на ваш сервер и только со своим устройственным токеном.
Окно QR — главное ограничение
Окно сканирования — около трёх минут, его задаёт Kaspi. В вендинге отсюда следуют сразу два вывода.
Постоянный QR на экране висеть не может. Счёт создаётся на каждую продажу заново. Заранее напечатанная картинка перестанет работать уже на следующий день.
Показывайте таймер. Возьмите expiresAt и выведите на экран «QR действителен ещё 2:40». Когда время вышло, дайте кнопку «Показать заново» — контроллер запросит новый счёт, старый останется expired.
Если всё же нужен постоянный печатный код (например, дополнительная строка «оплатить здесь, если что-то пошло не так»), это должен быть не QR счёта, а ссылка на оплату вида qut.kz/p/<slug>: у неё нет срока жизни, и есть вариант с открытой суммой.
На что обратить внимание
- Идемпотентность. Обрыв связи и повторная отправка запроса контроллером — обычное дело. Ставьте
Idempotency-Key, иначе одному покупателю выставится два счёта. - Обработчик вебхука должен быть идемпотентным. Пара
(invoice.id, status)обрабатывается ровно один раз, иначе автомат выдаст две бутылки. - Если товар не выдан. Деньги пришли, а ячейка не открылась (заклинило, товар кончился) — заложите автоматический
refund. Это самый частый спор в вендинге. - Держите вебхук и опрос статуса вместе. Покупатель стоит у автомата, ждать долго нельзя. Если вебхука нет 5-10 секунд, пусть контроллер спросит
GET /invoices/{id}: Оплата подтверждается медленно. - Адрес вебхука должен быть открыт. В бою принимается только
httpsи настоящий домен, IP и адреса туннелей не подойдут. Адрес должен отвечать без авторизации, иначе доставка будет падать: Вебхук не приходит. - Поздняя оплата. Если деньги придут на истёкший счёт, событие придёт с признаком
late: true. Покупателя рядом уже нет — возвращайте деньги. - С ростом сети пересмотрите тариф. Десять автоматов по 50 продаж в день — это 15 000 счетов в месяц: Какой тариф выбрать.
Вопросы и ответы
На автомате нет интернета — подойдёт? Нет. Счёт создаётся в облаке, а автомат должен узнать о подтверждении оплаты. Самый дешёвый вариант — контроллер с GSM-модемом.
Что будет, если один QR отсканируют несколько человек? Один QR — это один счёт. Поэтому он годится ровно на одну продажу, и после каждой экран нужно возвращать в исходное состояние.
У автомата нет экрана, только кнопки. Как быть? Придётся поставить небольшой дисплей. Если показать QR негде, схема не работает.
Покупатель отсканировал, и тут пропала связь. Где деньги? Деньги приходят на ваш счёт в Kaspi, от автомата это не зависит. Если товар выдать не удалось — делайте refund. Поэтому записывайте в журнал каждую команду контроллеру.
Можно один ключ на десять автоматов? Можно, но лучше отдельный ключ на точку: и отчётность разделится, и при утечке вы удалите только один ключ, не трогая остальные.