Перейти к содержанию
STONS
Разделы документации

Инвойсы

Инвойс — ожидание платежа на конкретную сумму в конкретном активе до дедлайна. Мерчант создаёт инвойс под свой заказ, получает адрес и показывает его пользователю вместе с суммой.

Активы

Что сервис принимает в боевой сети 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.