Инвойсы
Этот документ описывает инвойсы: как создать инвойс через 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.
Как держать свои заказы в соответствии:
- Обрабатывайте вебхуки — это быстрый путь.
- Храните момент последнего обработанного изменения.
- Если вебхуки не приходили, пройдите ленту с этого момента, страница за страницей, пока
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: только эта сумма закрывает инвойс.