Перейти к содержанию
STONS

Инвойсы

Этот документ описывает инвойсы: как создать инвойс через POST /v1/invoices, получить его состояние через GET /v1/invoices/{id} и понимать его статусы, суммы и комиссии. Прочитайте его до приёма первого платежа.

Инвойс — ожидание платежа на конкретную сумму в конкретном активе до дедлайна. Мерчант (в кабинете — проект) создаёт инвойс под заказ и либо сам показывает покупателю сумму и адрес, либо отправляет покупателя на страницу оплаты. Покупатель — тот, кто платит. Все времена в запросах и ответах — ISO 8601 в UTC, например 2026-09-11T21:03:05.531Z.

Активы

Что сервис принимает. Адреса контрактов прочитаны в сети, а не взяты из статей. Какие из этих активов доступны вашему проекту прямо сейчас, говорит GET /v1/assets: оператор может выключить актив или сеть, например на обслуживание, и тогда их в ответе не будет.

Активы TRON

Актив Контракт Знаков Минимальный депозит Подтверждений Выплаты
USDT_TRON TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t 6 1 USDT 20 да
USDC_TRON TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8 6 1 USDC 20 выключены оператором
TRX_TRON нативная монета 6 1 TRX 20 нет

USDC_TRON выключен оператором целиком: Circle прекратила выпуск USDC в сети TRON. Инвойс в USDC_TRON и инвойс с USDC_TRON в currencies отклоняются с 422 asset_payments_disabled; страница оплаты его не предлагает, в assets постоянного адреса его нет; выплата в нём отклоняется с 422 asset_payouts_disabled. Уже открытые инвойсы в нём оплачиваются, а перевод USDC на постоянный адрес per_user зачисляется, как раньше, — см. что приходит на адрес.

Активы EVM-сетей

Сеть Актив Контракт Знаков Минимальный депозит Подтверждений
ethereum USDT_ETHEREUM 0xdac17f958d2ee523a2206206994597c13d831ec7 6 1 USDT 96
ethereum USDC_ETHEREUM 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 6 1 USDC 96
ethereum ETH_ETHEREUM нативная монета 18 0,0001 ETH 96
bsc USDT_BSC 0x55d398326f99059ff775485246999027b3197955 18 1 USDT 400
bsc USDC_BSC 0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d 18 1 USDC 400
bsc BNB_BSC нативная монета 18 0,0001 BNB 400
polygon USDT0_POLYGON 0xc2132d05d31c914a87c6611c10748aeb04b58e8f 6 1 USDT0 4
polygon USDC_POLYGON 0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 6 1 USDC 4
polygon POL_POLYGON нативная монета 18 0,0001 POL 4
base USDC_BASE 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 6 1 USDC 600
base ETH_BASE нативная монета 18 0,0001 ETH 600
arbitrum USDT0_ARBITRUM 0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 6 1 USDT0 4800
arbitrum USDC_ARBITRUM 0xaf88d065e77c8cc2239327c5edb3a432268e5831 6 1 USDC 4800
arbitrum ETH_ARBITRUM нативная монета 18 0,0001 ETH 4800
optimism USDT0_OPTIMISM 0x01bff41798a0bcf287b996046ca68b395dbc1071 6 1 USDT0 900
optimism USDC_OPTIMISM 0x0b2c639c533813f4aa9d7837caf62653d097ff85 6 1 USDC 900
optimism ETH_OPTIMISM нативная монета 18 0,0001 ETH 900
avalanche USDT_AVALANCHE 0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7 6 1 USDT 1
avalanche USDC_AVALANCHE 0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e 6 1 USDC 1
avalanche AVAX_AVALANCHE нативная монета 18 0,0001 AVAX 1
  • Выплат в EVM-сетях нет: выплата в любом из этих активов отклоняется с 422 asset_not_payable.
  • USDT в Polygon, Arbitrum и OP Mainnet — это USDT0, отдельный контракт с собственным идентификатором актива. Для покупателя это тот же USDT, для вашего кода — другой asset.
  • Знаки после запятой разные: у USDT и USDC в BNB Smart Chain их 18, в остальных сетях — 6. Не вшивайте это число в код.
  • Число подтверждений — глубина, на которой сервис считает платёж окончательным в этой сети. Оно разное: от 1 в Avalanche до 4800 в Arbitrum, где блоки идут примерно каждые 0,25 секунды.

Активы TON

Контракт — мастер-контракт жетона в raw-форме, в которой его хранит каталог; тот же контракт в user-friendly форме — EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs:

Актив Контракт Знаков Минимальный депозит Подтверждений Выплаты
USDT_TON 0:b113a994b5024a16719f69139328eb759596c38a25f59028b146fecdc3621dfe 6 1 USDT 1 нет
GRAM_TON нативная монета 9 0,05 GRAM 1 нет

Платежу в TON нужно одно подтверждение: блок мастерчейна TON выходит примерно раз в 0,4 секунды, а сервис считает только транзакции, которые сеть сделала окончательными, поэтому платёж подтверждается за секунды. Адрес инвойса в TON — в user-friendly форме, см. адреса в TON.

Активы Solana

Контракт — mint токена, в base58 и регистрозависимо. Оба mint принадлежат классической токен-программе SPL TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA:

Актив Контракт Знаков Минимальный депозит Подтверждений Выплаты
USDT_SOLANA Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB 6 1 USDT 1 нет
USDC_SOLANA EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 6 1 USDC 1 нет
SOL_SOLANA нативная монета 9 0,01 SOL 1 нет
  • Выплат в Solana нет: выплата в любом из этих активов отклоняется с 422 asset_not_payable.
  • Платежу в Solana нужно одно подтверждение: сервис считает только то, что сеть сделала окончательным на настроенном оператором уровне подтверждения. По умолчанию это finalized — единственный уровень, который не откатывается.
  • У Solana нет chain id. Сеть этого семейства различается по genesis-хэшу, а не по номеру. Идентификаторы, которые видит ваш код, — это id сетей из реестра: solana для боевой сети.
  • Адрес и подпись транзакции в Solana — base58 и регистрозависимы. Подпись, которую сервис возвращает в tx_hash, — это 86–88 знаков base58, а не 64 шестнадцатеричных знака. Никогда не приводите ни адрес, ни подпись к нижнему регистру — см. адреса в Solana.
  • Токен в Solana лежит не на адресе владельца, а в его associated token account — отдельном аккаунте на пару (владелец, mint). Этот аккаунт сервис создаёт сам при выдаче адреса, поэтому кошелёк покупателя платит на адрес как обычно и знать о нём ничего не должен.

Нативные монеты

TRX_TRON, GRAM_TON, SOL_SOLANA и нативные монеты EVM-сетей оплачивают инвойсы наравне с токенами: цена в долларах США переводится в монету по курсу, как у любого актива с курсом, — см. цену и курс. Выплат в них нет: выплата отклоняется с 422 asset_not_payable.

Нативная монета EVM-сети засчитывается, отправил её кошелёк или смарт-контракт — суммой самой транзакции или переводом, который сделал внутри неё смарт-контракт (смарт-кошелёк, мультиподпись, вывод с биржи). Перевод из контракта сервис находит трейсом блока и засчитывает, только если трейс объясняет весь рост баланса адреса; перевод, который объяснить не удалось, автоматически не засчитывается: инвойс остаётся неоплаченным, вебхука нет, и его разбирает оператор вручную. Токен засчитывается по событию перевода, как бы его ни отправили.

