Как это работает у вас
В терминах парка:
Ваша система биллинга формирует список «водитель → сумма за смену» → выставляет счета через API (по одному или пачкой) → каждый водитель получает push в своём Kaspi → оплатил → ApiPay шлёт вебхук paid → ваша система закрывает задолженность водителя.
Для массового биллинга важна равномерность потока во времени, а не только дневной объём: Kaspi ограничивает частоту запросов кассира, и плотный залп в этот троттлинг упирается. Пачка POST /invoices/bulk экономит сетевые обращения и даёт один общий ответ, но темп выставления в Kaspi не меняет — разносите биллинг волнами сами.
Создание счёта асинхронное: 201 со status: processing, дальше вебхук переводит счёт в pending либо в error. Счёт, который так и не доехал до Kaspi, система сама закрывает в error с error_code: network_unavailable.
Компоненты ApiPay для массового биллинга
Массовый биллинг опирается на несколько частей Kaspi API:
| Компонент | Зачем парку |
|---|---|
| Счёт по номеру | Водитель получает push; счёт живёт 24 часа |
Идемпотентность (external_order_id_idempotency) |
Один период = один счёт: повтор запроса не создаст дубль (409) |
| Bulk-выставление | POST /invoices/bulk: до 100 счетов одним запросом вместо ста запросов, общий ответ с результатом по каждой позиции |
Вебхук paid/error |
Автозакрытие задолженности; перевыставление только по error |
| Тариф по объёму | Фиксированная подписка под ваш дневной объём счетов — тарифы и комиссия |
Пошаговый сетап
- Зарегистрируйтесь на apipay.kz и опишите объём поддержке: большие объёмы — обсудите лимит тарифа заранее (лимит счетов по тарифу).
- Отладьте цикл в песочнице по apipay.kz/docs: список → счета → вебхуки → закрытие задолженности.
- Сразу заложите идемпотентность:
external_order_id_idempotency= стабильный ID начисления. Если ваш внутренний ID длинный и «грязный» (например,A000AA00 за 03.06…) — возьмите от него md5-хэш и передавайте его: ключ ограничен 191 символом и должен быть одинаковым при всех повторах запроса. - Подключите номер кассира (требования) и включите рабочий режим. До одобрения анкеты «Расскажите о бизнесе» (зачем) организация ограничена одним реальным платежом в сутки: в
POST /invoices/bulkостальные позиции вернутся вfailed[]сerror_code: kyc_daily_limit_reached. Массовый биллинг запускайте после одобрения. - Постройте обработку статусов:
paid→ закрыть долг;error→ перевыставить самим;processing→ ждать, ничего не делать. - Спланируйте расписание: списки отправляйте bulk-запросами, а сам биллинг разносите на волны (например, по сменам) — темп задаёте вы, bulk его не сглаживает.
Грабли именно массового биллинга
- Двойное списание без идемпотентности. Типичный инцидент: два одинаковых POST с одного ключа, оба получили 201, водитель получил 2 push и оплатил оба. Без
external_order_id_idempotencyкаждый POST — новый живой платёж. С ним повтор получает 409. - Пересоздание счетов в
processing. Не пересоздавайте счёт, пока он вprocessing: получатся два живых счёта, и водитель может оплатить оба. Ждите терминального статуса — счёт либо перейдёт вpending, либо система сама закроет его вerrorс вебхуком. - Плотный залп ловит троттлинг Kaspi. Такие счета уходят не в
cancelled, а в терминальныйerrorсerror_code: kaspi_throttled(«попробуйте через 2–3 минуты») — их надо перевыставлять новым счётом, ждать бесполезно. Решение — разносить выставление во времени волнами. - Один номер — один кошелёк. Если в списке ошибочный номер, Kaspi может срезолвить его в реальный чужой аккаунт — все счета упадут в один кошелёк, а настоящие водители их не увидят. Валидируйте номера водителей при онбординге.
Контракт bulk-выставления
POST /api/v1/invoices/bulk — до 100 счетов за запрос, все на одного кассира, лимит 20 запросов/мин; элементы — те же поля, что у POST /invoices.
Ответ 201: {created, duplicates, failed, invoices:[{index, status:"created"|"duplicate"|"failed", ...}]} — разбирайте поэлементно по index. Структурная ошибка тела отклоняет весь батч атомарно (422).
Лимиты приходят поэлементно в failed[] (kyc_daily_limit_reached, tariff_limit_reached — только error_code и message, без Retry-After и meta), а 403 tariff_inactive отбивает весь запрос целиком: причина не в позиции, а в организации.
Частые вопросы
Можно ли спокойно выставлять сотни счетов в день?
Да, в пределах дневного лимита вашего тарифа — но ровным потоком, а не залпом: частоту ограничивает Kaspi.
Нужна ли заявка на официальное партнёрство Kaspi?
Нет. ApiPay — независимый сервис, не официальная интеграция; нужен только открытый счёт Kaspi Pay вашего парка.
Водители без Kaspi есть — что с ними?
Счёт уйдёт только владельцу Kaspi-аккаунта; для остальных предусмотрите другой канал оплаты и обрабатывайте ошибку client_not_found из вебхука.