# Инвойсы

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

Инвойс — ожидание платежа на конкретную сумму в конкретном активе до дедлайна. Мерчант (в кабинете — проект) создаёт инвойс под заказ и либо сам показывает покупателю сумму и адрес, либо отправляет покупателя на [страницу оплаты](https://stons.io/ru/docs/checkout.md). Покупатель — тот, кто платит. Все времена в запросах и ответах — 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` зачисляется, как раньше, — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес).

### Активы 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](https://stons.io/ru/docs/addresses.md#адреса-в-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](https://stons.io/ru/docs/addresses.md#адреса-в-solana).
- Токен в Solana лежит не на адресе владельца, а в его associated token account — отдельном аккаунте на пару (владелец, mint). Этот аккаунт сервис создаёт сам при выдаче адреса, поэтому кошелёк покупателя платит на адрес как обычно и знать о нём ничего не должен.

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

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

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

Перевод монеты на адрес инвойса в другом активе записывается как `unexpected_asset`: мерчанту он не зачисляется и инвойс не закрывает — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес).

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

Установка в тестовой сети 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`, пока каталог не менялся, — см. [условные чтения](https://stons.io/ru/docs/authentication.md#условные-чтения).

### Ответ

`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`, если метки нет |

- Список говорит, что можно предложить сейчас, а не что зачисляется: перевод в активе, выпавшем из списка, обрабатывается по правилам своего адреса — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес).
- Список не зависит от проверки мерчанта и от приостановленного режима адресов: они отклоняют сам `POST /v1/invoices` своими кодами.

### Ошибки

| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 401 | `unauthorized` | Ключ API не передан или неверен — см. [аутентификацию](https://stons.io/ru/docs/authentication.md) |
| 403 | `merchant_disabled` | Мерчант выключен |
| 429 | `rate_limited` | Превышен лимит запросов |
| 500 | `internal_error` | Необработанная ошибка сервиса |

### Пример

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

`200` в mainnet TRON:

```json
{
  "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-код](#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`, и на сайте мерчанта — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата) |
| `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](https://stons.io/ru/docs/addresses.md#адреса-в-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-код](#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 | [Страница оплаты](https://stons.io/ru/docs/checkout.md) этого инвойса; `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 или он неверный — см. [аутентификацию](https://stons.io/ru/docs/authentication.md) |
| 403 | `merchant_disabled` | Мерчант отключён |
| 403 | `merchant_not_approved` | Мерчант ещё не одобрен после проверки или отклонён |
| 409 | `order_id_conflict` | Инвойс с этим `order_id` есть и описывает другой платёж |
| 422 | `address_mode_disabled` | Платформа временно приостановила режим адресов мерчанта — см. [режим на паузе](https://stons.io/ru/docs/addresses.md#режим-на-паузе) |
| 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` не на сайте мерчанта — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата) |
| 429 | `rate_limited` | Превышен лимит запросов |
| 500 | `internal_error` | Необработанная ошибка сервиса |
| 503 | `addresses_exhausted` | Закончились индексы деривации адресов: заняты все 2^31. Практически недостижимо |
| 503 | `network_fee_quote_unavailable` | У проекта осталась прежняя настройка «комиссию сети платит покупатель». Владельцу нужно сохранить «Кто платит комиссии» в настройках проекта в кабинете |
| 503 | `rate_unavailable` | Без `asset`: ни у одного разрешённого токена сейчас нет свежего курса, и оплатить инвойс никто не смог бы. Инвойс не создаётся |
| 503 | `service_unavailable` | Сервис не может вывести новый адрес в текущей конфигурации; сообщение называет причину |

Что делать клиенту при каждом коде — см. [коды ошибок](https://stons.io/ru/docs/errors.md).

### Пример

```sh
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`:

```json
{
  "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": []
}
```

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

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

```sh
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` отдаёт по каждому активу проекта курс, **по которому и будет посчитан инвойс**: рыночный с уже снятой наценкой сервиса. Поэтому число, которое вы покажете покупателю заранее, совпадёт с тем, что встанет в инвойс.

```sh
curl -s https://api.stons.io/v1/rates -H "X-Api-Key: $API_KEY"
```

```json
{
  "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-кодов](#qr-код). В ответе есть `ETag`: проход, который не нашёл нового, стоит одного `304` без тела — см. [условные чтения](https://stons.io/ru/docs/authentication.md#условные-чтения).

### Ответ

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

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

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

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

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

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

Запрос без `asset` создаёт инвойс без токена: покупатель выбирает монету и сеть на [странице оплаты](https://stons.io/ru/docs/checkout.md#выбор-токена) и видит сумму и адрес только после выбора.

- **Цена.** `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`. Страница оплаты показывает адрес только после выбора, поэтому не давайте покупателю постоянный адрес для оплаты такого инвойса в обход страницы.

```sh
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`:

```json
{
  "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` — дедлайн инвойса, потому что токен привязан к доллару:

```json
{
  "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}` возвращает текущее состояние инвойса. Решение, отдавать ли товар, принимайте по этому ответу — после вебхука, после редиректа со страницы оплаты и при опросе, — а не по телу вебхука: см. [вебхуки](https://stons.io/ru/docs/webhooks.md).

### Запрос

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

Чтение условное: в ответе есть `ETag`, и запрос, который вернул его в `If-None-Match`, получает `304 Not Modified` без тела, пока ничего не изменилось, — см. [условные чтения](https://stons.io/ru/docs/authentication.md#условные-чтения).

### Ответ

`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` не возвращает.

### Пример

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

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

```json
{
  "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 оно не меняет ничего: те же запросы, те же статусы, те же вебхуки.

- **По умолчанию деньги уходят в хранилище сервиса**, а баланс мерчанта — это то, что сервис ему должен: читается через [баланс](https://stons.io/ru/docs/balance.md) и выводится в кабинете.
- **Проекту, которому оператор включил прямые сборы**, деньги каждого платежа пересылаются на его собственный кошелёк за вычетом комиссии сервиса, без всяких запросов. Баланс у такого проекта поэтому остаётся нулевым: ноль здесь значит, что деньги уже на его кошельке, а не что ему не платили.

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

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

## QR-код

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

```sh
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 КБ:

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

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

```html
<img src="data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgNDUgNDUi…" alt="" width="240" height="240">
```

- В коде лежит адрес оплаты `address` — целиком и в том виде, в каком его отдаёт инвойс. Это та же картинка, которую рисует [страница оплаты](https://stons.io/ru/docs/checkout.md), поэтому кошелёк читает ваш экран и наш одинаково.
- `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`: средства ушли с адреса приёма — см. [куда уходят деньги](#куда-уходят-деньги).

```mermaid
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`, а пока перевод вовремя ждёт подтверждений — и дольше.

О смене статуса мерчант получает [вебхук](https://stons.io/ru/docs/webhooks.md#события), названный по новому статусу: `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`. Страница оплаты делает это сама — см. [страницу оплаты](https://stons.io/ru/docs/checkout.md#страница-для-покупателя).
- **Остаток меньше минимального депозита** актива тоже оплачивает инвойс. Пока по инвойсу уже пришёл платёж, а `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` у пользователя мерчанта один постоянный адрес, и все инвойсы пользователя оплачиваются на него — см. [адреса](https://stons.io/ru/docs/addresses.md#режимы). Когда перевод на этот адрес появляется в блоке, сервис один раз решает, что он оплачивает:

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

Следствия:

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

## Комиссии

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

**Отдельной комиссии сети у инвойса нет.** Затраты сети на приём — перенос средств с адреса приёма в хранилище — входят в комиссию сервиса. Это и говорит строка `fees.network`: `status` = `included`. Своя комиссия есть у выплат — см. [комиссию за выплату](https://stons.io/ru/docs/payouts.md#комиссия-за-выплату).

Ставка и плательщики **записываются в инвойс при его создании**, а фиксированная сумма — когда становится известен актив: при создании у инвойса с `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`: только эта сумма закрывает инвойс.