Перевод монеты на адрес инвойса в другом активе записывается как unexpected_asset: мерчанту он не зачисляется и инвойс не закрывает — см. что приходит на адрес.

Тестовые сети

Установка в тестовой сети TRON (Nile или Shasta) принимает только USDT_TRON — с контрактом этой сети. Её сеть TON — TON testnet, и в ней только GRAM_TON: USDT выпущен только в боевой сети TON. Её сеть Solana — solanadevnet, Solana devnet, и в ней только USDC и нативная монета; идентификаторы активов и mint там свои — USDC в devnet — это 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU.

Правила, которые следуют из таблиц:

  • Перевод меньше минимального депозита своего актива — пыль: сервис его записывает, но мерчанту не зачисляет и ни в один инвойс не засчитывает. Исключение — остаток инвойса per_invoice: если по инвойсу уже пришёл платёж, а сумма ещё не набрана, перевод в активе инвойса на его адрес — платёж любой ненулевой суммы, см. частичная оплата.
  • Инвойс с ценой в долларах США (созданный без asset) оплачивается любым активом, который проект принимает и для которого у сервиса есть курс; currencies сужает этот список. Какие это активы в данный момент, говорит GET /v1/assets.
  • Число знаков, минимальные депозиты и число подтверждений берутся из каталога активов сервиса. В коде ни одно из этих чисел не зашито.

Суммы

Суммы передаются строками в единицах актива, например "49.900000". JSON-числа для сумм не используются: при разборе в число с плавающей точкой копейки теряются.

  • В запросе сумма — цифры с необязательной дробной частью через точку: 49, 49.9, 49.90, 0.5. Сумма отклоняется с 400 validation_failed, если в ней есть знак, экспонента, пробел, запятая, ведущий ноль (049), точка без цифр с обеих сторон (49., .9) или больше знаков после точки, чем у актива.
  • В ответе у каждой суммы в активе ровно столько знаков после точки, сколько у актива: "49.900000" для актива с 6 знаками. У цены в долларах США (price.amount) — всегда 6.
  • Число знаков. Не зашивайте число 6 в код: считайте знаки после точки в amount_expected — там всегда все знаки актива инвойса.
  • Сравнение. Сравнивайте суммы как десятичные числа, а не как строки: "49.9" в запросе и "49.900000" в ответе — одна и та же сумма.

Список активов

GET /v1/assets возвращает активы, в которых мерчант может выставить инвойс в момент запроса, с сетью, контрактом, числом знаков и минимальным депозитом каждого. По нему решают, какие токены предложить, вместо того чтобы зашивать список в код.

Запрос

У запроса нет параметров и тела. В ответе есть ETag, поэтому продукт, который держит свой список монет свежим, платит за один 304, пока каталог не менялся, — см. условные чтения.

Ответ

200 с массивом assets в порядке сетей, который задал оператор, затем по идентификатору. Актив попадает в список, если выполнено всё:

  • сервис принимает его для платежей, и оператор не выключил приём в нём;
  • его не выключили настройки проекта;
  • его сеть включена, и сервис сейчас выдаёт в ней адреса.
Поле Тип Описание
asset string Идентификатор актива: значение asset в POST /v1/invoices
symbol string Тикер токена, например USDT
network string Идентификатор сети, например tron
network_name string Название сети для людей, например TRON
token_standard string Как кошельки называют токены сети, например TRC-20
contract string или null Контракт токена в том виде, в каком его хранит каталог сервиса: в TON — мастер-контракт жетона в raw-форме, 0: и 64 шестнадцатеричные цифры; null у нативной монеты сети
decimals integer Число знаков актива: у каждой суммы в нём ровно столько знаков после точки
min_amount string Минимальный депозит со всеми знаками актива: перевод меньше — пыль, кроме остатка начатой оплаты (см. частичная оплата), и инвойс в активе не может быть меньше
confirmations integer Сколько подтверждений нужно переводу в этом активе, чтобы он был зачислен: по нему рисуется «2 из 20» на своей странице оплаты
icon string или null Метка актива в каталоге, для своего списка монет; null, если метки нет
  • Список говорит, что можно предложить сейчас, а не что зачисляется: перевод в активе, выпавшем из списка, обрабатывается по правилам своего адреса — см. что приходит на адрес.
  • Список не зависит от проверки мерчанта и от приостановленного режима адресов: они отклоняют сам POST /v1/invoices своими кодами.

Ошибки

HTTP error.code Когда
401 unauthorized Ключ API не передан или неверен — см. аутентификацию
403 merchant_disabled Мерчант выключен
429 rate_limited Превышен лимит запросов
500 internal_error Необработанная ошибка сервиса

Пример

curl -s https://api.stons.io/v1/assets \
  -H "X-Api-Key: $API_KEY"

200 в mainnet TRON:

{
  "assets": [
    {
      "asset": "USDT_TRON",
      "symbol": "USDT",
      "network": "tron",
      "network_name": "TRON",
      "token_standard": "TRC-20",
      "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
      "decimals": 6,
      "min_amount": "1.000000",
      "confirmations": 20,
      "icon": "usdt"
    }
  ]
}

Создать инвойс

POST /v1/invoices создаёт инвойс под заказ или возвращает инвойс, который уже есть для того же order_id, — см. идемпотентность.

Запрос

Тело — JSON-объект с Content-Type: application/json, не больше 16 КиБ. Поле, которого нет в таблице, отклоняется с 400 validation_failed.

У запроса есть один параметр строки запроса — include, с единственным значением qr: тогда в ответе есть QR-код адреса. Любой другой параметр и любое другое значение отклоняются с 400 validation_failed.

Поле Тип Обязательно Описание
order_id string да Идентификатор заказа мерчанта, уникальный в пределах мерчанта: 1–200 видимых ASCII-символов (0x21–0x7E) без пробелов. Ключ идемпотентности
asset string нет Идентификатор актива из таблицы, например USDT_TRON. Без asset токен выбирает покупатель на странице оплаты, а amount — цена в долларах США: см. токен выбирает покупатель
price_currency string нет Только вместе с asset, единственное значение — USD. С ним amount — это цена в долларах, а не количество монет: сервис сам пересчитает её в названный актив по своему курсу и вернёт готовый к оплате инвойс — см. цена в долларах в выбранной монете
amount string да С asset — сумма в единицах актива, не меньше его минимального депозита и не больше знаков после точки, чем у актива. С asset и price_currency — цена в долларах США. Без asset — тоже цена в долларах США. Цена: больше нуля, не больше 6 знаков после точки. Формат — см. суммы
currencies array of string нет Только без asset: активы, из которых выбирает покупатель, от 1 до 50 идентификаторов; порядок и повторы значения не имеют. Без поля покупатель выбирает из всех активов, которые мерчант и сервис принимают в момент выбора
expires_in integer нет Секунд от создания до дедлайна: от 300 (5 минут) до 259200 (72 часа). По умолчанию 3600 (1 час). JSON-число, не строка
external_user_id string в режиме per_user Пользователь мерчанта, в том же формате, что order_id. В режиме per_user инвойс оплачивается на постоянный адрес этого пользователя. В режиме per_invoice поле только сохраняется и возвращается
return_url string нет Куда страница оплаты отправит покупателя после оплаты — вместо адреса возврата мерчанта. Только https://, не длиннее 2000 символов, без @, пробелов и параметров invoice_id, order_id, status, ts, signature, и на сайте мерчанта — см. адреса возврата
cancel_url string нет Куда ведёт кнопка «Вернуться в магазин» на странице оплаты, пока инвойс не оплачен, и куда страница отправит покупателя, если срок вышел без полной суммы, — вместо адреса отмены мерчанта. Те же правила, что у return_url

