# Счета дублируются

> Если на один заказ выставляется несколько счетов, сначала надо остановить поток: удалите API-ключ — интеграция встанет в ту же секунду. Потом ищите причину и включайте идемпотентность.

## Коротко

Первым делом **остановите поток**: удалите API-ключ в кабинете. С момента удаления все запросы с этим ключом получают 401, то есть новые счета не создаются — и пока вы правите код, покупатели не увидят лишних счетов. Причину ищите уже после остановки: обычно это отсутствие идемпотентности, цикл повторов или неправильная обработка вебхуков. Надёжное решение — передавать заголовок `Idempotency-Key` в каждом запросе на создание счёта.

## Экстренная остановка

1. Заходите в кабинет, открываете раздел API-ключей
2. **Удаляете** ключ, которым выставляются счета
3. Правите код
4. Создаёте новый ключ и меняете его на сервере

Удаление ключа — самый быстрый «рубильник». Ни передеплой, ни остановка сервера для этого не нужны.

Что не меняется: ранее созданные счета остаются на месте, настройки вебхуков сохраняются, привязка Kaspi не рвётся. Недействительным становится только сам ключ.

Учтите: это останавливает всю интеграцию, то есть правильные счета тоже перестанут создаваться. Если сломан один участок и ключей у вас несколько, удаляйте только его ключ.

## Уборка лишних счетов

После остановки потока:

- Закройте неоплаченные лишние счета через `POST /api/v1/invoices/{id}/cancel` — тогда покупатель не оплатит их случайно
- Если покупатель всё-таки заплатил дважды, лишнее возвращаете: [Возврат не проходит](/kb/ru/refund-not-working)
- Отфильтруйте список счетов по `externalOrderId` и посмотрите, сколько их вышло на один заказ

Не торопитесь: счёт в статусе `pending` ещё может быть оплачен, поэтому уборку начинайте только после остановки потока.

## Поиск причины

| Признак | Вероятная причина |
|---|---|
| Ровно два счёта на заказ | Покупатель дважды нажал «Оплатить» или форма ушла дважды |
| Десятки счетов на один заказ | Цикл в коде: повтор при ошибке без условия выхода |
| Счета выходят с ровным интервалом | Задача по расписанию создаёт новый счёт на каждом проходе |
| Счёт появляется на каждый вебхук | Обработчик вебхука сам создаёт счёт, а вебхук повторяется 11 раз |
| Пришла ошибка `tariff_daily_burst` | Сработала суточная защита — она и сделана для отлова таких циклов |

`tariff_daily_burst` — это не бизнес-лимит, а предохранитель от зациклившейся интеграции. Если он сработал, не спешите повышать тариф: сначала проверьте код. Месячный лимит приходит с другим кодом — `tariff_limit_reached`.

## Как работает Idempotency-Key

Вы добавляете к запросу на создание счёта заголовок `Idempotency-Key`. При повторной отправке с тем же ключом **новый счёт не создаётся**: возвращается прежний, с HTTP 200 и признаком `idempotentReplay: true` в ответе.

```
POST /api/v1/invoices
X-API-Key: qp_live_…
Idempotency-Key: order-10482
Content-Type: application/json

{ "amount": 12500, "externalOrderId": "10482" }
```

Как выбирать ключ:

- **Привязывайте его к заказу**: номер заказа, идентификатор корзины, идентификатор попытки оплаты
- **Не делайте его случайным.** Новый UUID на каждый запрос лишает идемпотентность смысла
- **Не допускайте коллизий.** Если по одному заказу нужны два разных платежа, добавьте номер: `order-10482-1`, `order-10482-2`
- Заодно заполняйте `externalOrderId`: он возвращается в вебхуке и удобен для поиска счетов

Идемпотентность — не разовая заплатка, а норма. Держите её включённой и тогда, когда всё работает: она сама выручит при обрыве сети, таймауте или перезапуске.

## Как правильно писать повторы

Чтобы повтор запроса на создание счёта был безопасным:

- **Повторяйте только то, что имеет смысл повторять**: 502, 503 и 429 с учётом `Retry-After`. Ошибки 4xx повторять бесполезно — надо исправлять запрос
- **Наращивайте паузу**: 1, 2, 4, 8 секунд. Не долбите в цикле
- **Ограничьте число попыток**: например, после пяти остановиться и пометить заказ для ручного разбора
- **В каждой попытке шлите тот же `Idempotency-Key`**, а не новый
- **Не считайте таймаут отказом.** Запрос мог дойти, счёт мог создаться, а потеряться мог только ответ. Ровно этот случай и закрывает ключ идемпотентности

## Идемпотентность в обработке вебхуков

Если вы отвечаете не 2xx, мы повторяем доставку **11 раз**. Поэтому одно событие приходит несколько раз — это нормально. Если ваш обработчик на каждое пришедшее уведомление создаёт новый счёт или заново проводит заказ, дубли берутся именно отсюда.

Берите пару `(invoice.id, status)` как ключ и не обрабатывайте повторно то, что уже обработали. Если обработка долгая, сначала ответьте 200, а работу делайте в фоне — иначе мы засчитаем таймаут и отправим снова.

## Профилактика

- Отключайте кнопку «Оплатить» после первого нажатия
- Перед созданием нового счёта проверяйте, нет ли по этому заказу открытого
- Специально проверьте повтор в песочнице: отправьте дважды с одним `Idempotency-Key` и убедитесь, что во втором ответе пришло `idempotentReplay: true`
- Подключите Telegram-бота, чтобы резкий рост числа счетов не остался незамеченным

## Вопросы и ответы

**Если удалить ключ, пропадут ли старые счета?** Нет. Счета, их статусы и история сохраняются. Недействительным становится только ключ.

**Сколько живёт Idempotency-Key?** Не рассчитывайте переиспользовать один ключ сколь угодно долго: он защищает от повторов в ближайшее время. Если берёте за ключ номер заказа, на практике этого достаточно.

**Считаются ли счета из песочницы в лимит?** Нет, счета песочницы в месячный лимит не идут.

**Покупатель оплатил оба счёта, что теперь?** Вернуть лишнюю сумму. Частичный возврат тоже поддерживается.

**Достаточно ли `externalOrderId` для защиты от дублей?** Нет, это поле для поиска и учёта. От повторов защищает заголовок `Idempotency-Key`.
