Инвойсы
Инвойс — ожидание платежа на конкретную сумму в конкретном активе до дедлайна. Мерчант создаёт инвойс под свой заказ, получает адрес и показывает его пользователю вместе с суммой.
Активы
Что сервис принимает в боевой сети TRON. Адреса контрактов прочитаны в сети, а не взяты из статей (ADR-0026):
| Актив | Контракт | Знаков | Минимум | Выплаты |
|---|---|---|---|---|
USDT_TRON |
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
6 | 1 USDT | да |
USDC_TRON |
TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8 |
6 | 1 USDC | да |
TRX_TRON |
нативная монета | 6 | — | нет |
TRX_TRON платежом пока не считается: перевод TRX на адрес приёма записывается как unexpected_asset и инвойс не закрывает. Причина и условие для изменения — в том же ADR. Выплаты в TRX не появятся и после: суточные лимиты складывают суммы в одной учётной единице, что верно только для привязанных к доллару активов.
В тестовых сетях принимается только USDT: проверяемого контракта USDC в Nile и Shasta нет — см. тестовые сети.
Единственный источник знаков, минимумов и числа подтверждений — таблица assets; в коде эти значения не зашиты нигде, поэтому добавление актива не меняет ни одной строки логики.
Создать инвойс
POST /v1/invoices
Запрос
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
order_id |
string | да | Идентификатор заказа мерчанта, уникальный в пределах мерчанта: 1–200 видимых ASCII-символов без пробелов. Ключ идемпотентности |
asset |
string | да | Актив, например USDT_TRON |
amount |
string | да | Сумма в единицах актива: "49.9". Не больше знаков после запятой, чем у актива, без знака, экспоненты и пробелов |
expires_in |
integer | да | Сколько секунд ждать платежа: от 60 до 604800 (7 дней) |
external_user_id |
string | в режиме per_user |
Пользователь мерчанта. В режиме per_user инвойс получает его постоянный адрес. В режиме per_invoice поле только сохраняется |
return_url |
string | нет | Куда страница оплаты вернёт покупателя после оплаты. Переопределяет адрес мерчанта и допускается только с тем же origin, что у него; https, до 2000 символов, без @, пробелов и параметров invoice_id, order_id, status, ts, signature |
cancel_url |
string | нет | Куда страница оплаты вернёт покупателя, если срок вышел без полной суммы. Те же правила, что у return_url, относительно адреса отмены мерчанта |
Сумма должна быть не меньше минимального депозита актива: для USDT_TRON это 1 USDT. Переводы меньше минимального депозита сервис считает пылью и не зачисляет.
Ответ
201 — инвойс создан, 200 — инвойс для этого order_id уже был создан раньше. Тело в обоих случаях одинаковое:
| Поле | Тип | Описание |
|---|---|---|
invoice_id |
string (UUID) | Идентификатор инвойса |
order_id |
string | Идентификатор заказа мерчанта |
status |
string | Статус, см. ниже |
asset |
string | Актив |
network |
string | Сеть актива: tron |
address |
string | Адрес для платежа, целиком |
amount_requested |
string | Цена мерчанта — amount запроса со всеми знаками актива: "49.900000" |
amount_expected |
string | Сумма, которую должен заплатить покупатель: цена и комиссия сервиса, если её платит покупатель — см. комиссии |
amount_received |
string | Сумма подтверждённых платежей |
amount_credited |
string | Подтверждённые платежи за вычетом удержанных комиссий: сколько зачислено мерчанту |
fees |
object | Комиссия сервиса и комиссия сети — см. комиссии |
external_user_id |
string или null | Пользователь мерчанта |
return_url |
string или null | Переопределение адреса возврата, как его прислал мерчант; null — действует адрес мерчанта |
cancel_url |
string или null | Переопределение адреса отмены, как его прислал мерчант; null — действует адрес мерчанта |
checkout_url |
string или null | Страница оплаты этого инвойса; null, если оператор её не настроил |
expires_at |
string | Дедлайн, ISO 8601 в UTC |
created_at |
string | Время создания, ISO 8601 в UTC |
deposits |
array | Платежи по инвойсу, см. получить инвойс |
Идемпотентность
Повторный запрос с тем же order_id, активом, суммой, external_user_id, return_url и cancel_url не создаёт второй инвойс и не возвращает ошибку. Ответом будет уже существующий инвойс с кодом 200. Поэтому запрос можно безопасно повторять после таймаута. Одновременные запросы с одним order_id тоже создают один инвойс.
Сумма повтора сравнивается с ценой amount_requested, а не с amount_expected: повтор присылает ту же цену, а комиссия, добавленная к ней при создании, — условие уже выставленного инвойса. Ответ на повтор показывает инвойс с условиями, записанными при создании, даже если ставка с тех пор изменилась.
Запрос с тем же order_id, но другой суммой, активом, пользователем или адресами возврата отклоняется с кодом 409 order_id_conflict: это другой платёж, а не повтор. Повтор принятого запроса возвращает инвойс, даже если оператор с тех пор сменил адреса мерчанта: переопределения проверяются при создании.
Ошибки
| HTTP | error.code |
Когда |
|---|---|---|
| 400 | validation_failed |
Поле отсутствует, лишнее или неверного формата; у суммы слишком много знаков после запятой; в режиме per_user нет external_user_id |
| 401 | unauthorized |
Нет ключа API или он неверный |
| 403 | merchant_disabled |
Мерчант отключён |
| 403 | merchant_not_approved |
Проект ещё не прошёл проверку или отклонён. Повтор запроса с order_id уже созданного инвойса по-прежнему возвращает инвойс |
| 409 | order_id_conflict |
Инвойс с этим order_id уже есть и описывает другой платёж |
| 422 | redirect_url_not_allowed |
return_url или cancel_url указаны, но у мерчанта нет адреса того же вида или origin отличается от него |
| 422 | asset_not_supported |
Актив неизвестен или не принимается к оплате |
| 422 | asset_payments_disabled |
Оператор временно выключил приём в этом активе. Повтор запроса с order_id уже созданного инвойса по-прежнему возвращает инвойс |
| 422 | asset_disabled_by_merchant |
Приём в этом активе выключен в настройках проекта. Повтор запроса с order_id уже созданного инвойса по-прежнему возвращает инвойс |
| 422 | amount_below_minimum |
Сумма меньше минимального депозита актива |
| 429 | rate_limited |
Превышен лимит запросов |
| 503 | addresses_exhausted |
Индексы деривации закончились. Практически недостижимо |
| 503 | network_fee_quote_unavailable |
В настройках проекта комиссию сети платит покупатель, а посчитать её сейчас нельзя: инвойс не создаётся. Повтор запроса с order_id уже созданного инвойса по-прежнему возвращает инвойс |
Пример
curl -s -X POST https://api.stons.io/v1/invoices \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"order_id": "ord_1001", "asset": "USDT_TRON", "amount": "49.9", "expires_in": 3600}'
{
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"order_id": "ord_1001",
"status": "created",
"asset": "USDT_TRON",
"network": "tron",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"amount_requested": "49.900000",
"amount_expected": "49.900000",
"amount_received": "0.000000",
"amount_credited": "0.000000",
"fees": {
"service": { "payer": "merchant", "percent": "1.00", "amount": "0.000000" },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "pending" }
},
"external_user_id": null,
"return_url": null,
"cancel_url": null,
"checkout_url": null,
"expires_at": "2026-09-11T21:03:05.531Z",
"created_at": "2026-09-11T20:03:05.531Z",
"deposits": []
}
Получить инвойс
GET /v1/invoices/{id} возвращает текущее состояние инвойса в том же формате, что и ответ на создание. Именно по этому ответу, а не по телу вебхука, решается, отдавать ли товар — см. вебхуки.
Инвойс другого мерчанта для вызывающего не существует: ответ 404 invoice_not_found. Идентификатор не в формате UUID — 400 validation_failed.
Поля элемента deposits:
| Поле | Тип | Описание |
|---|---|---|
tx_hash |
string | Хэш транзакции |
amount |
string | Сумма перевода в единицах актива |
confirmations |
integer | Сколько блоков набрано над блоком перевода |
status |
string | seen — перевод в блоке, подтверждений недостаточно; confirmed — подтверждений достаточно |
block_time |
string | Время блока с переводом, ISO 8601 в UTC |
late |
boolean | true, если блок с переводом новее дедлайна инвойса |
В deposits попадают только платежи. Пыль, переводы другого актива и переводы, исчезнувшие при реорганизации цепи, в список не входят.
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44 -H "X-Api-Key: $API_KEY"
Статусы
Схема статусов и её обоснование — в ADR-0006.
| Статус | Значение | Что делать мерчанту |
|---|---|---|
created |
Инвойс создан, переводов нет | Ждать |
seen |
До дедлайна пришёл перевод, но подтверждённая сумма меньше ожидаемой | Ждать: перевод ещё можно откатить |
confirmed |
Подтверждённые переводы до дедлайна покрывают сумму. Переплата — тоже confirmed, см. amount_received |
Отдать товар |
underpaid |
Дедлайн прошёл, подтверждено больше нуля, но меньше ожидаемого | Решить самому: доплата, частичная выдача, возврат через поддержку |
expired |
Дедлайн прошёл, подтверждённых переводов нет | Закрыть заказ |
paid_late |
После expired или underpaid пришёл и подтвердился новый перевод |
Решить самому |
settled |
Средства инвойса перенесены в хранилище | Ничего: для мерчанта это то же, что confirmed |
stateDiagram-v2
[*] --> created
created --> seen: перевод в блоке до дедлайна
seen --> created: перевод исчез при реорганизации
seen --> confirmed: подтверждено не меньше ожидаемого
created --> confirmed: подтверждено не меньше ожидаемого
seen --> underpaid: дедлайн, подтверждено меньше ожидаемого
created --> expired: дедлайн без переводов
underpaid --> paid_late: подтверждён поздний перевод
expired --> paid_late: подтверждён поздний перевод
confirmed --> settled: средства в хранилище
Правила:
- Вовремя пришёл перевод или нет, решает время блока, в который он попал, а не момент подтверждения. Перевод, отправленный за минуту до дедлайна, считается своевременным, даже если подтверждения наберутся позже.
- Инвойс становится
underpaidилиexpiredтолько после того, как сервис просканировал блоки после дедлайна и все своевременные переводы подтвердились. Поэтому инвойс может оставаться вcreatedилиseenнемного дольше дедлайна. - До дедлайна переводы на адрес инвойса суммируются: первый перевод не считается окончательным.
- Переплата не отдельный статус: инвойс становится
confirmed, аamount_receivedбольшеamount_expected. - Перевод, исчезнувший из цепи при реорганизации, перестаёт учитываться: инвойс без других переводов возвращается в
created, мерчант получаетdeposit.orphaned. Если перевод затем снова попадает в цепь, он учитывается заново. В этом редком случае инвойс может выйти изexpiredилиunderpaidобратно вseen.
Статусы меняет сканер блокчейна. Он читает блоки с отставанием в один блок от головы цепи, поэтому перевод виден через несколько секунд. Подтверждение USDT в TRON — 20 блоков, около минуты. О каждой смене статуса мерчант получает вебхук.
Режим per_user
В режиме per_user у пользователя один постоянный адрес. Перевод закрывает самый ранний открытый инвойс этого пользователя в том же активе на точно такую же сумму. Перевод на другую сумму — это пополнение баланса, а не частичная оплата. Поэтому в этом режиме не бывает статусов underpaid и paid_late, а перевод на точную сумму после дедлайна тоже считается пополнением.
Сумма, с которой сравнивается перевод, — amount_expected: если комиссию сервиса платит покупатель, это цена вместе с комиссией.
Комиссии
С каждого подтверждённого платежа STONS удерживает комиссию сервиса — процент, который назначает оператор. Кто её платит — мерчант или покупатель, — задаёт настройка проекта. Комиссия сети по инвойсам пока не начисляется: её расчёт по каждой операции появится отдельным обновлением, и поля fees.network до тех пор пустые.
Ставка и плательщики записываются в инвойс при его создании и дальше не меняются: смена тарифа или настроек проекта касается только новых инвойсов. Инвойсы, созданные до появления комиссии, записаны со ставкой 0.
Поля
| Поле | Описание |
|---|---|
fees.service.payer |
merchant — комиссия удерживается из полученного; customer — комиссия добавлена к цене и входит в amount_expected |
fees.service.percent |
Ставка, записанная в инвойс, в процентах с двумя знаками: "1.00" |
fees.service.amount |
Комиссия, удержанная с подтверждённых платежей инвойса на сейчас. До первого подтверждения — ноль |
fees.network.payer |
Кто несёт комиссию сети. Сейчас всегда merchant |
fees.network.quote |
Котировка комиссии сети, которую платит покупатель. Сейчас null |
fees.network.quote_rate |
Курс, по которому посчитана котировка. Сейчас null |
fees.network.actual |
Комиссия сети по факту операций. Сейчас null: сетевая комиссия по инвойсам ещё не начисляется |
fees.network.status |
quoted — котировка зафиксирована; pending — рассчитается по факту операции; settled — проведена. Сейчас всегда pending |
amount_credited |
amount_received минус fees.service.amount: сколько из полученного зачислено мерчанту |
Как считается комиссия сервиса
- Покупатель платит комиссию. К цене прибавляется ставка от цены, округлённая вверх до последнего знака актива:
amount_expected = amount_requested + ⌈amount_requested × ставка⌉. Инвойс на 100 USDT при 1% ждёт 101 USDT. - Удерживается при подтверждении каждого платежа, в той же операции, что его зачисление, и считается от суммы всех подтверждённых платежей инвойса:
- платит мерчант —
⌊получено × ставка⌋; - платит покупатель —
⌊получено × ставка / (1 + ставка)⌋, доля комиссии внутри уже оплаченной суммы.
- платит мерчант —
- Округление вниз, в пользу мерчанта. Инвойс, оплаченный несколькими переводами, удерживает столько же, сколько оплаченный одним: каждый следующий перевод удерживает разницу между комиссией с новой суммы и уже удержанной.
- Переплата — комиссия с большей суммы. Недоплата — с полученного. Платёж после срока — как любой подтверждённый.
- Пыль и переводы другого актива комиссии не несут: это не платежи. Перевод, исчезнувший при реорганизации до подтверждения, не зачислен и комиссии не несёт.
- Пополнение
per_userбез инвойса — комиссия с суммы перевода по ставке мерчанта на момент блока перевода, платит мерчант. Её видно в балансе проекта; событияdeposit.*полей комиссий не несут.
Пример: цена 100 USDT, ставка 1%, комиссию платит покупатель.
| Момент | amount_requested |
amount_expected |
amount_received |
fees.service.amount |
amount_credited |
|---|---|---|---|---|---|
| Создан | 100.000000 |
101.000000 |
0.000000 |
0.000000 |
0.000000 |
| Оплачен | 100.000000 |
101.000000 |
101.000000 |
1.000000 |
100.000000 |
Интеграция сравнивает свою цену с amount_requested, а покупателю показывает amount_expected: только эта сумма закрывает инвойс.
Суммы
Суммы передаются строками в единицах актива, например "49.900000". JSON-числа для сумм не используются: при разборе в число с плавающей точкой копейки теряются. Число знаков после запятой у каждого актива своё: у USDT в TRON — 6, у USDT в BNB Chain — 18.