Необязательное поле без значения не передаётся: null отклоняется с 400 validation_failed.

Ответ

201 — инвойс создан, 200 — инвойс для этого order_id уже был создан раньше. Тело в обоих случаях одинаковое. Все поля присутствуют всегда; поле, которое не применимо, равно null.

Поле Тип Описание
invoice_id string (UUID) Идентификатор инвойса
order_id string order_id запроса
status string created, seen, partially_paid, confirmed, underpaid, expired, paid_late или settled — см. статусы
asset string или null Идентификатор актива; null, пока покупатель не выбрал токен
network string или null Идентификатор сети актива, например tron; null, пока покупатель не выбрал токен
network_name string или null Название сети для людей из реестра сетей сервиса, например TRON; null, пока покупатель не выбрал токен
address string или null Адрес для платежа, целиком: в TRON — base58, в EVM-сетях — 0x и 40 шестнадцатеричных цифр в нижнем регистре, в TON — user-friendly форма, начинается с UQ (0Q в тестовой сети), см. адреса в TON. null, пока покупатель не выбрал токен
address_explorer_url string или null Страница address в обозревателе сети; null, пока покупатель не выбрал токен или у сети нет ссылки на обозреватель
payment_uri string или null Ссылка, открывающая кошелёк с заполненным переводом, — для своей страницы оплаты. null, если у сети нет ссылки, которой сервис доверяет, и null, пока точная сумма не запрошена: инвойс ждёт денег (created или partially_paid с остатком), дедлайн не прошёл и котировка держится. Сейчас ссылка есть только у TON
qr object Только при include=qr: address — QR-код адреса оплаты, SVG-картинка в data:-ссылке, или null, пока покупатель не выбрал токен, — см. QR-код. Без include=qr поля нет
price object или null У инвойса с ценой в долларах — созданного без asset или с price_currency: amount — цена в долларах США с 6 знаками после точки, currency — USD. null у инвойса, сумма которого задана в монетах
currencies array of string или null currencies запроса, отсортированные и без повторов; null, если в запросе поля не было
asset_selected_at string или null Когда у инвойса появился токен: покупатель выбрал его, или сервис выбрал названный вами при создании с price_currency. null у инвойса, сумма которого задана в монетах, и пока выбора не было
quote object или null У инвойса с ценой в долларах, после того как токен определён: курс, по которому посчитаны суммы в токене, и до какого момента они действуют, — см. котировку. У инвойса с price_currency приходит сразу в ответе на создание. null у инвойса, сумма которого задана в монетах
amount_requested string или null Цена мерчанта в единицах актива: amount запроса или цена, переведённая в выбранный токен по курсу; null, пока покупатель не выбрал токен
amount_expected string или null Сумма, которую должен заплатить покупатель: amount_requested плюс комиссия сервиса, если её платит покупатель, — см. комиссии. Только эта сумма закрывает инвойс. У инвойса, созданного без asset, может измениться после окна котировки — см. окно котировки. null, пока покупатель не выбрал токен
amount_received string или null Сумма подтверждённых платежей — вовремя и с опозданием; null, пока покупатель не выбрал токен
amount_remaining string или null amount_expected минус amount_received, но не меньше нуля: сколько ещё прислать на адрес. Неподтверждённые переводы не вычитаются. После дедлайна перевод остатка не делает инвойс confirmed — см. частичную оплату. null, пока покупатель не выбрал токен
amount_credited string или null amount_received минус fees.service.amount: сколько зачислено мерчанту; null, пока покупатель не выбрал токен
fees object Комиссия сервиса и строка комиссии сети — см. комиссии
external_user_id string или null external_user_id запроса; null, если в запросе поля не было
return_url string или null return_url запроса, как он прислан; null — действует адрес возврата мерчанта
cancel_url string или null cancel_url запроса, как он прислан; null — действует адрес отмены мерчанта
checkout_url string или null Страница оплаты этого инвойса; null, если в установке нет страницы оплаты
expires_at string Дедлайн: время создания плюс expires_in
created_at string Время создания
deposits array Платежи по инвойсу — см. получить инвойс

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

order_id — ключ идемпотентности. Когда приходит запрос с order_id, для которого инвойс уже есть, сервис сравнивает запрос с этим инвойсом:

  • Тот же запрос получает 200 и текущее состояние инвойса. Запрос тот же, если совпадает всё перечисленное:
    • у инвойса, созданного с asset: тот же asset и тот же amount как число — он сравнивается с amount_requested, а не с amount_expected;
    • у инвойса, созданного без asset: нет asset, та же цена как число — она сравнивается с price.amount — и тот же набор currencies без учёта порядка и повторов. Так же и после того, как покупатель выбрал токен;
    • в обоих случаях те же external_user_id, return_url и cancel_url; отсутствующее поле совпадает только с отсутствующим.
  • Любое отличие получает 409 order_id_conflict: это другой платёж. Запрос с asset для инвойса, созданного без него, и наоборот — тоже отличие.
  • expires_in не сравнивается. Повтор с другим значением получает инвойс с прежним expires_at.

Одновременные запросы с одним order_id создают один инвойс; остальные получают 200 или 409 по тому же сравнению. Ответ на повтор показывает условия, записанные при создании инвойса, даже если ставка комиссии или настройки мерчанта с тех пор изменились.

Часть проверок выполняется до поиска существующего инвойса, поэтому повтор, который их не проходит, получает ошибку, а не инвойс:

  • формат запроса, включая границы expires_in (400 validation_failed);
  • external_user_id в режиме per_user (400 validation_failed);
  • с asset: актив принимается к оплате (422 asset_not_supported), формат и число знаков суммы (400 validation_failed), минимальный депозит (422 amount_below_minimum);
  • без asset: формат цены и то, что она больше нуля (400 validation_failed).

Все остальные проверки выполняются, только когда инвойса для order_id ещё нет, поэтому повтор уже созданного инвойса они не отклоняют. Это проверки: одобрение мерчанта, пауза режима адресов платформой, выключатели актива у оператора и у мерчанта, проверки currencies и курсов, адреса возврата и настройки комиссий.

Ошибки

