Главный принцип
Каждый денежный запрос отправляйте с заголовком
Idempotency-Key. Тогда любой повтор безопасен.Два механизма, а не один
Дедупликацию дают два независимых механизма. Они работают вместе, и лучше использовать оба.
Ключевое правило заголовка: значение генерируется один раз, до первой отправки, и остаётся тем
же во всех ретраях этого действия. Если вы сгенерируете новый ключ на каждую попытку — защиты не
будет вовсе, вы просто отправите N разных запросов.
call() (базовая версия — в инструкции
Как подписать запрос):
Заголовок
Idempotency-Key не участвует в подписи — подписывается только каноническая строка
ts\nMETHOD\npath\nbody. Добавление заголовка ничего не ломает в подписи.Что делать, если заголовок ещё не внедрён
order_id тоже дедуплицирует — это ваша страховка. Если вы не готовы менять транспортный слой,
минимум остаётся прежним: всегда задавайте order_id, и повтор вернёт существующий объект.
Опасен только случай, когда нет ни того, ни другого: без заголовка и без order_id повтор создаст
дубль. Для выплат это невозможно (order_id обязателен), для платежей — вполне.
Проблема таймаута: «ответ не пришёл — операция прошла или нет?»
Самая коварная ситуация. Вы отправилиPOST /v1/payout, но ответ не дошёл (таймаут, разрыв). Выплата
могла:
- не создаться — запрос не дошёл до сервера;
- создаться — сервер обработал, но ответ потерялся по пути к вам.
Вариант А. Повторить тот же запрос
Повторяйте с тем жеIdempotency-Key и тем же order_id:
Idempotent-Replayed: true). Если
не прошла — создастся сейчас. В обоих случаях итог один, и денег не уйдёт дважды.
409 idempotency.in_progress означает, что запрос с этим ключом прямо сейчас выполняется на
сервере. Это не ошибка и не отказ: подождите и повторите — получите результат первой попытки.Вариант Б. Спросить статус
/v1/payment/info, для выплат —
/v1/payout/info.
Почему подписи недостаточно
Подпись запроса живёт в окне ±5 минут — она гасит переигрывание запроса старше пяти минут, но не защищает от повторной обработки внутри окна. Реальную защиту от дублей даютIdempotency-Key и
order_id, а не подпись. Не полагайтесь на подпись как на защиту от двойного списания. →
Аутентификация
Какие ошибки повторять, а какие — нет
Не все 409 финальны
Самая дорогая ошибка в клиенте: трактовать любой409 как финал и бросить операцию. Так вы
потеряете законную выплату. Среди 409 есть ретраибельные:
Полный список кодов — Справочник кодов ошибок.
Особый случай: 409 payout.funds_maturing
Это не повод отказаться от выплаты навсегда. Средства ещё дозревают (см.
Модель баланса). Это временно: повторите позже с backoff — когда депозит
наберёт подтверждения, выплата пройдёт. Не путайте с 409 payout.insufficient_funds (реально не
хватает денег — нужно пополнение, а не ожидание).
Экспоненциальный backoff
Для5xx/503/rate limit повторяйте с растущей задержкой, а не в цикле без пауз.
Ключевая идея: повторять надо по HTTP‑статусу (429/5xx) и по сетевым сбоям, а не только по
error.code. Здесь signed_post / signedPost — ваш POST с подписью, возвращающий сырой ответ
(requests.Response в Python, Response из fetch в Node.js — в отличие от call(), который отдаёт
уже разобранное тело), чтобы был виден статус и заголовок Retry-After.
Почему по HTTP-статусу, а не по
error.code? Ответ 429 приходит в особом виде
{"state":1,"message":"rate limit exceeded"} — без error.code, поэтому ветвиться на код
бесполезно: 429 надо ловить именно по статусу и читать заголовок Retry-After.Практические правила
Idempotency-Keyгенерируется до первой отправки и не меняется при ретраях. Новый ключ на каждую попытку = никакой защиты.order_idуникален и стабилен на операцию. Один заказ → одинorder_id, и при ретраях — тот же. Не генерируйте новый на каждую попытку.- Таймаут не равен провалу. Прежде чем считать операцию неудачной, повторите тот же запрос (тот же
Idempotency-Keyиorder_id) или спросите*/info. 409— не повторяйте вслепую, но и не хороните.payout.funds_maturing,idempotency.in_progress,payout.frozen— повторяйте позже; остальные — сначала выясните состояние через*/info.- Разделяйте временное и постоянное.
5xx/503/429/funds_maturing— ждите и повторяйте;4xx— чините запрос. - Храните сопоставление
order_id↔ ваш заказ на своей стороне — это ваш якорь для сверки после любого сбоя. - Ограничьте число попыток и логируйте исчерпание — бесконечный ретрай маскирует реальные проблемы.
Как это сочетается с вебхуками
Ретраи касаются исходящих запросов. Для входящих вебхуков действует зеркальный принцип: они доставляются at-least-once, поэтому ваш обработчик тоже должен быть идемпотентным (дедуп поuuid + status). Устойчивость нужна с обеих сторон. → Настройка вебхуков