Перейти к содержимому
ZeroKYC Pay

Жизненный цикл инвойса

Стейт-машина инвойса: статусы, статусы вариантов, observations, окна уникальных сумм и какой вебхук когда срабатывает.

Инвойс — это небольшая стейт-машина. Знание точных состояний, кто их двигает и какой вебхук срабатывает на каждом переходе, делает логику исполнения заказа герметичной.

Состояния

pending ──► detecting ──► confirmed
   │             │
   │             └──► underpaid   (пришло меньше приемлемого диапазона — терминальный)
   ├──► expired                (TTL истёк, ничего не пришло)
   └──► canceled               (вы отменили до оплаты)
СтатусСмыслТерминальный?
pendingСоздан, ждёт платежанет
detectingНайдена совпадающая транзакция; ждём подтвержденийнет
confirmedДостигнут порог подтверждений — отгружайтеда
underpaidПлатёж пришёл ниже приемлемого диапазонада
expiredTTL истёк, совпадающих транзакций не былода
canceledОтменён через API до оплатыда

Два правила, которые стоит усвоить:

  • Мемпул побеждает TTL. Транзакция, замеченная в мемпуле до expires_at, остаётся валидной, даже если подтверждения приходят после истечения — такие инвойсы переходят pending → detecting и подтверждаются штатно; в expired они не попадают.
  • Первый платёж выигрывает. При payment_currency=any первый валидный платёж по любому варианту подтверждает весь инвойс; остальные варианты в этот момент закрываются.

Что когда срабатывает

ПереходВебхук
транзакция замечена (pending → detecting)payment.detected
достигнуты подтверждения (→ confirmed)payment.confirmed
платёж ниже приемлемого диапазонаpayment.underpaid (инвойс становится underpaid)
TTL истёк, ничего не замеченоinvoice.expired
POST /v1/invoices/{id}/cancelinvoice.canceled

Rescan-воркер перепроверяет каждый открытый инвойс раз в 3 минуты, поэтому опоздавшие данные цепи самолечатся — переходы (и их вебхуки) могут приходить через минуты после события на цепи.

Платёжные варианты и observations

Ответ инвойса несёт два массива, по которым видно, где именно деньги:

  • options[] — по одному на включённый актив: адрес, amount_crypto к оплате, использованный rate и собственный статус: open → detected (по этому варианту замечена транзакция) → confirmed; когда подтверждается другой вариант, остальные становятся closed.
  • observations[] — сырые наблюдения с цепи: tx_hash, output_index, amount_paid, текущие confirmations и seen_at. Пригодятся для саппорта и чтобы заметить частичные или повторные переводы.

Окна уникальных сумм (window_low / window_high)

На сетях, где кошельки не умеют выводить свежий адрес на каждый инвойс (TRON, TON), используется один статический адрес, а инвойсы различаются суммой: каждому открытому инвойсу выделяется личный диапазон сумм. В варианте это выглядит так:

{
  "asset": "USDT",
  "network": "tron",
  "amount_crypto": "101.4",
  "window_low": "101.3",
  "window_high": "101.5"
}

Любой платёж внутри [window_low, window_high] соответствует этому инвойсу и ничему другому. Суммы соседних инвойсов разнесены так, что окна не пересекаются. На сетях с деривацией адресов (EVM, BTC) окна — null: совпадение идёт по адресу.

У вариантов на TON может быть и payment_url_extra — memo/tag, обязательный к переводу. Кошельки подставляют его из payment URI и QR; если вы строите свой платёжный флоу, перевод без memo безвозвратно теряется.

TTL

ttl_minutes при создании: по умолчанию 360, диапазон 10–4320. По истечении без единого наблюдения инвойс переходит в expired (варианты — тоже). Underpaid — это исход платежа, а не TTL: деньги пришли, но мало; принять, ждать доплаты или вернуть — решать вам; свяжитесь с клиентом по оставленному в metadata идентификатору.