HTTP error.code Когда
400 validation_failed Тело не соответствует таблице запроса: поле отсутствует, лишнее, null, неверного типа или формата; expires_in вне границ; currencies передан вместе с asset. Или: у amount больше знаков после точки, чем у актива; цена равна нулю или у неё больше 6 знаков после точки; в режиме per_user нет external_user_id; сумма вместе с комиссией сервиса длиннее 78 цифр в минимальных единицах
400, 413, 415 bad_request Тело — не JSON или пустое (400), больше 16 КиБ (413) или с неподдерживаемым Content-Type (415)
401 unauthorized Нет ключа API или он неверный — см. аутентификацию
403 merchant_disabled Мерчант отключён
403 merchant_not_approved Мерчант ещё не одобрен после проверки или отклонён
409 order_id_conflict Инвойс с этим order_id есть и описывает другой платёж
422 address_mode_disabled Платформа временно приостановила режим адресов мерчанта — см. режим на паузе
422 asset_not_supported asset или актив из currencies неизвестен или не принимается к оплате
422 amount_below_minimum С asset — amount меньше минимального депозита актива. Без asset — цена, переведённая по текущему курсу, меньше минимального депозита разрешённого токена
422 asset_payments_disabled Оператор временно выключил приём в asset или в активе из currencies
422 asset_disabled_by_merchant Приём в asset или в активе из currencies выключен в настройках проекта
422 asset_required Нет asset, а в установке нет страницы оплаты, где покупатель выбрал бы токен
422 no_payable_assets Нет ни asset, ни currencies, а мерчант сейчас не принимает ни одного актива, у которого есть курс
422 asset_not_offered У актива из currencies нет курса, поэтому его нельзя предложить для цены в долларах США
422 redirect_url_not_allowed return_url или cancel_url не на сайте мерчанта — см. адреса возврата
429 rate_limited Превышен лимит запросов
500 internal_error Необработанная ошибка сервиса
503 addresses_exhausted Закончились индексы деривации адресов: заняты все 2^31. Практически недостижимо
503 network_fee_quote_unavailable У проекта осталась прежняя настройка «комиссию сети платит покупатель». Владельцу нужно сохранить «Кто платит комиссии» в настройках проекта в кабинете
503 rate_unavailable Без asset: ни у одного разрешённого токена сейчас нет свежего курса, и оплатить инвойс никто не смог бы. Инвойс не создаётся
503 service_unavailable Сервис не может вывести новый адрес в текущей конфигурации; сообщение называет причину

Что делать клиенту при каждом коде — см. коды ошибок.

Пример

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}'

201:

{
  "invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
  "order_id": "ord_1001",
  "status": "created",
  "asset": "USDT_TRON",
  "network": "tron",
  "network_name": "TRON",
  "address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "payment_uri": null,
  "price": null,
  "currencies": null,
  "asset_selected_at": null,
  "quote": null,
  "amount_requested": "49.900000",
  "amount_expected": "49.900000",
  "amount_received": "0.000000",
  "amount_remaining": "49.900000",
  "amount_credited": "0.000000",
  "fees": {
    "service": { "payer": "merchant", "percent": "1.00", "fixed": "0.000000", "amount": "0.000000" },
    "network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
  },
  "external_user_id": null,
  "return_url": null,
  "cancel_url": null,
  "checkout_url": "https://pay.example.com/pay/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
  "expires_at": "2026-09-11T21:03:05.531Z",
  "created_at": "2026-09-11T20:03:05.531Z",
  "deposits": []
}

Цена в долларах в выбранной монете

Самый короткий путь, если вы держите цены в долларах и уже знаете, в чём хотите получить деньги: один запрос, и в ответе всё для оплаты. Курс и пересчёт делает сервис.

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_2001", "asset": "SOL_SOLANA", "amount": "99", "price_currency": "USD", "expires_in": 3600}'

price_currency: "USD" означает, что amount — цена в долларах, а не количество монет. Поле необязательное: без него amount по-прежнему сумма в единицах актива, и ничего в вашей интеграции не меняется.

В ответе сразу то, что нужно показать покупателю:

  • address — адрес, на который платить;
  • amount_expected — сколько монет отправить;
  • price — цена в долларах, как вы её прислали;
  • quote.rate — курс, по которому посчитана сумма, и quote.expires_at — до какого момента она держится;
  • expires_at — дедлайн инвойса; qr — при include=qr.

Страница оплаты такому инвойсу не нужна: монету выбрали вы.

Что с курсом дальше

Сумма зафиксирована котировкой. В токене, привязанном к доллару США, например USDT_TRON, она действует до дедлайна инвойса и не пересчитывается. В любом другом токене, когда окно котировки истекает, а инвойс всё ещё ждёт денег, сервис пересчитывает остаток по новому курсу: уже полученное остаётся полученным, а недоплата считается в долларах по курсу инвойса. Подробности — в окне котировки.

Если курса нет или он устарел, инвойс не создаётся: ответ 503 rate_unavailable, и в базе ничего не остаётся. Лучше честный отказ, чем инвойс без адреса и суммы.

Повтор с тем же order_id возвращает тот же инвойс с той же суммой — как и для любого другого инвойса.

Показать «≈ 0,45 SOL» до создания

GET /v1/rates отдаёт по каждому активу проекта курс, по которому и будет посчитан инвойс: рыночный с уже снятой наценкой сервиса. Поэтому число, которое вы покажете покупателю заранее, совпадёт с тем, что встанет в инвойс.

curl -s https://api.stons.io/v1/rates -H "X-Api-Key: $API_KEY"
{
  "rates": [
    {
      "asset": "SOL_SOLANA",
      "symbol": "SOL",
      "rate": "213.450000000000000000",
      "market_rate": "220.050000000000000000",
      "markdown_percent": "3.00",
      "observed_at": "2026-10-06T12:31:07.218Z",
      "fresh": true,
      "currency": "USD"
    }
  ]
}
Поле Что значит
rate Долларов за одну монету по курсу, который применит инвойс: рыночный минус наценка сервиса. null, если курс этой монеты сервис ещё ни разу не принял
market_rate Тот же курс до наценки
markdown_percent Наценка на нестабильную монету; 0.00 у монеты, привязанной к доллару
observed_at Когда курс наблюдали
fresh false — курс устарел: он показан, но инвойс в этой монете сейчас отклонят с 503 rate_unavailable

Лента изменений

GET /v1/invoices отдаёт инвойсы проекта в том порядке, в каком менялось их состояние, от самого старого изменения к новому. Это догон: вебхук приходит за секунды и остаётся основным путём, а лента нужна, когда приёмник лежал дольше, чем покрывают семь доставок.

Запрос

Параметр Значение
updated_since Момент в формате ISO 8601; отдаются инвойсы, состояние которых изменилось в этот момент или позже. Без него лента идёт с самого первого инвойса проекта
status Отдавать только инвойсы в этом статусе
limit Сколько инвойсов на странице: от 1 до 100, по умолчанию 50
cursor Значение next_cursor предыдущей страницы

Любой другой параметр строки запроса отклоняется с 400 validation_failed; include — в их числе, поэтому в ленте никогда нет QR-кодов. В ответе есть ETag: проход, который не нашёл нового, стоит одного 304 без тела — см. условные чтения.

Ответ

Поле Тип Значение
invoices array Инвойсы той же формы, что отдаёт GET /v1/invoices/{id}, без поля deposits и без qr
next_cursor string или null Передайте его как cursor, чтобы получить следующую страницу; null — страница последняя

Переводы в ленту не кладутся намеренно: страница из пятидесяти инвойсов несла бы сотни строк, о которых не просили, а сколько пришло, видно по суммам самого инвойса. Нужны переводы — возьмите инвойс по идентификатору.

Страница заканчивается на последней строке, и следующая начинается строго после неё. Поэтому инвойс, изменившийся во время обхода, не сдвигает страницу — в отличие от смещения, при котором строка может быть пропущена или прочитана дважды. Курсор, который сервис не выдавал, отклоняется с 400 validation_failed.

Как держать свои заказы в соответствии:

  1. Обрабатывайте вебхуки — это быстрый путь.
  2. Храните момент последнего обработанного изменения.
  3. Если вебхуки не приходили, пройдите ленту с этого момента, страница за страницей, пока next_cursor не станет null, и сдвиньте момент.

Токен выбирает покупатель

