Жизненный цикл инвойса
Стейт-машина инвойса: статусы, статусы вариантов, observations, окна уникальных сумм и какой вебхук когда срабатывает.
Инвойс — это небольшая стейт-машина. Знание точных состояний, кто их двигает и какой вебхук срабатывает на каждом переходе, делает логику исполнения заказа герметичной.
Состояния
pending ──► detecting ──► confirmed
│ │
│ └──► underpaid (пришло меньше приемлемого диапазона — терминальный)
├──► expired (TTL истёк, ничего не пришло)
└──► canceled (вы отменили до оплаты)
| Статус | Смысл | Терминальный? |
|---|---|---|
pending | Создан, ждёт платежа | нет |
detecting | Найдена совпадающая транзакция; ждём подтверждений | нет |
confirmed | Достигнут порог подтверждений — отгружайте | да |
underpaid | Платёж пришёл ниже приемлемого диапазона | да |
expired | TTL истёк, совпадающих транзакций не было | да |
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}/cancel | invoice.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 идентификатору.