Коротко
Если счета падают не по одному, а пачками, первым делом остановите отправку. Причина чаще всего в частоте: через одного кассира за короткое время ушло слишком много запросов. Kaspi может ограничить частоту запросов кассира, а мы не переотправляем упавший счёт сами — повтор это задача вашего кода. Значит, нужно остановить цикл, посмотреть на код ошибки и переотправить по очереди.
Опаснее всего код, который повторяет запрос сразу после каждой ошибки. Он только ухудшает положение и упирается в суточную защиту.
Сначала остановитесь
Порядок действий при массовом падении:
- Остановите процесс отправки. Cron, очередь, скрипт — что бы это ни было.
- Соберите поле
errorу последних 50 ошибок. Одинаковые они или разные — от этого зависит решение. - Отправьте вручную один счёт. Прошёл — дело в частоте. Упал — причина в другом.
- Выставьте счёт вручную из кабинета. Получилось — привязка Kaspi жива.
В экстренном случае, например если точно известно, что код ушёл в цикл, самый надёжный стоп-кран — удалить этот API-ключ в кабинете. После удаления ключа не создаётся ни один счёт. Новый ключ сделаете позже.
Разбор по кодам ошибок
error | Что означает | Что делать |
|---|---|---|
rate_limited, request_rate_limited | Превышена частота запросов | Прочитайте заголовок Retry-After и выждите указанное время |
too_many_attempts | Слишком частые повторы одного действия | Подождите минуту |
tariff_daily_burst | Сработала суточная защита | Это не бизнес-лимит. Проверьте, нет ли цикла в коде |
tariff_limit_reached | Исчерпан месячный лимит счетов по тарифу | Повысьте тариф или дождитесь нового месяца |
invoice_create_failed | Kaspi не принял счёт | Можно повторить с задержкой |
kaspi_session_expired | Привязка кассира оборвалась | Переподключите кассира |
insufficient_scope, tariff_inactive | Права или тариф | API отвечает 403 |
unauthorized | Ключ не распознан | API отвечает 401 |
Если код у всех один и тот же — причина одна, и чинится она легко. Если коды смешанные — чаще всего дело в частоте: система отбивает запросы в разных местах.
Не путайте tariff_daily_burst и tariff_limit_reached. Первое — суточная защита, обычно следствие цикла в коде. Второе — месячный лимит вашего тарифа, то есть настоящий бизнес-показатель.
Автоматических повторов нет
Это важно понимать точно: если запрос на создание счёта упал, мы не отправляем его заново сами. Так сделано намеренно — повторив без вашего ведома, мы рискуем отправить покупателю два счёта.
Повторы должны быть на вашей стороне и по таким правилам:
- Задержка между повторами растёт. 1, 2, 4, 8, 16 секунд. Мгновенный повтор не помогает никогда.
- Число повторов ограничено. Три-пять попыток, дальше — в очередь и уведомление.
- Ошибки 4xx не повторяются. Неверную сумму не примут ни с первого раза, ни со сотого.
- Отправляйте
Idempotency-Key. Тогда при повторе новый счёт не создастся, вернётся прежний.
const res = await fetch('https://api.qut.kz/api/v1/invoices', {
method: 'POST',
headers: {
'X-API-Key': key,
'Content-Type': 'application/json',
'Idempotency-Key': `order-${orderId}`,
},
body: JSON.stringify({ amount, description, externalOrderId: String(orderId) }),
});
В качестве Idempotency-Key берите собственный номер заказа — тогда повтор безопасен и счёт не раздваивается.
Отправляйте по очереди
Отправлять сто счетов за секунду незачем. Кассир — всего лишь одна роль в Kaspi, и частота его запросов тоже ограничена.
Схема, которая работает на практике:
async function sendQueue(orders) {
const failed = [];
for (const order of orders) {
try {
await createInvoice(order);
} catch (e) {
failed.push({ order, error: e.code });
if (e.code === 'rate_limited' || e.code === 'tariff_daily_burst') {
await sleep(60_000); // минута паузы, потом продолжаем
}
}
await sleep(300); // небольшая пауза между счетами
}
return failed;
}
Три главных принципа:
- Отправляйте последовательно, а не параллельно. Десять параллельных потоков — самая частая причина массового падения.
- Оставляйте паузу между счетами. Нескольких сотен миллисекунд достаточно.
- Остановитесь на первой ошибке и прочитайте причину. Слепое продолжение означает потерю всего списка.
Если в одном запросе действительно нужно несколько счетов, вместо отдельных вызовов используйте POST /api/v1/invoices/bulk: от 1 до 100 счетов за запрос, каждый проверяется отдельно, и в ответе видно, какие прошли, а какие упали.
Режим накопления
В системах с постоянным большим потоком самая надёжная схема — накапливать и отправлять постепенно:
- Каждое событие, требующее счёта, кладите не сразу в API, а в свою очередь
- Один рабочий процесс забирает из очереди и отправляет по очереди
- Упавшее остаётся в очереди и переотправляется с задержкой
- Если и после трёх попыток не прошло, уходит уведомление человеку
Такая схема сглаживает пики: даже если покупатели сделают сто заказов разом, в API пойдёт ровный поток. И главное — если система упадёт, заказы не потеряются, а останутся в очереди.
Чтобы не повторялось
- Поставьте защиту от цикла. В коде проверяйте, что по одному заказу создаётся ровно один счёт.
- Передавайте
externalOrderId. Тогда видно, какому заказу принадлежит счёт, в том числе в вебхуке, и дубли замечаются сразу. - Ведите журнал. Время запроса, код ответа, значение
error— без этого причину не найти. - Следите за суточным счётчиком. Приближение к суточной защите — повод проверить, нет ли цикла.
Вопросы и ответы
Создадутся ли упавшие счета позже сами? Нет. Переотправка — ваша задача.
Упёрся в суточную защиту, как её снять? Она отпускает сама со временем. Но сначала найдите причину: чаще всего в коде есть цикл.
Сколько счетов можно отправлять одновременно? Точного числа нет, ограничение зависит от ситуации. На практике отправка по одному с небольшой паузой не вызывает проблем никогда.
Если подключить несколько кассиров, станет быстрее? Нагрузка распределится, но это не основное решение. Сначала приведите в порядок логику отправки.
Как понять, что дело не во мне, а в Kaspi? Есть двухминутная диагностика: Проблема у вас или у Kaspi.