Запрос без asset создаёт инвойс без токена: покупатель выбирает монету и сеть на странице оплаты и видит сумму и адрес только после выбора.

  • Цена. amount — цена в долларах США. При выборе сервис переводит её в выбранный токен по курсу — см. цену и курс.
  • Страница оплаты. Она обязательна. Если в установке её нет, запрос отклоняется с 422 asset_required.
  • Разрешённые токены с currencies. При создании каждый актив из списка должен приниматься к оплате (422 asset_not_supported), быть включён оператором (422 asset_payments_disabled) и в настройках проекта (422 asset_disabled_by_merchant) и иметь курс (422 asset_not_offered). Цена, переведённая по текущему курсу, должна быть не меньше его минимального депозита (422 amount_below_minimum).
  • Разрешённые токены без currencies. При создании сервис берёт все активы, которые принимаются к оплате, включены оператором и в настройках проекта и имеют курс. Если таких нет — 422 no_payable_assets. Каждый взятый актив должен принимать цену, переведённую по текущему курсу, не меньше минимального депозита — с той же ошибкой, что выше.
  • Свежего курса при создании нет. Разрешённый актив, курс которого в этот момент не свежий, с минимальным депозитом не сверяется: его проверит страница оплаты, когда будет предлагать выбор. Если свежего курса нет ни у одного разрешённого актива, запрос отклоняется с 503 rate_unavailable, и инвойс не создаётся: оплатить его сейчас никто не смог бы.
  • Что предлагает страница. Из разрешённых токенов страница оплаты предлагает те, что проходят те же проверки в момент показа страницы, каждый с суммой по текущему курсу.
  • До выбора asset, network, address, quote, amount_requested, amount_expected, amount_received, amount_remaining, amount_credited, fees.service.fixed и fees.service.amount равны null, а в price — цена. Ставка комиссии и плательщики записываются при создании, как у любого инвойса.
  • Выбор возможен, пока статус created и часы сервиса не дошли до expires_at. Выбор окончательный: сменить токен нельзя. При выборе сервис записывает актив, выдаёт адрес, переводит цену в токен и записывает первую котировку. amount_expected он считает от переведённой цены так же, как у инвойса, созданного в этом активе: со ставкой и плательщиками, записанными при создании, и фиксированной частью комиссии этого актива на момент выбора. asset_selected_at — момент выбора. Дальше инвойс оплачивается как инвойс, созданный с этим активом, с одним отличием: после окна котировки остаток долга пересчитывается.
  • Выбора не было до дедлайна. Когда часы сервиса доходят до expires_at, инвойс становится expired, и приходит invoice.expired с asset и суммами, равными null. Оплатить такой инвойс нельзя: адреса у него нет.
  • Режим per_user. external_user_id обязателен, как всегда, а после выбора инвойс получает постоянный адрес пользователя. Перевод на этот адрес, попавший в блок до выбора, инвойс не оплачивает: это пополнение, оно зачисляется на баланс мерчанта с комиссией пополнения, и приходит deposit.confirmed с invoice_id, равным null. Страница оплаты показывает адрес только после выбора, поэтому не давайте покупателю постоянный адрес для оплаты такого инвойса в обход страницы.
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_1002", "amount": "49.9", "currencies": ["USDT_TRON"]}'

201:

{
  "invoice_id": "8d2c7a4e-1b3f-4c5d-9e6f-7a8b9c0d1e2f",
  "order_id": "ord_1002",
  "status": "created",
  "asset": null,
  "network": null,
  "network_name": null,
  "address": null,
  "address_explorer_url": null,
  "payment_uri": null,
  "price": { "amount": "49.900000", "currency": "USD" },
  "currencies": ["USDT_TRON"],
  "asset_selected_at": null,
  "quote": null,
  "amount_requested": null,
  "amount_expected": null,
  "amount_received": null,
  "amount_remaining": null,
  "amount_credited": null,
  "fees": {
    "service": { "payer": "merchant", "percent": "1.00", "fixed": null, "amount": null },
    "network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
  },
  "external_user_id": null,
  "return_url": null,
  "cancel_url": null,
  "checkout_url": "https://pay.example.com/pay/8d2c7a4e-1b3f-4c5d-9e6f-7a8b9c0d1e2f",
  "expires_at": "2026-09-11T21:03:05.531Z",
  "created_at": "2026-09-11T20:03:05.531Z",
  "deposits": []
}

Цена и курс

Цену в долларах США сервис переводит в токен по курсу — цене одной целой монеты токена в долларах США. Курсы сервис собирает сам из источника рыночных данных и считает по курсу, только пока тот свежий.

  • Стейблкоины тоже. Токен, привязанный к доллару США, переводится по своему рыночному курсу, а не один к одному: при 0.9991 USD за USDT цена 100 USD — это 100.090082 USDT.
  • Токен, не привязанный к доллару США, переводится по рыночному курсу, пониженному на наценку платформы, поэтому монет покупатель отправляет больше. У токена, привязанного к доллару США, наценки нет.
  • Округление вверх. Сумма округляется вверх до шага токена, поэтому покупатель платит не меньше цены. Шаг зависит от того, к чему токен привязан. У токена, привязанного к доллару, шаг — его цент: цена $229.00 при курсе 0.9999 просит 229.03, а не 229.024488. У токена, не привязанного ни к чему, шаг — 0.00000001 его единицы: цент эфира стоил бы 26 долларов, и округлять до него нельзя. Шаг никогда не мельче минимальной единицы самого токена.
  • Любая цена с не больше чем 6 знаками переводится: отказа из-за того, что цена не записывается в токене точно, нет.
  • По какому курсу. По курсу, который страница оплаты показала покупателю, если при выборе он ещё свежий; иначе — по последнему свежему. Сумма, записанная при выборе, — ровно та, что была показана, пока показанный курс свежий.
  • Комиссия сервиса считается от переведённой цены, как у инвойса, созданного в этом токене, — см. комиссии.
  • Свою цену сверяйте с price.amount. amount_requested такого инвойса — цена, переведённая в токен.

Котировка

quote описывает курс, по которому посчитаны текущие суммы в токене:

Поле Тип Описание
rate string Курс сумм: долларов США за целую единицу токена, с 18 знаками после точки. У токена, не привязанного к доллару США, наценка уже учтена
market_rate string Рыночный курс до наценки, с 18 знаками после точки
markdown_percent string Наценка, применённая к market_rate, в процентах с двумя знаками после точки; "0.00" у токена, привязанного к доллару США
currency string USD
number integer Номер котировки: 1 при выборе, дальше на единицу больше с каждой новой котировкой. У инвойса, подтверждённого платежом прежней суммы, — номер той котировки, см. окно котировки
expires_at string До какого момента действуют суммы этой котировки — см. окно котировки

quote равен null у инвойса, созданного с asset, до выбора и у инвойса, токен которого выбран до появления курсов: такой инвойс переведён один к одному и не пересчитывается.

quote сразу после выбора USDT_TRON по курсу 0.9991 USD за USDT; expires_at — дедлайн инвойса, потому что токен привязан к доллару:

{
  "rate": "0.999100000000000000",
  "market_rate": "0.999100000000000000",
  "markdown_percent": "0.00",
  "currency": "USD",
  "number": 1,
  "expires_at": "2026-09-11T21:03:05.531Z"
}

Окно котировки

