Идемпотентность, вебхуки и ретраи: как не потерять деньги
Любая интеграция с платным API однажды упирается в один и тот же вопрос: запрос ушёл, ответа нет, повторять или нет. Ответ зависит от того, как устроена идемпотентность. Разберём механику и места, где на ней теряют деньги.
Ключ идемпотентности
Идентификатор заказа придумываете вы, удобнее всего брать UUID. Повторный запрос на создание с тем же идентификатором вернёт тот же заказ, даже если параметры в теле другие, а повторное подтверждение оплаты не спишет депозит второй раз. Уникальность держит база платформы, а не её память о недавних запросах, то есть ретрай безопасен и через час.
Отсюда простое правило: новый заказ это новый идентификатор, а повтор упавшего запроса это старый идентификатор. Нарушение первой половины даёт клиенту чужой заказ, нарушение второй даёт двойное списание.
order_id = uuid4() # генерируем ОДИН раз и сохраняем у себя
save(order_id, status="new")
for attempt in range(3): # ретраим с тем же order_id
try:
r = post("/order/create", {"custom_id": order_id, ...})
break
except Timeout:
sleep(2 ** attempt)Ключевая деталь в первой строке: идентификатор генерируется до запроса и сохраняется в вашей базе. Если генерировать его внутри цикла повторов, идемпотентности не будет, сколько бы её ни обещало API.
Вебхук это ускорение, а не истина
Уведомление о смене статуса подписано и приходит с тремя повторами при неудаче. Но оно может не дойти вовсе: ваш сервер лежал, домен не разрешился, ответ был не из двухсотых. Поэтому опрос статуса заказа до терминального состояния нужен всегда, даже когда уведомления настроены.
Подпись проверяйте до разбора тела и сравнивайте её постоянным по времени сравнением. Незнакомые типы событий игнорируйте молча: список пополняется, и падать на новом событии интеграция не должна.
Три ловушки, на которых теряют деньги
Первая: признак возврата у успешного заказа. При частичной выдаче, когда поставщик отдал меньше позиций, чем заказано, статус остаётся «доставлен», а разница за невыданное возвращается на депозит. Если ветвиться по одному признаку возврата, такой заказ будет посчитан провальным, и покупателю вернут деньги за товар, который он уже получил. Смотрите на статус, а сумму берите из поля возврата.
Вторая: состояние «доставка на проверке». Это означает, что доставка идёт дольше обычного и её смотрит человек, а не что она провалилась. Деньги не возвращены, состояние не терминальное, возвращать оплату покупателю рано.
Третья: свой таймаут на асинхронных товарах. У части позиций доставка занимает часы, а в отдельных режимах ещё и ждёт действия покупателя. Жёсткий таймаут в пятнадцать минут превратит нормальный заказ в возврат, а клиент получит и деньги, и товар.
Что делать с незнакомым
Список товаров, событий и полей растёт. Незнакомое событие уведомления игнорируйте, незнакомый код провала считайте обычным провалом, новые поля в ответах не считайте нарушением схемы. Интеграция, которая падает на новом поле, будет ломаться при каждом обновлении на стороне платформы.
Ретраи и лимиты
- Повторяйте только сетевые сбои и пятисотые, но не 400 и не 409.
- На 429 ждите столько, сколько сказано в заголовке, и не быстрее.
- Ставьте потолок на число повторов, дальше пишите в свою очередь на разбор.
- Логируйте свой идентификатор заказа рядом с ответом, иначе разбор инцидента невозможен.
Минимальный чек-лист перед продом
- Идентификатор заказа генерируется до запроса и лежит в вашей базе.
- Опрос статуса работает и без уведомлений.
- Подпись уведомления проверяется до разбора тела.
- Возврат покупателю делается по статусу, а не по одному признаку возврата.
- Суммы считаются в целых единицах, округление одно и на выводе.
Ключ выдаётся через Telegram
Семь эндпоинтов, оптовые цены с депозита, идемпотентные заказы. Регистрация и KYC не нужны.