Skip to main content
Сеть ненадёжна, а вы работаете с деньгами. Эта инструкция — про то, как построить клиент, который переживает таймауты, повторы и временные сбои, не создавая дублей и не теряя платежи. Здесь сведены воедино идемпотентность, обработка таймаутов, ретраи и backoff. Справочная база — Идемпотентность и Формат ответа и коды ошибок.

Главный принцип

Каждый денежный запрос отправляйте с заголовком 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). Устойчивость нужна с обеих сторон. → Настройка вебхуков

Связанные страницы

Идемпотентность

Формат ответа и коды ошибок

Справочник кодов ошибок

Модель баланса

Настройка вебхуков

Чек‑лист перед запуском