Суммы котировки действуют до quote.expires_at.

  • Токен, привязанный к доллару США (USDT_*, USDC_*): quote.expires_at равен expires_at инвойса. Сумма, зафиксированная при выборе, действует всю жизнь инвойса, и остаток после частичной оплаты тоже: курс такого токена отходит от доллара на сотые доли процента, допуска недоплаты нет, и сумма, сдвинувшаяся под покупателем, оставила бы того, кто заплатил ровно показанное, без нескольких центов. Курс при выборе всё равно читается, один к одному его не берут.
  • Любой другой токен: окно задаёт платформа, оно отсчитывается от момента котировки и никогда не заканчивается позже expires_at инвойса.
  • После окна токена, не привязанного к доллару, сервис пересчитывает остаток долга по текущему курсу, если инвойс в статусе created или partially_paid, его дедлайн не прошёл и ни один перевод на него не ждёт подтверждений. Окно считается по времени блоков: сервис пересчитывает, только когда обработал блоки позже конца окна, поэтому каждый перевод, отправленный в окне, уже учтён.
  • Что меняется. Остаток долга стоит в долларах США столько, сколько стоил по старому курсу; новая котировка просит эту стоимость по новому курсу, с тем же округлением вверх. amount_expected становится равным amount_received плюс новый остаток, и amount_remaining следует за ним. amount_requested меняется на ту же величину: часть amount_expected, которая была комиссией сервиса на покупателе, сохраняет своё число единиц токена. У quote — следующий number, новые курсы и новый expires_at.
  • Что не меняется. Цена в price, статус, адрес, ставка комиссии и плательщики. Вебхук не отправляется: текущие суммы читайте запросом GET /v1/invoices/{id}. Страница оплаты показывает их сама.
  • Платёж прежней суммы. Покупатель мог скопировать сумму, пока действовала прежняя котировка, а перевод — попасть в блок уже после того, как её сменила новая. Перевод в блоке не позже 10 минут после конца окна прежней котировки оценивается и по её сумме: если платежи к этому моменту её покрыли, инвойс становится confirmed, и его amount_requested, amount_expected и quote снова те, что у прежней котировки. Перевод позже оценивается только по действующей котировке. Зачисляется всегда то, что пришло.
  • Свежего курса нет. Старые суммы действуют, пока свежий курс не появится; сервис пробует снова на следующем проходе.
  • Инвойс, созданный с asset, не пересчитывается никогда: его сумму мерчант задал в токене.

Получить инвойс

GET /v1/invoices/{id} возвращает текущее состояние инвойса. Решение, отдавать ли товар, принимайте по этому ответу — после вебхука, после редиректа со страницы оплаты и при опросе, — а не по телу вебхука: см. вебхуки.

Запрос

Параметр Где Тип Обязательно Описание
id путь string (UUID) да invoice_id из ответа на создание
include строка запроса string нет Единственное значение — qr: тогда в ответе есть QR-код адреса. Любой другой параметр и любое другое значение отклоняются с 400 validation_failed

Чтение условное: в ответе есть ETag, и запрос, который вернул его в If-None-Match, получает 304 Not Modified без тела, пока ничего не изменилось, — см. условные чтения.

Ответ

200 с тем же объектом, что в ответе на создание, — см. создать инвойс. Поля элемента deposits:

Поле Тип Описание
tx_hash string Хэш транзакции
amount string Сумма перевода в единицах актива
confirmations integer Для seen — номер последнего блока, который видел сервис, минус номер блока с переводом; обновляется с новыми блоками. Для confirmed — это число на момент подтверждения; дальше не обновляется
status string seen — перевод в блоке, но подтверждений меньше, чем требует актив; confirmed — подтверждений достаточно
block_time string Время блока с переводом
late boolean true, если block_time позже expires_at
explorer_url string или null Страница транзакции в обозревателе сети; null, если у сети нет ссылки на обозреватель

В deposits — только платежи по этому инвойсу, в порядке их блоков. Пыли, переводов другого актива, переводов, исчезнувших при реорганизации цепи, и пополнений per_user там нет.

Ошибки

HTTP error.code Когда
400 validation_failed id — не UUID
401 unauthorized Нет ключа API или он неверный
403 merchant_disabled Мерчант отключён
404 invoice_not_found Такого инвойса нет, или он принадлежит другому мерчанту
429 rate_limited Превышен лимит запросов
500 internal_error Необработанная ошибка сервиса

Мерчант, который ещё не одобрен, читать свои инвойсы может: этот маршрут merchant_not_approved не возвращает.

Пример

curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44 -H "X-Api-Key: $API_KEY"

200 — инвойс из примера создания после оплаты:

{
  "invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
  "order_id": "ord_1001",
  "status": "confirmed",
  "asset": "USDT_TRON",
  "network": "tron",
  "network_name": "TRON",
  "address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "payment_uri": null,
  "price": null,
  "currencies": null,
  "asset_selected_at": null,
  "quote": null,
  "amount_requested": "49.900000",
  "amount_expected": "49.900000",
  "amount_received": "49.900000",
  "amount_remaining": "0.000000",
  "amount_credited": "49.401000",
  "fees": {
    "service": { "payer": "merchant", "percent": "1.00", "fixed": "0.000000", "amount": "0.499000" },
    "network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
  },
  "external_user_id": null,
  "return_url": null,
  "cancel_url": null,
  "checkout_url": "https://pay.example.com/pay/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
  "expires_at": "2026-09-11T21:03:05.531Z",
  "created_at": "2026-09-11T20:03:05.531Z",
  "deposits": [
    {
      "tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
      "amount": "49.900000",
      "confirmations": 20,
      "status": "confirmed",
      "block_time": "2026-09-11T20:10:42.000Z",
      "late": false,
      "explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
    }
  ]
}

Куда уходят деньги

Подтверждённый платёж ждёт на адресе приёма, пока сервис его не перенесёт. Куда переносить — свойство проекта, которое задаёт оператор, и для API оно не меняет ничего: те же запросы, те же статусы, те же вебхуки.

  • По умолчанию деньги уходят в хранилище сервиса, а баланс мерчанта — это то, что сервис ему должен: читается через баланс и выводится в кабинете.
  • Проекту, которому оператор включил прямые сборы, деньги каждого платежа пересылаются на его собственный кошелёк за вычетом комиссии сервиса, без всяких запросов. Баланс у такого проекта поэтому остаётся нулевым: ноль здесь значит, что деньги уже на его кошельке, а не что ему не платили.

Что из этого следует для интеграции:

  • settled значит «деньги ушли с адреса приёма». Для проекта с прямыми сборами это его собственный кошелёк, для всех остальных — хранилище. Товар в обоих случаях отдаётся на confirmed, ждать settled не нужно никогда.
  • Мелкие платежи копятся. Сервис переносит деньги, только когда перевод стоит своих затрат, а это порог по активу: ниже порога платёж ждёт на адресе приёма, пока следующие платежи на тот же адрес не наберут сумму или пока его не перенесёт сотрудник руками. В TRON перевод стоит около доллара, поэтому порог там — десятки долларов; в EVM-сетях и TON — центы. Инвойс при этом всё равно confirmed, а amount_received говорит правду о том, что пришло.
  • Заявка на вывод по-прежнему работает для того, что проект заработал до переключения. Деньги, уже едущие на собственный кошелёк проекта, к выводу не предлагаются: это были бы одни деньги дважды.

QR-код

Продукт, который рисует свою страницу оплаты, просит QR-код адреса параметром include=qr — при создании и при чтении одного инвойса:

curl -s "https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44?include=qr" -H "X-Api-Key: $API_KEY"

У инвойса появляется одно поле — qr — с кодом в виде SVG-картинки в data:-ссылке. Здесь она сокращена, целиком это около 11 КБ:

"qr": { "address": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDUgNDUi…" }

Показывайте её как src у <img>: ни библиотеки QR на вашей стороне, ни второго запроса.

<img src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDUgNDUi…" alt="" width="240" height="240">
  • В коде лежит адрес оплаты address — целиком и в том виде, в каком его отдаёт инвойс. Это та же картинка, которую рисует страница оплаты, поэтому кошелёк читает ваш экран и наш одинаково.
  • qr.address — null, пока покупатель не выбрал токен: у инвойса ещё нет адреса. Самого поля нет, если его не попросили через include=qr.
  • include принимают только POST /v1/invoices и GET /v1/invoices/{id}. Лента изменений — нет: страница из пятидесяти кодов это полмегабайта, которых никто не просил.
  • В коде нет payment_uri. Наша страница даёт эту ссылку кнопкой, а не кодом; рисуйте свой код по ней, если проверили, что кошельки ваших покупателей читают такие ссылки с камеры.
  • Картинка — SVG, в котором ничего не исполняется, чёрное на белом, и масштабируется до любого размера. Задайте ей размер в своей разметке, а alt оставьте пустым: адрес стоит рядом текстом и целиком, и читать с экрана надо его, а не картинку.

Статусы

Статусы меняет сканер блокчейна. Он читает блоки с отставанием в один блок от головы цепи, поэтому перевод виден через несколько секунд. Подтверждение USDT в TRON — 20 блоков, около минуты. В TON — один блок, несколько секунд.

Термины ниже:

  • Вовремя — перевод, у которого block_time не позже expires_at. С опозданием — перевод, у которого block_time позже.
  • Дедлайн прошёл — сервис обработал блок со временем позже expires_at. Для инвойса, токен которого не выбран, — часы сервиса дошли до expires_at.
Статус Условие Конечный Что делать мерчанту
created Переводов вовремя нет нет Ждать
seen Подтверждённая сумма переводов вовремя меньше amount_expected, и хотя бы один перевод вовремя ещё не подтверждён — до дедлайна или после нет Ждать: неподтверждённый перевод ещё может исчезнуть
partially_paid Дедлайн не прошёл, подтверждённые переводы вовремя в сумме больше нуля и меньше amount_expected, и неподтверждённых переводов вовремя нет нет Не отдавать товар. Покупатель может дослать amount_remaining на тот же адрес до дедлайна — см. частичную оплату
confirmed Подтверждённые переводы вовремя в сумме не меньше amount_expected. Переплата — тоже confirmed дальше только settled Проверить статус через API и отдать товар
underpaid Дедлайн прошёл, все переводы вовремя подтверждены, их сумма больше нуля и меньше amount_expected, подтверждённых переводов с опозданием нет нет Решить самому: подтверждённые переводы зачислены
expired Дедлайн прошёл, подтверждённых переводов нет нет Закрыть заказ
paid_late Дедлайн прошёл, подтверждённая сумма переводов вовремя меньше amount_expected, и подтверждён хотя бы один перевод с опозданием да Решить самому: amount_received показывает итог, переводы зачислены
settled Инвойс был confirmed, и средства всех его подтверждённых платежей ушли с адреса приёма: в хранилище сервиса или сразу на собственный кошелёк мерчанта, если оператор включил это проекту, — см. куда уходят деньги да Ничего: для мерчанта это то же, что confirmed

Переходы:

  • created → seen: в блоке появился перевод вовремя.
  • seen → confirmed: подтверждённые переводы вовремя достигли amount_expected.
  • seen → partially_paid: до дедлайна последний неподтверждённый перевод вовремя подтвердился или исчез при реорганизации, а подтверждённых переводов вовремя меньше amount_expected.
  • partially_paid → seen: до дедлайна в блоке появился ещё один перевод вовремя.
  • seen → created: до дедлайна единственный перевод вовремя исчез при реорганизации цепи.
  • created → expired: дедлайн прошёл без переводов вовремя.
  • partially_paid → underpaid, partially_paid → paid_late: дедлайн прошёл, остаток вовремя не пришёл; новый статус следует из таблицы.
  • seen → underpaid, seen → paid_late, seen → expired: дедлайн прошёл, и последний неподтверждённый перевод вовремя подтвердился или исчез; новый статус следует из таблицы.
  • underpaid → paid_late, expired → paid_late: подтвердился перевод с опозданием.
  • underpaid → seen, expired → seen: исчезнувший при реорганизации перевод вернулся в блок до дедлайна. Это редкий случай.
  • confirmed → settled: средства ушли с адреса приёма — см. куда уходят деньги.
stateDiagram-v2
  [*] --> created
  created --> seen: перевод вовремя в блоке
  seen --> created: перевод исчез при реорганизации
  seen --> confirmed: подтверждённая сумма вовремя достигла ожидаемой
  seen --> partially_paid: до дедлайна подтверждена часть суммы
  partially_paid --> seen: ещё перевод вовремя в блоке
  partially_paid --> underpaid: дедлайн, остаток не пришёл
  partially_paid --> paid_late: дедлайн, подтверждён перевод с опозданием
  created --> expired: дедлайн без переводов
  seen --> underpaid: дедлайн, подтверждено меньше ожидаемого
  seen --> expired: дедлайн, перевод вовремя исчез
  seen --> paid_late: дедлайн, подтверждён перевод с опозданием
  underpaid --> paid_late: подтверждён перевод с опозданием
  expired --> paid_late: подтверждён перевод с опозданием
  underpaid --> seen: перевод вернулся в цепь
  expired --> seen: перевод вернулся в цепь
  confirmed --> settled: средства ушли с адреса

Правила:

  • До дедлайна переводы на адрес инвойса суммируются: первый перевод не окончательный.
  • Подтверждённый перевод остаётся в учёте. Исчезнуть при реорганизации может только неподтверждённый перевод; мерчант тогда получает deposit.orphaned.
  • Перевод на инвойс в статусе confirmed, settled или paid_late статус не меняет: amount_received растёт, а deposit.confirmed сообщает о переводе.
  • Дедлайн определяется по обработанным блокам, поэтому инвойс может оставаться created, seen или partially_paid несколько секунд после expires_at, а пока перевод вовремя ждёт подтверждений — и дольше.

О смене статуса мерчант получает вебхук, названный по новому статусу: invoice.seen, invoice.partially_paid, invoice.confirmed, invoice.underpaid, invoice.expired или invoice.paid_late. При переплате вместе с invoice.confirmed приходит ещё invoice.overpaid. У переходов в created и в settled вебхука нет. Событие приходит при каждом переходе: инвойс, который переходит из partially_paid в seen с новым переводом и обратно, когда этот перевод исчез, получает invoice.seen и invoice.partially_paid снова.

Частичная оплата

Допуска недоплаты нет: инвойс становится confirmed, только когда подтверждённые переводы вовремя покрыли весь amount_expected. Если покупатель прислал меньше, до дедлайна он может дослать остаток на тот же адрес.

  • partially_paid — момент просить доплату. Покажите покупателю amount_remaining и тот же address и напомните про expires_at. Страница оплаты делает это сама — см. страницу оплаты.
  • Остаток меньше минимального депозита актива тоже оплачивает инвойс. Пока по инвойсу уже пришёл платёж, а amount_expected ещё не набран, перевод в активе инвойса на его адрес — платёж, даже если он меньше min_amount, лишь бы больше нуля. Первый перевод по инвойсу и перевод после того, как сумма набрана, по-прежнему пыль, если они меньше минимального депозита.
  • seen — перевод ждёт подтверждений, и amount_remaining его ещё не учитывает. Не просите доплату: остаток, возможно, уже в пути. После подтверждения инвойс станет confirmed или partially_paid с точным остатком.
  • После дедлайна остаток уже не оплачивает инвойс. Перевод в блоке позже expires_at делает его paid_late, а без такого перевода инвойс становится underpaid. Что делать с заказом, решает мерчант.
  • У инвойса, созданного без asset, в токене, не привязанном к доллару США, остаток может пересчитаться после окна котировки: берите amount_remaining из свежего ответа, а не из прежнего.
  • Биржа может удержать комиссию за вывод из суммы, которую отправляет покупатель, и на адрес придёт меньше. Предупредите покупателя: на адрес должна прийти вся сумма.

amount_remaining считается как amount_expected минус amount_received, но не меньше нуля, поэтому у confirmed и settled он равен нулю. У underpaid и paid_late это недостача относительно amount_expected: переводы с опозданием в amount_received входят.

Режим per_user

В режиме per_user у пользователя мерчанта один постоянный адрес, и все инвойсы пользователя оплачиваются на него — см. адреса. Когда перевод на этот адрес появляется в блоке, сервис один раз решает, что он оплачивает:

  • Инвойс, если у пользователя есть подходящий: тот же актив, amount_expected равен сумме перевода, статус created без других переводов, и expires_at не раньше времени блока перевода. Из нескольких таких инвойсов оплачивается созданный раньше всех.
  • Пополнение в остальных случаях: перевод зачисляется на баланс мерчанта с комиссией пополнения, и приходит deposit.confirmed с invoice_id, равным null. Пополнение никогда не оплачивает инвойс позже.

Следствия:

  • перевод на другую сумму — пополнение, а не частичная оплата;
  • перевод на точную сумму после дедлайна — пополнение;
  • инвойс, токен которого не выбран, в сопоставлении не участвует;
  • сравнивается amount_expected: если комиссию сервиса платит покупатель, это цена вместе с комиссией;
  • статусов partially_paid и underpaid в этом режиме не бывает, а paid_late бывает, только если реорганизация перенесла сопоставленный перевод в блок после дедлайна.

Комиссии

С каждого подтверждённого платежа STONS удерживает комиссию сервиса — процент и фиксированную сумму, которые назначает оператор. Процент — общий для платформы или свой у конкретного мерчанта; фиксированная сумма задаётся для каждого актива и одна для всех мерчантов. Любое из чисел может быть нулём. Кто платит комиссию сервиса — мерчант или покупатель, — задаёт настройка проекта «Кто платит комиссии» в кабинете.

Отдельной комиссии сети у инвойса нет. Затраты сети на приём — перенос средств с адреса приёма в хранилище — входят в комиссию сервиса. Это и говорит строка fees.network: status = included. Своя комиссия есть у выплат — см. комиссию за выплату.

Ставка и плательщики записываются в инвойс при его создании, а фиксированная сумма — когда становится известен актив: при создании у инвойса с asset, при выборе покупателя у инвойса без него. Записанные условия не меняются: смена тарифа или настроек проекта касается только новых инвойсов. Инвойсы, созданные до появления комиссии, записаны со ставкой 0, а созданные до появления фиксированной суммы — с фиксированной суммой 0.

Поля

Поле Описание
fees.service.payer merchant — комиссия удерживается из полученного; customer — комиссия добавлена к цене и входит в amount_expected
fees.service.percent Ставка, записанная в инвойс, в процентах с двумя знаками после точки: "1.00"
fees.service.fixed Фиксированная сумма, записанная в инвойс, в единицах актива: "1.700000". Удерживается один раз на инвойс, сколько бы переводов его ни оплатили. null, пока покупатель не выбрал токен
fees.service.amount Комиссия — процент и фиксированная сумма вместе, — удержанная с подтверждённых платежей инвойса на сейчас. До первого подтверждения — ноль; null, пока покупатель не выбрал токен
fees.network.status Всегда included: затраты сети входят в комиссию сервиса, отдельно не начисляются и не удерживаются
fees.network.payer Всегда merchant: покупатель сеть отдельно не оплачивает
fees.network.quote, fees.network.quote_rate, fees.network.actual Всегда null. Оставлены, чтобы не ломать интеграции, которые уже читают эти поля
amount_credited amount_received минус fees.service.amount: сколько из полученного зачислено мерчанту

Как считается комиссия сервиса

В формулах «ставка» — fees.service.percent, делённый на 100, «фиксированная» — fees.service.fixed, «получено» — сумма всех подтверждённых платежей инвойса.

  • Покупатель платит комиссию. К цене прибавляются ставка от цены, округлённая вверх до последнего знака актива, и фиксированная сумма: amount_expected = amount_requested + ⌈amount_requested × ставка⌉ + фиксированная. Инвойс на 100 USDT при 2% и 1.70 USDT ждёт 103.70 USDT.
  • Комиссия удерживается при подтверждении каждого платежа, в той же операции, что его зачисление, и считается от «получено»:
    • платит мерчант — min(получено, ⌊получено × ставка⌋ + фиксированная);
    • платит покупатель — min(получено, фиксированная + ⌊max(0, получено − фиксированная) × ставка / (1 + ставка)⌋): сначала фиксированная сумма, затем доля процента внутри оплаченного.
  • Не больше полученного. Платёж меньше фиксированной суммы удерживается целиком, но не больше: зачисленное не уходит ниже нуля.
  • Округление вниз, в пользу мерчанта. Каждый следующий перевод удерживает разницу между комиссией с новой суммы и уже удержанной, поэтому инвойс, оплаченный несколькими переводами, удерживает столько же, сколько оплаченный одним, а фиксированную сумму — один раз.
  • Точная оплата покупателем удерживает ровно то, что прибавилось к цене, или на одну минимальную единицу актива меньше, если процент от цены не делится нацело: округление вниз оставляет эту единицу мерчанту.
  • Переплата — комиссия с большей суммы. Недоплата — с полученного. Платёж после срока — как любой подтверждённый.
  • Пыль и переводы другого актива комиссии не несут: это не платежи. Перевод, исчезнувший при реорганизации до подтверждения, не зачислен и комиссии не несёт.
  • Пополнение per_user несёт комиссию с суммы перевода по ставке мерчанта и фиксированной сумме актива, действовавшим на время блока перевода, не больше суммы перевода; платит мерчант. Каждое пополнение — отдельная операция со своей фиксированной суммой. На баланс мерчанта поступает сумма перевода за вычетом этой комиссии; вебхуки deposit.* полей комиссий не несут.

Пример: цена 100 USDT, ставка 2%, фиксированная сумма 1.70 USDT, комиссию платит покупатель. Он платит двумя переводами: 1 USDT, затем 102.70 USDT.

Момент amount_requested amount_expected amount_received fees.service.amount amount_credited
Создан 100.000000 103.700000 0.000000 0.000000 0.000000
Первый перевод подтверждён 100.000000 103.700000 1.000000 1.000000 0.000000
Второй перевод подтверждён 100.000000 103.700000 103.700000 3.700000 100.000000

Интеграция сравнивает свою цену с amount_requested у инвойса, созданного с asset, и с price.amount у инвойса, созданного без него: там amount_requested — цена, переведённая в токен. Покупателю показывается amount_expected: только эта сумма закрывает инвойс.