# STONS: платёжный API
> Приём криптоплатежей через API и выплаты на адреса из вайтлиста. Приём работает в TRON, в EVM-сетях — Ethereum, BNB Smart Chain, Polygon, Base, Arbitrum, OP Mainnet, Avalanche, — в TON и в Solana, в USDT, USDT0, USDC и нативной монете каждой сети; выплаты сервис делает только в TRON. Не зашивайте этот список в код: какие активы доступны проекту прямо сейчас, отвечает GET /v1/assets. Инвойс в токене отдаёт адрес целиком и точную сумму для оплаты на своей странице; инвойс без токена оплачивается на странице оплаты сервиса, где покупатель выбирает монету и сеть. События приходят вебхуками с подписью HMAC-SHA256, а источник истины для любого статуса — API. API: https://api.stons.io/v1, ключ в заголовке X-Api-Key, суммы — десятичные строки.
---
# Интеграция под ключ с ИИ-ассистентом
Этот документ — задание для ИИ-ассистента в проекте мерчанта: Claude Code, Cursor или любого другого, который пишет код по инструкции. В нём по порядку работ перечислено, что построить для приёма платежей через STONS, какие решения принять и как, и что проверить в конце. Каждое правило ссылается на документ API, в котором оно записано; при расхождении прав документ API.
## Словарь
Кабинет — веб-приложение STONS, в котором владелец проекта управляет проектом. Кабинет и API называют некоторые вещи по-разному; эта документация пользуется терминами из первого столбца.
| Термин | В кабинете | Что это |
| --- | --- | --- |
| кабинет | «Кабинет» | Веб-приложение STONS по адресу `https://app.stons.io`, в котором владелец проекта управляет проектом |
| мерчант | проект («Проекты») | Сайт, бот или сервис, который принимает платежи со своими ключами API и настройками |
| владелец проекта | учётная запись, создавшая проект | Выпускает ключи API и задаёт адрес вебхуков, адреса возврата, токены, которые принимает проект, и кто платит комиссии. Видит доставки вебхуков и отправляет недоставленные заново |
| оператор | поддержка | Сотрудник STONS: проверяет проекты, ведёт вайтлист выплат, подтверждает выплаты, которые ждут человека, назначает комиссии и лимиты, разрешает проекту ограниченные режимы — постоянный адрес на пользователя и прямые сборы на собственный кошелёк проекта |
| инвойс | счёт, в разделе «Платежи» | Просьба заплатить конкретную сумму до дедлайна: `POST /v1/invoices` |
| ключ API | «Ключ API» | `sk_<16 шестнадцатеричных символов>_<43 символа base64url>`, передаётся в заголовке `X-Api-Key`. Показывается один раз, при выпуске |
| секрет вебхуков | «Секрет подписи вебхуков» | Ключ подписей HMAC-SHA256 у вебхуков и у редиректов со страницы оплаты. Показывается один раз, при создании проекта. Используется как строка, без преобразований |
| актив | токен и сеть | Идентификатор токена в сети, `<ТИКЕР>_<СЕТЬ>`: `USDT_TRON`, `USDC_SOLANA` |
| страница оплаты | ссылка на оплату | Собственная страница сервиса для одного инвойса, по ссылке `checkout_url` из инвойса |
| подтверждение | — | Блок, добавленный поверх блока с переводом. Перевод засчитывается, когда у него столько подтверждений, сколько требует его актив |
| пополнение | — | В режиме `per_user`: перевод на постоянный адрес пользователя, который не оплачивает ни один инвойс. Зачисляется на баланс проекта, а `deposit.confirmed` приходит с `invoice_id`, равным `null` |
| выплата | — | Перевод с горячего кошелька сервиса на адрес из вайтлиста мерчанта: `POST /v1/payouts` |
| вайтлист | — | Адреса для выплат мерчанта, по активам. Их вносит оператор; новый адрес становится доступен после задержки, по умолчанию через 24 часа |
## Шаг 0. Что спросить у человека
Прежде чем писать код, получите у владельца проекта:
1. **Ключ API.** Владелец выпускает его в настройках проекта в кабинете — см. [аутентификацию](https://stons.io/ru/docs/authentication.md).
2. **Секрет вебхуков.** Кабинет показывает его один раз, вместе с первым ключом, при создании проекта — см. [вебхуки](https://stons.io/ru/docs/webhooks.md#подпись).
3. **Прошёл ли проект проверку.** Пока проект не одобрен, инвойсы не создаются: `403 merchant_not_approved`. Ключи при этом уже работают, так что код писать можно — см. [коды ошибок](https://stons.io/ru/docs/errors.md#перечень).
4. **Режим адресов** проекта: `per_invoice` — новый адрес под каждый инвойс, `per_user` — постоянный адрес пользователя. Проект создаётся в `per_invoice`; `per_user` — ограниченный режим, его разрешает оператор по проектам, и только после этого проект можно перевести в него. Режим может измениться позже, и смена решает, где будет собран **следующий** платёж: уже выданные адреса остаются при своём режиме и зачисляются как раньше — см. [адреса](https://stons.io/ru/docs/addresses.md#режимы). Два отказа значат разное: `422 address_mode_disabled` — платформа приостановила режим для всех ([режим на паузе](https://stons.io/ru/docs/addresses.md#режим-на-паузе)); `422 mode_not_allowed` — этому проекту режим не разрешён. Спрашивайте у владельца, в каком режиме проект сейчас, а не считайте режим неизменным.
5. **Как показывать оплату:** своей страницей в продукте или страницей оплаты. Чтобы задать верный вопрос, используйте правила шага 3.
6. **Токены:** какие активы продукт предлагает. Приём работает в TRON, в семи EVM-сетях, в TON и в Solana — в USDT, USDT0, USDC и нативной монете каждой сети; `USDC_TRON` оператор выключил целиком — см. [сети и токены](https://stons.io/ru/docs/networks.md). **Не зашивайте этот список в код.** Активы, которые проект может принять прямо сейчас, с сетью, числом знаков и минимальной суммой каждого отдаёт `GET /v1/assets`; какие токены принимает проект, решают его настройки в кабинете — см. [список активов](https://stons.io/ru/docs/invoices.md#список-активов).
7. **Кто платит комиссию сервиса** — настройка проекта «Кто платит комиссии». Если платит покупатель, сумма к оплате больше цены — см. [комиссии](https://stons.io/ru/docs/invoices.md#комиссии).
8. **Для страницы оплаты:** адреса возврата и отмены, которые владелец задаёт в настройках проекта как «После успешной оплаты» и «Вернуться в магазин / при неудаче» — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата).
9. **Нужны ли выплаты.** Если нужны, каждый адрес для выплат оператор должен заранее внести в вайтлист — см. [выплаты](https://stons.io/ru/docs/payouts.md#куда-можно-платить).
Когда обработчик вебхуков готов, направьте проект на него сами: `PATCH /v1/project` с полем `webhook_url`, тем же ключом — в кабинет заходить никому не нужно ([настройки проекта](https://stons.io/ru/docs/authentication.md#настройки-проекта)). `GET /v1/project` сначала покажет, что задано сейчас и одобрен ли проект. Пока адрес не задан, проект не получает событий, и события того времени позже не досылаются — см. [доставку](https://stons.io/ru/docs/webhooks.md#доставка).
Тем же ключом задаются адреса возврата страницы оплаты (`return_url`, `cancel_url`) и выбираются принимаемые токены (`GET` и `PUT /v1/project/assets`). Два ограничения, которые стоит знать заранее: адрес вебхука можно заменить, но не удалить, а секрет подписи не возвращает ни один маршрут — попросите его у владельца один раз, при создании проекта.
Не придумывайте значения за человека: ключ и секрет существуют только у него.
## Шаг 1. Переменные окружения
Ключ и секрет — только на сервере продукта, в менеджере секретов или переменных окружения. Их нет в браузере, мобильном приложении, репозитории и логах — см. [чек-лист](https://stons.io/ru/docs/checklist.md#ключи-и-доступ). Имена переменных выбирает проект, например:
```sh
PAYMENTS_API_URL=https://api.stons.io
PAYMENTS_API_KEY=sk_...
PAYMENTS_WEBHOOK_SECRET=...
```
Адрес API — с `https://`: по `http://` ключ ушёл бы открытым текстом ещё до перенаправления — см. [аутентификацию](https://stons.io/ru/docs/authentication.md#сеть).
## Шаг 2. Клиент API
- Базовый адрес — `https://api.stons.io/v1`, ключ — в заголовке `X-Api-Key`, тела запросов и ответов — JSON с `Content-Type: application/json`.
- Описание API в формате OpenAPI 3.1 — `https://api.stons.io/v1/openapi.json`. Тела запросов принимают только описанные поля: неизвестное поле получает `400 validation_failed`.
- **Суммы — строки** в единицах актива: `"49.9"` в запросе, `"49.900000"` в ответе. У суммы актива в ответе после точки ровно столько цифр, сколько знаков у актива, поэтому без точки она даёт целое число в минимальных единицах. В сумме запроса знаков после точки не больше, чем у актива — см. [суммы](https://stons.io/ru/docs/invoices.md#суммы).
- Ошибка — тело `{"error": {"code", "message", "details"}}`. Решения принимайте по `error.code`, а не по тексту `message` — см. [коды ошибок](https://stons.io/ru/docs/errors.md).
- `429 rate_limited` — повторить через число секунд из заголовка `Retry-After`, см. [лимиты запросов](https://stons.io/ru/docs/authentication.md#лимиты-запросов).
- `500 internal_error` и `503 service_unavailable` — повторить позже с тем же `order_id` или `reference`. Что делать при остальных кодах — в [перечне кодов](https://stons.io/ru/docs/errors.md#перечень).
- `X-Request-Id` из ответа сохраняйте в логах: по нему поддержка находит запрос.
## Шаг 3. Выбрать, как показывать оплату
Примените первое подходящее правило:
- **Если монету и сеть должен выбрать покупатель**, создайте инвойс без `asset` и отправьте покупателя по `checkout_url` (шаг 5Б). У такого инвойса нет адреса до выбора, поэтому показать его на своей странице нельзя. Если у сервиса нет страницы оплаты, запрос получит `422 asset_required`: тогда примените следующее правило — см. [токен выбирает покупатель](https://stons.io/ru/docs/invoices.md#токен-выбирает-покупатель).
- **Если цены у продукта в долларах, а монета известна заранее**, создайте инвойс с `asset` и `price_currency: "USD"`, передав цену в `amount`. Сервис сам пересчитает её по курсу и вернёт `address`, `amount_expected` и `quote.rate` в том же ответе — считать курс самим не нужно и не следует. Чтобы показать «≈ 0,45 SOL» до создания, спросите `GET /v1/rates` — см. [цену в долларах в выбранной монете](https://stons.io/ru/docs/invoices.md#цена-в-долларах-в-выбранной-монете).
- **Если у продукта свой экран оплаты**, создайте инвойс с `asset` и покажите на нём `address` и `amount_expected` (шаг 5А).
- **Иначе** создайте инвойс с `asset` и отправьте покупателя по `checkout_url` (шаг 5Б). Если `checkout_url` равен `null`, страницы оплаты у сервиса нет: используйте шаг 5А.
Независимо от выбора:
- **Если режим `per_user`**, передавайте `external_user_id` в каждом инвойсе. Постоянный адрес пользователя без инвойса выдаёт `POST /v1/addresses` — см. [адреса](https://stons.io/ru/docs/addresses.md#постоянный-адрес-пользователя).
- **Если продукт зачисляет пополнения `per_user`** на балансы пользователей, проверяйте каждый `deposit.confirmed` с `invoice_id`, равным `null`, запросом `GET /v1/deposits/{tx_hash}` и зачисляйте пользователю, только когда у перевода с `log_index` из события `status` = `confirmed` и `credited` = `true`, — см. [проверку перевода](https://stons.io/ru/docs/addresses.md#проверить-перевод).
## Шаг 4. Создать инвойс
`POST /v1/invoices` — см. [создание инвойса](https://stons.io/ru/docs/invoices.md#создать-инвойс).
```sh
curl -s -X POST https://api.stons.io/v1/invoices \
-H "X-Api-Key: $PAYMENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"order_id": "ord_1001", "asset": "USDT_TRON", "amount": "49.9", "expires_in": 3600}'
```
- `order_id` — номер заказа в продукте, 1–200 видимых ASCII-символов без пробелов. Это ключ идемпотентности: при таймауте повторяйте запрос **с тем же** `order_id` и теми же полями — вернётся тот же инвойс с кодом `200` вместо `201`, второй не создастся. Тот же `order_id` с другой суммой, активом, пользователем или адресами возврата получает `409 order_id_conflict` — см. [идемпотентность](https://stons.io/ru/docs/invoices.md#идемпотентность).
- `expires_in` — необязательно: от 300 (5 минут) до 259200 секунд (72 часа), по умолчанию 3600.
- Сумма не меньше минимального депозита актива, иначе `422 amount_below_minimum`.
- Сохраните у заказа `invoice_id`: по нему дальше проверяется статус.
Без `asset` поле `amount` — цена в долларах США, а необязательный `currencies` — из каких активов выбирает покупатель. В ответе `asset`, `network`, `address` и суммы в активе — `null`, а цена лежит в `price`:
```sh
curl -s -X POST https://api.stons.io/v1/invoices \
-H "X-Api-Key: $PAYMENTS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"order_id": "ord_1002", "amount": "49.9"}'
```
## Шаг 5А. Оплата на своей странице
Покажите покупателю из ответа:
- `address` — **целиком**, без сокращений вида «начало…конец»: на сокращённые адреса охотятся подменой похожим адресом — см. [показ адреса](https://stons.io/ru/docs/addresses.md#показ-адреса);
- `amount_expected` и актив. Это сумма к оплате: если комиссию сервиса платит покупатель, она больше цены заказа, а сама цена — в `amount_requested`, см. [комиссии](https://stons.io/ru/docs/invoices.md#комиссии);
- сеть актива по названию из `network_name` — TRON, одна из EVM-сетей, TON или Solana — с предупреждением, что перевод другого токена или в другой сети не зачисляется автоматически — см. [чек-лист](https://stons.io/ru/docs/checklist.md#отображение);
- срок оплаты, `expires_at`;
- `payment_uri` — ссылка, открывающая кошелёк с заполненным переводом, если она не `null`: покупателю не придётся ни копировать адрес, ни набирать сумму. Сейчас её даёт TON; в Solana и остальных сетях она `null`, и адрес с суммой копируют со страницы;
- QR-код — попросите его параметром `include=qr` при создании или чтении инвойса и покажите `qr.address` как `src` у `
`. В нём тот же адрес, и это та же картинка, которую рисует наша страница оплаты, — библиотека QR на вашей стороне не нужна, см. [QR-код](https://stons.io/ru/docs/invoices.md#qr-код).
Что не меняется от инвойса к инвойсу — число подтверждений актива (`confirmations`), его метку (`icon`), стандарт токена и минимальную сумму — берите один раз из `GET /v1/assets` и держите у себя: по `confirmations` рисуется «2 из 20», пока перевод набирает подтверждения.
Проекту могут быть включены прямые сборы: деньги каждого платежа пересылаются на собственный кошелёк проекта за вычетом комиссии сервиса, а не хранятся у нас. Режим разрешает проекту оператор; кошелёк владелец проекта вписывает сам в кабинете, а сотрудник сервиса его одобряет — до одобрения адрес денег не получает. В API от этого не меняется ничего, и товар в любом случае отдаётся на `confirmed`; меняется смысл двух вещей: баланс такого проекта остаётся нулевым, потому что деньги уже на его кошельке, а `settled` значит этот кошелёк, а не хранилище сервиса. В активе, по которому действующего кошелька у проекта нет, платёж уходит в хранилище, и проект получает его заявкой на вывод, как любой другой, — так что нулевой баланс тоже не обещание. Не делайте интеграцию, которая ждёт роста баланса, — см. [куда уходят деньги](https://stons.io/ru/docs/invoices.md#куда-уходят-деньги).
Пришедшие деньги читаются запросом `GET /v1/balance`: баланс по реестру по каждому активу, что ещё не подтверждено, и то и другое в долларах — см. [баланс](https://stons.io/ru/docs/balance.md).
Статус узнавайте из вебхуков (шаг 6) и запросом `GET /v1/invoices/{id}`. Если вебхук не дошёл, пройдите [ленту изменений](https://stons.io/ru/docs/invoices.md#лента-изменений) — шаг 7. Экран, который опрашивает инвойс, держит `ETag` последнего ответа и присылает его обратно в `If-None-Match`: «ничего не изменилось» тогда стоит одного `304` без тела, см. [условные чтения](https://stons.io/ru/docs/authentication.md#условные-чтения).
## Шаг 5Б. Страница оплаты
Отправьте покупателя по `checkout_url` из ответа. Если инвойс создан без `asset`, страница сначала даёт покупателю выбрать монету и сеть. Страница показывает сумму, адрес, QR-код и статус, а когда у инвойса появляется исход, возвращает покупателя на адрес возврата или отмены — см. [страницу оплаты](https://stons.io/ru/docs/checkout.md).
- Для отдельного инвойса адреса можно переопределить полями `return_url` и `cancel_url` в `POST /v1/invoices`, но только в пределах сайтов проекта, иначе `422 redirect_url_not_allowed` — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата).
- Пока инвойс не оплачен, страница показывает кнопку «Вернуться в магазин» на адрес отмены со `status=created`, `status=seen` или `status=partially_paid`. Инвойс ещё можно оплатить: заказ по такому переходу не отменяйте — см. [переход до исхода](https://stons.io/ru/docs/checkout.md#переход-до-исхода).
## Шаг 6. Обработчик вебхуков
Сервис шлёт `POST` на адрес вебхуков проекта. Обработчик — см. [вебхуки](https://stons.io/ru/docs/webhooks.md):
1. Читает **сырое тело** запроса, до разбора JSON.
2. Проверяет подпись: `X-Signature: sha256=` — это HMAC-SHA256 секретом вебхуков по строке `.<сырое тело>`. Отклоняет вебхук, если `X-Timestamp` отличается от текущего времени больше чем на 5 минут, и сравнивает подписи за постоянное время. Код проверки на Node.js или Python и пример для проверки реализации возьмите из [подписи](https://stons.io/ru/docs/webhooks.md#подпись).
3. Отбрасывает дубли по `id` события: одно событие может прийти несколько раз.
4. Отвечает `2xx` за 10 секунд, в том числе на типы событий, которые не обрабатывает, а работу выполняет асинхронно: без ответа `2xx` вебхук доставляется повторно.
5. **Не доверяет телу.** Для `invoice.*` и для `deposit.*` с `invoice_id` запрашивает `GET /v1/invoices/{id}`, для `deposit.*` без `invoice_id` — `GET /v1/deposits/{tx_hash}`, для `payout.*` — `GET /v1/payouts/{id}`, и действует по ответу API — см. [главное правило](https://stons.io/ru/docs/webhooks.md#главное-правило).
Порядок доставки не гарантирован: `invoice.confirmed` может прийти раньше `invoice.seen`. Ориентируйтесь на состояние из API.
## Шаг 7. Когда отдавать товар
По статусу из `GET /v1/invoices/{id}` — см. [статусы](https://stons.io/ru/docs/invoices.md#статусы):
| Статус | Что делать |
| --- | --- |
| `created`, `seen` | Ждать. `seen` — перевод замечен, но ещё может откатиться |
| `partially_paid` | Не отдавать товар. До срока пришла часть суммы: если продукт показывает оплату сам, покажите покупателю `amount_remaining` и тот же адрес — см. [частичную оплату](https://stons.io/ru/docs/invoices.md#частичная-оплата) |
| `confirmed`, `settled` | Отдать товар. Переплата — тоже `confirmed`, `amount_received` больше `amount_expected` |
| `underpaid` | Срок вышел, пришла часть суммы: решает продукт |
| `expired` | Срок вышел без платежа: закрыть заказ |
| `paid_late` | Платёж пришёл после срока: решает продукт |
- Свою цену сверяйте с `amount_requested` у инвойса, созданного с `asset`. У инвойса, созданного без `asset`, сверяйте с `price.amount`: `amount_requested` до выбора — `null`, а после — цена, переведённая по курсу, см. [цену и курс](https://stons.io/ru/docs/invoices.md#цена-и-курс).
- `expired` и `underpaid` ещё могут стать `paid_late`, если деньги придут позже — см. [страницу оплаты](https://stons.io/ru/docs/checkout.md#страница-для-покупателя).
- Выдачу товара делайте идемпотентной по `order_id`: повтор вебхука, второе событие того же изменения или редирект не должны выдать товар второй раз.
## Шаг 8. Возврат со страницы оплаты
Если используется страница оплаты, покупатель приходит на адрес возврата после `confirmed`, `settled` или `paid_late`, а на адрес отмены — после `expired` или `underpaid` либо по кнопке «Вернуться в магазин». Параметры — `invoice_id`, `order_id`, `status`, `ts` и `signature`. Обработчик — см. [подпись редиректа](https://stons.io/ru/docs/checkout.md#подпись):
1. Проверяет подпись: HMAC-SHA256 секретом вебхуков по строкам `redirect`, `ts`, `invoice_id`, `status`, `order_id`, соединённым символом `\n`, и окно `ts` в 5 минут. Код проверки и пример — в разделе по ссылке.
2. Проверяет, что `invoice_id` — инвойс этого `order_id`.
3. Берёт статус из `GET /v1/invoices/{id}`, а не из параметра. **Товар по редиректу не отдаётся.**
## Шаг 9. Выплаты — если нужны
`POST /v1/payouts` — см. [выплаты](https://stons.io/ru/docs/payouts.md):
- Платить можно только на адреса из вайтлиста мерчанта и только после их задержки. Адрес для выплаты никогда не берётся из истории транзакций.
- `reference` — ключ идемпотентности: повтор с тем же `reference`, активом, адресом и суммой возвращает ту же выплату с кодом `200` вместо `202`. Другие значения получают `409 reference_conflict` — см. [идемпотентность](https://stons.io/ru/docs/payouts.md#идемпотентность).
- `pending_review` — не ошибка: выплата принята и ждёт решения человека, а `review_reasons` говорит почему.
- `503 payouts_halted` — повторить позже с тем же `reference`.
- Исход берите из `GET /v1/payouts/{id}`. Вебхуки `payout.*` — сигнал сделать этот запрос, и не у каждой смены статуса он есть: какие статусы окончательные и какие переходы присылают вебхук — в [статусах выплат](https://stons.io/ru/docs/payouts.md#статусы).
## Шаг 10. Перед запуском
Пройдите [чек-лист перед продом](https://stons.io/ru/docs/checklist.md) целиком и проверьте весь путь на небольшой сумме: инвойс, оплата, вебхук, проверка статуса, выдача товара.
## Чего не делать никогда
- Не зашивайте в код число знаков и не превращайте суммы в числа с плавающей точкой: храните строки или целые числа в минимальных единицах — см. [суммы](https://stons.io/ru/docs/invoices.md#суммы).
- Не сокращайте адрес нигде, где он показывается или пишется в лог — см. [показ адреса](https://stons.io/ru/docs/addresses.md#показ-адреса).
- Не приводите адрес к нижнему регистру и не нормализуйте его иначе. В Solana адрес — base58, и регистр — его часть: адрес, приведённый к нижнему регистру, — это адрес, ключа к которому ни у кого нет. Храните и сравнивайте адрес ровно таким, каким его вернул API, — см. [идентификаторы и знаки после запятой](https://stons.io/ru/docs/networks.md#идентификаторы-и-знаки-после-запятой).
- Не отдавайте товар по телу вебхука или по редиректу без `GET /v1/invoices/{id}` — см. [главное правило вебхуков](https://stons.io/ru/docs/webhooks.md#главное-правило) и [страницы оплаты](https://stons.io/ru/docs/checkout.md#главное-правило).
- Не отдавайте товар при `seen`.
- Не разбирайте и не сериализуйте тело вебхука заново перед проверкой подписи — см. [подпись](https://stons.io/ru/docs/webhooks.md#подпись).
- Не создавайте второй инвойс или вторую выплату после таймаута: повторяйте запрос с тем же `order_id` или `reference`.
- Не берите адрес для выплаты из истории транзакций — см. [выплаты](https://stons.io/ru/docs/payouts.md#куда-можно-платить).
- Не кладите ключ API и секрет вебхуков в браузер, мобильное приложение, репозиторий и логи — см. [аутентификацию](https://stons.io/ru/docs/authentication.md).
- Не принимайте решений по `error.message` — см. [коды ошибок](https://stons.io/ru/docs/errors.md).
## Самопроверка
Прежде чем сообщить, что интеграция готова, подтвердите в коде каждый пункт:
- [ ] Ключ и секрет читаются из окружения сервера и не пишутся в логи.
- [ ] Каждый вызов API идёт на `https://`, передаёт `X-Api-Key` и обрабатывает `error.code`, `429` с `Retry-After`, а также `500` и `503 service_unavailable` повтором с тем же ключом идемпотентности.
- [ ] Суммы везде — строки или целые числа в минимальных единицах, в том числе в базе продукта.
- [ ] Инвойс создаётся с постоянным `order_id`, а `invoice_id` сохраняется у заказа.
- [ ] Страница показывает адрес целиком, `amount_expected`, актив и сеть — или покупатель уходит по `checkout_url`.
- [ ] Обработчик вебхуков проверяет подпись по сырому телу с окном в 5 минут, отбрасывает дубли по `id`, отвечает `2xx` за 10 секунд и проходит пример из [подписи](https://stons.io/ru/docs/webhooks.md#подпись).
- [ ] Товар отдаётся, только когда `GET /v1/invoices/{id}` возвращает `confirmed` или `settled`, и один раз на `order_id`.
- [ ] У продукта есть решение для `underpaid`, `paid_late` и переплаты.
- [ ] При `partially_paid` товар не отдаётся.
- [ ] Обработчики адресов возврата и отмены проверяют подпись и сами не отдают товар и не отменяют заказ.
- [ ] Выплаты, если они есть, создаются с постоянным `reference`, а исход берут из `GET /v1/payouts/{id}`.
- [ ] Весь путь проверен на небольшой сумме, и [чек-лист перед продом](https://stons.io/ru/docs/checklist.md) пройден.
---
# Сети и токены
Этот документ перечисляет сети и токены STONS и показывает, где API сообщает интеграции, какие активы она может предложить. Прочитайте его, прежде чем выбирать токены для своего продукта.
## Что сервис делает в каждой сети
| Семейство | Сети | Приём платежей | Выплаты |
| --- | --- | --- | --- |
| TRON | TRON | да | да |
| EVM | Ethereum, BNB Smart Chain, Polygon, Base, Arbitrum, OP Mainnet, Avalanche | да | нет |
| TON | TON | да | нет |
| Solana | Solana | да | нет |
- **Эта таблица говорит, что умеет сервис. Что доступно вашему проекту прямо сейчас, говорит `GET /v1/assets`** — он учитывает каталог, переключатели оператора и настройки проекта в кабинете. Берите список оттуда, а не отсюда: сеть или токен могут выключить, например на обслуживание, и тогда их активов в ответе не будет — см. [какие активы можно предложить](#какие-активы-можно-предложить).
- Сеть в API — идентификатор из реестра сетей сервиса: `tron`, `ethereum`, `bsc`, `polygon`, `base`, `arbitrum`, `optimism`, `avalanche`, `ton`, `solana`, — вместе с названием для людей в `network_name`.
- **Выплаты сервис делает только в сети TRON.** Выплата в активе любой другой сети отклоняется с `422 asset_not_payable`.
- Что сервис делает с деньгами после их прихода — держит их и остаётся должен проекту или пересылает на собственный кошелёк проекта — на суммы, адреса и статусы инвойсов не влияет ни в одном из случаев; это описано в [куда уходят деньги](https://stons.io/ru/docs/invoices.md#куда-уходят-деньги).
## Токены
Токены — это каталог активов сервиса: контракты, число знаков после запятой, минимальные депозиты и число подтверждений берутся из него, а не из кода. Сейчас это стейблкоины USDT, USDT0 и USDC и нативные монеты сетей. Контракты, знаки и минимальные депозиты всех активов — в [активах](https://stons.io/ru/docs/invoices.md#активы).
| Токены | Где |
| --- | --- |
| USDT | `USDT_TRON`, `USDT_ETHEREUM`, `USDT_BSC`, `USDT_AVALANCHE`, `USDT_TON`, `USDT_SOLANA`. Выплаты — только в `USDT_TRON` |
| USDT0 | `USDT0_POLYGON`, `USDT0_ARBITRUM`, `USDT0_OPTIMISM`: в этих сетях USDT выпущен как USDT0, и это другой контракт |
| USDC | `USDC_ETHEREUM`, `USDC_BSC`, `USDC_POLYGON`, `USDC_BASE`, `USDC_ARBITRUM`, `USDC_OPTIMISM`, `USDC_AVALANCHE`, `USDC_SOLANA` |
| USDC в TRON: `USDC_TRON` | Выключен оператором целиком: Circle прекратила выпуск USDC в сети TRON. В новых счетах он не принимается, и выплаты в нём тоже выключены. Уже открытые счета в нём оплачиваются, а перевод на постоянный адрес зачисляется — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес) |
| Нативные монеты | `TRX_TRON`, `GRAM_TON`, `SOL_SOLANA`, `ETH_ETHEREUM`, `BNB_BSC`, `POL_POLYGON`, `ETH_BASE`, `ETH_ARBITRUM`, `ETH_OPTIMISM`, `AVAX_AVALANCHE`. Цена в долларах США переводится в монету по курсу; выплат в них нет. Засчитывается и перевод, который сделал смарт-контракт, — см. ниже |
| Другие токены | Каталог расширяет оператор, токен за токеном: контракт, число знаков, минимальный депозит и подтверждения читаются из сети, а не записаны в коде. Токен принимается к оплате, пока он либо привязан к доллару США, либо у него есть монета, по курсу которой он оценивается, — тогда цена в долларах США переводится по этому курсу. Выплатам нужна привязка, поэтому их нет в нативных монетах |
**Нативная монета EVM-сети засчитывается, отправил её кошелёк или смарт-контракт** — суммой самой транзакции или переводом, который сделал внутри неё смарт-контракт (смарт-кошелёк, мультиподпись, вывод с биржи). Перевод из контракта сервис находит трейсом блока и засчитывает, только если трейс объясняет весь рост баланса адреса. Перевод, который объяснить не удалось, автоматически не засчитывается: инвойс остаётся неоплаченным, вебхука нет. Такой перевод оператор сервиса разбирает вручную. Токен засчитывается по событию перевода, как бы его ни отправили.
## Идентификаторы и знаки после запятой
Актив — это токен в сети: `USDT_TRON` — это USDT в сети TRON. Один и тот же токен в разных сетях — разные активы, с разными контрактами и, возможно, разным числом знаков после запятой: у USDT в TRON 6 знаков, а в BNB Smart Chain — 18.
- Не вшивайте число знаков в код: у суммы в ответе API ровно столько цифр после точки, сколько знаков у её актива — см. [суммы](https://stons.io/ru/docs/invoices.md#суммы).
- Показывайте покупателю сеть вместе с активом и полным адресом — см. [чек-лист](https://stons.io/ru/docs/checklist.md#отображение). Адрес в TON записан в user-friendly форме, которую принимает кошелёк, — см. [адреса в TON](https://stons.io/ru/docs/addresses.md#адреса-в-ton).
**Адрес в Solana — base58, от 32 до 44 знаков, и он регистрозависимый.** В base58 строчные и заглавные буквы латиницы — разные символы, поэтому `So11…` и `so11…` — разные адреса. Никогда не приводите адрес этой сети к нижнему или верхнему регистру и не нормализуйте его иначе — ни в своей базе, ни в URL, ни при сравнении, ни в QR-коде. После такой правки это адрес, ключа к которому ни у кого нет, и отправленные туда деньги потеряны. Храните и сравнивайте адрес ровно таким, каким его вернул API, и показывайте целиком. То же относится к подписи транзакции Solana, которую сервис возвращает в `tx_hash`: это base58 из 86–88 знаков, а не 64 шестнадцатеричных знака, — см. [адреса в Solana](https://stons.io/ru/docs/addresses.md#адреса-в-solana).
## Какие активы можно предложить
Какие активы проект может предложить в данный момент, решают каталог сервиса, сети, которые включил оператор, переключатели оператора и настройки проекта в кабинете. API применяет их в этих местах:
| Где | Что даёт |
| --- | --- |
| `GET /v1/assets` | Все активы, в которых проект может выставить инвойс **сейчас**, с сетью, контрактом, числом знаков и минимальным депозитом — см. [список активов](https://stons.io/ru/docs/invoices.md#список-активов). Актив, выключенный оператором или настройками проекта, в ответе не появляется |
| `POST /v1/addresses`, поле `assets` | Активы, которые можно предложить пользователю для оплаты на этот адрес в момент запроса — см. [постоянный адрес пользователя](https://stons.io/ru/docs/addresses.md#постоянный-адрес-пользователя) |
| `POST /v1/invoices` без `asset` | Покупатель выбирает монету и сеть на странице оплаты из активов, которые принимает проект и для которых есть курс; цена переводится в выбранный по курсу, а `currencies` сужает список — см. [токен выбирает покупатель](https://stons.io/ru/docs/invoices.md#токен-выбирает-покупатель) |
| `GET /checkout/v1/invoices/{id}`, поле `choices` | Активы, которые покупатель может выбрать сейчас, с суммой к оплате в каждом — см. [прочитать инвойс для страницы](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы) |
Актив, которым нельзя воспользоваться, `POST /v1/invoices` отклоняет: `422 asset_not_supported` — сервис не принимает его для платежей, `422 asset_payments_disabled` — оператор выключил приём в нём, `422 asset_disabled_by_merchant` — его выключили настройки проекта — см. [коды ошибок](https://stons.io/ru/docs/errors.md#перечень).
## Когда список меняется
Новая сеть или новый токен записываются в [журнал изменений](https://stons.io/ru/docs/changelog.md) и появляются в этом документе. Но список, на который опирается ваш код, — это ответ `GET /v1/assets`: он меняется и без записи в журнале, когда оператор выключает актив или сеть, например на обслуживание. Читайте его перед тем, как показать покупателю выбор сети, и не зашивайте сети в код.
---
# Аутентификация
Этот документ описывает, как сервер мерчанта аутентифицируется в API ключом API, и связанные с ключом лимиты запросов, ротацию и сетевые правила. Прочитайте его до первого запроса.
## Ключ API
Каждый запрос к маршрутам `/v1` передаёт ключ API мерчанта в заголовке `X-Api-Key`. Единственное исключение — `GET /v1/openapi.json`, машиночитаемое описание API: ему ключ не нужен.
```sh
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44 -H "X-Api-Key: $API_KEY"
```
Ключ принадлежит мерчанту — в кабинете он называется проектом. Первый ключ владелец проекта получает при подаче заявки на проект в кабинете, следующие выпускает в разделе проекта «Ключи API» кнопкой «Выпустить новый ключ». Ключ имеет вид `sk_<16 шестнадцатеричных символов>_<43 символа base64url>`: первая часть — публичный идентификатор ключа, который кабинет показывает в списке ключей, вторая — секрет.
Сервис хранит не ключ, а хэш argon2id его секрета, и сравнивает хэши за постоянное время. Отсюда два следствия:
- утерянный ключ нельзя восстановить, только выпустить новый;
- ключ показывается один раз, в момент выпуска, — сохраните его в менеджер секретов.
Правила обращения с ключом:
- Ключ предназначен только для запросов сервер-сервер. Он не должен попадать в браузер, мобильное приложение, репозиторий или логи.
- Ключ видит только данные своего мерчанта: инвойса другого мерчанта для него не существует (`404`).
- Ключ работает с момента выпуска. Пока проект не одобрен после проверки, `POST /v1/invoices` и `POST /v1/addresses` отвечают `403 merchant_not_approved`, а чтение инвойсов работает.
- Успешную проверку ключа сервис помнит до 60 секунд. Поэтому отозванный ключ и ключ отключённого мерчанта отклоняются не позже чем через 60 секунд после изменения.
## Ошибки
Ключ проверяется раньше тела запроса, поэтому запрос с неверным ключом получает `401`, даже если и тело неверное.
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 401 | `unauthorized` | Заголовка нет, ключ неверного формата, неизвестен, отозван или секрет не совпадает. Ответ не уточняет, что именно |
| 403 | `merchant_disabled` | Ключ верный, но мерчант отключён — оператором или владельцем в кабинете |
| 429 | `rate_limited` | Превышен лимит запросов — см. [лимиты запросов](#лимиты-запросов) |
Пример: запрос без ключа.
```sh
curl -s https://api.stons.io/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
```
`401`:
```json
{"error":{"code":"unauthorized","message":"Missing or invalid X-Api-Key header"}}
```
## Лимиты запросов
Запросы к маршрутам `/v1` ограничены дважды, в окне 60 секунд: для IP-адреса клиента и для ключа API. Значения лимитов задаёт оператор установки.
- Лимит по IP проверяется первым, до ключа: в него засчитываются и запросы без ключа или с неверным ключом.
- Лимит по ключу проверяется после ключа: он считает запросы одного ключа с любых адресов.
- `GET /v1/openapi.json` и несуществующие пути не ограничены.
Ответы ограниченных маршрутов содержат заголовки:
| Заголовок | Смысл |
| --- | --- |
| `x-ratelimit-limit` | Сколько запросов разрешено в окне. Запрос, прошедший оба лимита, сообщает лимит по ключу; запрос, отклонённый лимитом, — этот лимит |
| `x-ratelimit-remaining` | Сколько запросов осталось в окне |
| `x-ratelimit-reset` | Через сколько секунд окно начнётся заново |
| `retry-after` | Только в ответе `429`: через сколько секунд повторить |
## Условные чтения
Каждое простое чтение отвечает заголовком `ETag` — метку именно этого ответа дают `GET /v1/assets`, `GET /v1/invoices`, `GET /v1/invoices/{id}` и `GET /v1/deposits/{tx_hash}`. Пришлите метку обратно в `If-None-Match`, и ответ, который не изменился, придёт как `304 Not Modified` без тела. Это нужно продукту, который опрашивает инвойс, пока покупатель платит, или проходит [ленту изменений](https://stons.io/ru/docs/invoices.md#лента-изменений) между вебхуками.
```http
GET /v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
X-Api-Key: <ваш ключ>
If-None-Match: "LXEWQrcmsEQBYnyp-6wy9chTD7GQPMTbAiWHF5IaSIE"
```
- `304` приходит с той же меткой и без тела: то, что у вас есть, актуально. `200` приходит с новой меткой — храните её рядом с ответом и присылайте со следующим запросом.
- Метка считается по всему телу, поэтому меняется от любого изменения ответа: пришёл перевод, сменился статус, пересчиталась сумма, тот же инвойс запросили вместе с QR-кодом. Это не номер версии инвойса, и у двух разных запросов об одном инвойсе метки разные.
- Метка непрозрачна: это строка в кавычках, которую надо сохранить и вернуть, читать из неё нечего. `If-None-Match: *` совпадает с любым ответом, который есть у сервиса.
- Условный запрос — это запрос: он считается в лимитах выше и стоит сервису той же работы. Экономия на вашей стороне — тело по сети и его разбор.
- Метку получает только `200`. Ошибка метки не получает, и `304` её не заменяет: запрос, который ответил бы `404`, отвечает `404`.
## Настройки проекта
Интеграция настраивает проект сама, своим ключом: указывает, куда присылать
[вебхуки](https://stons.io/ru/docs/webhooks.md), куда [страница оплаты](https://stons.io/ru/docs/checkout.md#адреса-возврата)
возвращает покупателя, и какие токены проект принимает. В кабинет заходить для
этого не нужно (ADR-0067).
Секрет подписи вебхуков эти маршруты не возвращают. Он показывается один раз,
при создании проекта; ключ API — не доступ к секрету.
### `GET /v1/project`
```json
{
"project_id": "7f2c1b94-3d5a-4e86-9a2f-6c0b1d8e4a37",
"name": "Shop",
"address_mode": "per_invoice",
"webhook_url": "https://shop.example/hooks",
"return_url": "https://shop.example/thanks",
"cancel_url": null,
"review_status": "approved"
}
```
- `address_mode` решает, что этот API принимает: `POST /v1/addresses` работает
только в `per_user`, а счёт в `per_user` без `external_user_id` отклоняется —
см. [адреса](https://stons.io/ru/docs/addresses.md).
- `review_status` = `pending` или `rejected` означает, что счета и постоянные
адреса отвечают `403 merchant_not_approved`: проект ещё не одобрен.
### `PATCH /v1/project`
Меняет одно или несколько полей; пустое тело и любое другое поле — `400`:
```http
PATCH /v1/project
X-Api-Key: <ваш ключ>
Content-Type: application/json
{ "webhook_url": "https://shop.example/hooks", "return_url": "https://shop.example/thanks" }
```
- `webhook_url` можно задать и заменить, **но не удалить**: `null` — `400`.
События, уже поставленные в очередь, должны куда-то уходить.
- `return_url` и `cancel_url`: адрес задаёт, `null` очищает, отсутствие поля
оставляет как есть. Правила адресов — у [страницы оплаты](https://stons.io/ru/docs/checkout.md#адреса-возврата).
- Режим адресов этим маршрутом не меняется: его разрешает оператор.
- Ответ — объект проекта после изменения. Изменение попадает в журнал аудита с
именем ключа, которым сделано.
### `GET /v1/project/assets`
Какие активы проект принимает, с двумя переключателями — платформы и своим:
```json
{
"assets": [
{
"asset": "USDT_TRON",
"network": "tron",
"symbol": "USDT",
"decimals": 6,
"min_deposit": "1000000",
"platform_enabled": true,
"enabled": true,
"accepting": true
}
]
}
```
`accepting` — можно ли создать счёт в этом активе сейчас: платформа принимает
его **и** проект его не выключил.
### `PUT /v1/project/assets`
Выбирает принимаемые активы. Перечислять можно только те, что сервис принимает
к оплате; от одного до пятидесяти, каждый не больше одного раза:
```http
PUT /v1/project/assets
X-Api-Key: <ваш ключ>
Content-Type: application/json
{ "assets": [{ "asset": "USDT_TRON", "enabled": true }, { "asset": "TRX_TRON", "enabled": false }] }
```
Актив, которого сервис не принимает, — `422 asset_not_supported`, и из запроса
не записывается ничего. Ответ — весь список после изменения. Переключатель,
который ничего не меняет, аудита не создаёт.
## Ротация
У мерчанта может быть несколько действующих ключей одновременно, поэтому ключ заменяется без простоя:
1. Владелец проекта выпускает новый ключ в разделе проекта «Ключи API» в кабинете.
2. Продукт переключается на новый ключ.
3. Владелец там же отзывает старый ключ. API отклоняет отозванный ключ не позже чем через 60 секунд после отзыва. Отозванный ключ не восстанавливается.
## Сеть
API мерчантов отвечает по адресу `https://api.stons.io/v1`. В боевой установке перед ним стоит HTTPS-прокси:
- из интернета доступны только пути, начинающиеся с `/v1/`; любой другой путь отвечает `404` с пустым телом;
- запрос по `http://` перенаправляется на `https://` кодом `308`. К моменту перенаправления заголовок с ключом уже ушёл по сети открытым текстом, поэтому в клиенте указывайте адрес с `https://`.
Маршруты `/admin/v1`, `/cabinet/v1` и `/checkout/v1` обслуживают админку сотрудников, приложение кабинета и сервер страницы оплаты STONS. Они не часть интеграции мерчанта и из интернета недоступны.
---
# Инвойсы
Этот документ описывает инвойсы: как создать инвойс через `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` у `
`: ни библиотеки QR на вашей стороне, ни второго запроса.
```html
```
- В коде лежит адрес оплаты `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`: только эта сумма закрывает инвойс.
---
# Адреса для приёма платежей
Этот документ описывает адреса для приёма платежей: два режима адресов, как `POST /v1/addresses` выдаёт постоянный адрес пользователю мерчанта в режиме `per_user` и что происходит с каждым видом перевода, пришедшего на адрес. Прочитайте его до выбора режима адресов и до того, как показывать адрес покупателю.
## Режимы
Режим выбирает владелец при создании проекта (мерчанта) в кабинете: там предлагается `per_invoice`. `per_user` — ограниченный режим, его разрешает оператор по проектам; после разрешения владелец переключается на него в кабинете (ADR-0066). Смена решает, где будет собран следующий платёж: уже выданные адреса остаются при своём режиме, зачисляются и переносятся как раньше.
| Режим | Адрес | Когда подходит |
| --- | --- | --- |
| `per_invoice` | Новый адрес под каждый инвойс, выдаётся при создании инвойса; у инвойса без токена — когда покупатель выбрал токен. Адрес однозначно определяет инвойс | Оплата конкретного заказа |
| `per_user` | Один постоянный адрес на каждого пользователя мерчанта (`external_user_id`), выдаётся первым `POST /v1/addresses` для этого пользователя или когда адрес получает его первый инвойс | Пополнение баланса и повторные платежи на один адрес |
- В режиме `per_invoice` адрес приходит в ответе на `POST /v1/invoices`, а `POST /v1/addresses` отклоняется с `409 address_mode_mismatch`.
- В режиме `per_user` адрес пользователя и адрес каждого его инвойса — один и тот же адрес. Какой инвойс оплачивает перевод, решает сумма — см. [режим per_user](https://stons.io/ru/docs/invoices.md#режим-per_user).
- Платёж никогда не сопоставляется с заказом по «уникальной сумме» с копейками: решает адрес.
### Режим на паузе
Платформа может на время приостановить режим адресов. Режим на паузе останавливает только новое в нём, и каждый такой запрос получает `422 address_mode_disabled`:
- новый проект в этом режиме;
- у проекта в этом режиме: `POST /v1/addresses` — и для нового постоянного адреса, и для адреса, выданного раньше; `POST /v1/invoices` — с `asset` и без него; выбор токена на странице оплаты, `POST /checkout/v1/invoices/{id}/select`. Пока режим на паузе, страница не предлагает такому инвойсу ни одного токена.
Повтор `POST /v1/invoices` с `order_id` инвойса, созданного до паузы, возвращает этот инвойс. Всё, что касается денег в пути, идёт как обычно: перевод на выданный раньше адрес, в том числе постоянный, записывается, зачисляется и приходит вебхуками; инвойсы, у которых уже есть адрес, оплачиваются и закрываются; `GET /v1/invoices/{id}` и `GET /v1/deposits/{tx_hash}` отвечают. Сообщите покупателю, что оплата сейчас недоступна, и повторите позже: ответ не изменится, пока платформа не включит режим обратно.
## Постоянный адрес пользователя
`POST /v1/addresses` возвращает постоянный адрес пользователя мерчанта в режиме `per_user` и создаёт его при первом запросе.
### Запрос
Тело — JSON-объект с `Content-Type: application/json`, не больше 16 КиБ. Поле, которого нет в таблице, отклоняется с `400 validation_failed`.
| Поле | Тип | Обязательно | Описание |
| --- | --- | --- | --- |
| `external_user_id` | string | да | Идентификатор пользователя на стороне мерчанта: 1–200 видимых ASCII-символов (`0x21`–`0x7E`) без пробелов |
| `network` | string | да | Сеть адреса: идентификатор включённой сети из реестра сервиса, в которой сервис выдаёт адреса. Сети предлагаемых активов перечисляет `GET /v1/assets` — см. [список активов](https://stons.io/ru/docs/invoices.md#список-активов). Сейчас это только `tron` |
### Ответ
`200` — и для нового адреса, и для существующего.
| Поле | Тип | Описание |
| --- | --- | --- |
| `address` | string | Адрес целиком: в TRON — base58, в EVM-сетях — `0x` и 40 шестнадцатеричных цифр в нижнем регистре, в TON — user-friendly форма: 48 символов base64url, начинается с `UQ` (`0Q` в тестовой сети). EVM-адреса сравнивайте без учёта регистра, а людям показывайте с контрольной суммой EIP-55. Адрес TON показывайте людям ровно таким, каким он пришёл, — см. [адреса в TON](#адреса-в-ton) |
| `network` | string | Сеть из запроса |
| `assets` | array of string | Активы, которые мерчант может предлагать пользователю для оплаты на этот адрес в момент запроса, отсортированные по идентификатору. Актив попадает в список, если сервис принимает его к оплате, оператор не выключил в нём приём и он не выключен в настройках проекта. В боевой сети в списке для `tron` сейчас может быть только `USDT_TRON`: приём в `USDC_TRON` выключен; для `ton` — `USDT_TON`, а `GRAM_TON` — после того как платформа включила приём в нём — см. [активы](https://stons.io/ru/docs/invoices.md#активы) |
- Повторный запрос с тем же `external_user_id` возвращает тот же адрес. Одновременные запросы для одного пользователя тоже получают один адрес.
- `assets` показывает, что предлагать, а не что зачисляется: перевод в активе, выпавшем из списка, всё равно зачисляется — см. [что приходит на адрес](#что-приходит-на-адрес).
### Ошибки
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 400 | `validation_failed` | Поле отсутствует, лишнее, неверного типа или формата; `network` — не сеть, в которой сервис выдаёт адреса |
| 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 | `address_mode_mismatch` | Мерчант работает в режиме `per_invoice` |
| 422 | `address_mode_disabled` | Платформа приостановила режим `per_user` — см. [режим на паузе](#режим-на-паузе). Проверяется после одобрения |
| 429 | `rate_limited` | Превышен лимит запросов |
| 500 | `internal_error` | Необработанная ошибка сервиса |
| 503 | `addresses_exhausted` | Закончились индексы деривации адресов: заняты все 2^31. Практически недостижимо |
| 503 | `service_unavailable` | Сервис не может вывести новый адрес в текущей конфигурации; сообщение называет причину |
Что делать клиенту при каждом коде — см. [коды ошибок](https://stons.io/ru/docs/errors.md).
### Пример
```sh
curl -s -X POST https://api.stons.io/v1/addresses \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"external_user_id": "user_42", "network": "tron"}'
```
`200`:
```json
{"address":"TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH","network":"tron","assets":["USDT_TRON"]}
```
Адрес в примере условный: у каждой установки свои адреса, выведенные из её собственного ключа.
## Откуда берутся адреса
- Каждый адрес выводится из мастер-ключа сервиса по стандарту BIP44: по пути `m/44'/195'/0'/0/{index}` в TRON и `m/44'/60'/0'/0/{index}` в EVM-сетях. В TON адрес — контракт кошелька Wallet V5R1 одного публичного ключа пути `m/44'/607'/0'`, а индекс записан в идентификатор кошелька; адрес зависит ещё и от сети — mainnet или testnet. В Solana адрес — это сам публичный ключ пути `m/44'/501'/0'/{index}'`, в котором каждый шаг hardened: SLIP-0010 выводит ключи ed25519 только по hardened-путям.
- Адрес принадлежит семейству сетей, а не одной сети: один EVM-адрес принимает во всех EVM-сетях. Поэтому у пользователя `per_user` один адрес на все EVM-сети и отдельный в TRON, а повторный `POST /v1/addresses` для другой EVM-сети возвращает тот же адрес.
- Индексы выдаются строго по порядку внутри семейства и никогда не используются повторно, даже если адрес так и не получил платежа. Два пользователя или два инвойса никогда не получают один адрес.
- Сервер API знает только публичную часть мастер-ключа: он умеет выводить адреса, но не может потратить средства с них. В Solana он знает ещё меньше: расширенного публичного ключа для такого ключа не существует, поэтому сервер API только выдаёт публичные ключи, которые сервер подписи вывел и опубликовал ему заранее, и сам вычислить ни один адрес не может.
- Адреса выдаются в сетях, которые включил сервис, — см. [сети и токены](https://stons.io/ru/docs/networks.md). Показывайте покупателю сеть вместе с адресом.
## Адреса в TON
У аккаунта TON две записи: raw — `0:` и 64 шестнадцатеричные цифры — и user-friendly — 48 символов base64url с контрольной суммой. API везде отдаёт в `address` user-friendly форму — в `POST /v1/addresses`, в инвойсах, в `GET /v1/deposits/{tx_hash}` и в вебхуках `deposit.*`: её принимают и проверяют кошельки.
- Адрес без флага bounce: начинается с `UQ`, в тестовой сети — с `0Q`. Адрес приёма не развёрнут, пока сервис не перенесёт с него деньги, и на такой адрес кошелёк отправляет монеты так, чтобы они остались; сообщение с флагом bounce на неразвёрнутый адрес возвращает их отправителю, и инвойс не оплачивается.
- Показывайте адрес покупателю и кладите в QR-код ровно в том виде, в каком его вернул API: в другую форму не переводите.
- Платежу в TON не нужен комментарий (memo): чей это платёж, говорит сам адрес. Комментарий, если покупатель его добавит, ничего не меняет.
- Токен в TON лежит на отдельном жетон-кошельке владельца. Покупатель отправляет токен на адрес приёма как обычно, а сервис находит его на жетон-кошельке этого адреса.
## Адреса в Solana
Адрес Solana — публичный ключ, записанный в base58: от 32 до 44 знаков, и **регистрозависимо**. В base58 строчные и заглавные буквы латиницы — разные символы, поэтому адрес, отличающийся только регистром, — это другой адрес, ключа к которому ни у кого нет.
- **Никогда не приводите адрес этой сети к нижнему или верхнему регистру** и не нормализуйте его иначе: ни в своей базе, ни в URL, ни перед сравнением, ни в QR-коде. Храните и сравнивайте его побайтово таким, каким его вернул API, и показывайте целиком.
- **Подпись транзакции Solana — тоже base58**: 86–88 знаков, а не 64 шестнадцатеричных знака. Сервис отдаёт её в `tx_hash` — в `GET /v1/deposits/{tx_hash}` и в вебхуках `deposit.*`, — и в колонку, рассчитанную на шестнадцатеричный хэш, она не поместится. Подписи сравнивайте с учётом регистра.
- **Токен в Solana лежит не на адресе владельца.** Он лежит в associated token account — отдельном аккаунте на пару (владелец, mint), адрес которого выводится детерминированно. Этот аккаунт сервис создаёт сам при выдаче адреса, поэтому кошелёк покупателя отправляет токен на адрес приёма как обычно и знать о нём ничего не должен.
- **Следствие, важное, если вы проверяете платёж сами:** `getSignaturesForAddress` на адрес приёма перевод токена SPL не покажет вовсе. Подписи индексируются по аккаунтам, которые затронула транзакция, а перевод токена затрагивает associated token account, а не адрес владельца. Сам адрес показывает только переводы нативной монеты SOL, которая действительно меняет его собственный баланс. Это свойство метода, а не провайдера. Вместо чтения цепи своими руками используйте `GET /v1/deposits/{tx_hash}` — см. [проверить перевод](#проверить-перевод).
- Адресу Solana не нужна активация, и своей аренды он не держит: комиссию перевода с него платит отдельный аккаунт сервиса, поэтому никакой монеты на адрес приёма заранее отправлять не нужно.
## Что приходит на адрес
Сервис следит за каждым выданным адресом всегда, в том числе после дедлайна инвойса. Перевод на адрес обрабатывается по этим правилам, в этом порядке:
1. **Токен, которого сервис не знает** (контракт, которого нет в его каталоге активов), игнорируется и не записывается. Среди таких токенов бывают контракты-ловушки: не взаимодействуйте с ними.
2. **Сумма меньше минимального депозита своего актива** — пыль: записывается, мерчанту не зачисляется, вебхука нет. Исключение — остаток инвойса на адресе `per_invoice`: перевод в активе инвойса больше нуля — платёж, если по инвойсу уже пришёл платёж, а `amount_expected` ещё не набран (см. [частичная оплата](https://stons.io/ru/docs/invoices.md#частичная-оплата)).
3. **Адрес `per_invoice`:**
- перевод в активе инвойса — платёж по этому инвойсу, до дедлайна или после — см. [статусы](https://stons.io/ru/docs/invoices.md#статусы);
- перевод в другом активе из каталога — например `USDC_TRON` на адрес инвойса в `USDT_TRON` или `TRX_TRON` — записывается как `unexpected_asset`: не зачисляется, инвойс не меняется, вебхука нет.
4. **Адрес `per_user`:**
- перевод в активе, который сервис принимает к оплате (`USDT_TRON`, `USDC_TRON`, а `TRX_TRON` — после того как платформа включила приём в нём), оплачивает подходящий инвойс или пополняет баланс мерчанта — см. [режим per_user](https://stons.io/ru/docs/invoices.md#режим-per_user). Он зачисляется, даже если оператор или настройки проекта выключили актив или платформа приостановила режим: выключатели останавливают новые инвойсы, адреса и проекты и убирают актив из `assets`, но не зачисление;
- перевод в активе, который сервис к оплате не принимает (`TRX_TRON`, пока платформа не включила приём в нём), записывается как `unexpected_asset`: не зачисляется, вебхука нет.
5. **Нативная монета EVM-сети** подчиняется этим правилам, только пока платформа принимает её к оплате, отправил её кошелёк или смарт-контракт (смарт-кошелёк, мультиподпись, вывод с биржи). Перевод из контракта находится трейсом его блока и записывается, только если трейс объясняет весь рост баланса адреса. Перевод, который объяснить не удалось, не зачисляется и не попадает в ответ `GET /v1/deposits/{tx_hash}`, вебхука нет: его разбирает оператор вручную. Пока платформа нативную монету EVM-сети не принимает, её переводы не записываются вовсе.
6. **GRAM в TON** подчиняется этим правилам, только если пришёл обычным переводом с кошелька — сообщением без тела или с текстовым комментарием — и только пока платформа принимает его к оплате. GRAM, пришедший с сообщением другого вида, например тот, что перевод токена несёт на оплату уведомления, не зачисляется, вебхука нет.
Переводы `unexpected_asset` владелец проекта видит в кабинете. Сервис их не зачисляет; чтобы разобраться с таким переводом, обратитесь к оператору.
## Проверить перевод
`GET /v1/deposits/{tx_hash}` возвращает все переводы одной транзакции на адреса мерчанта, как их записал сервис. По нему действуют, получив вебхук `deposit.*`, в том числе о пополнении `per_user` без инвойса, вместо того чтобы доверять телу вебхука, — см. [главное правило](https://stons.io/ru/docs/webhooks.md#главное-правило). Маршрут работает в обоих режимах адресов, и когда платформа приостановила режим, тоже.
### Запрос
`tx_hash` в пути — хэш транзакции так, как его пишет сеть, в любом регистре: в TRON и TON — 64 шестнадцатеричных символа без `0x`, в EVM-сетях — `0x` и 64 шестнадцатеричных символа. Берите его из `data.tx_hash` вебхука: сервис ищет ровно эту форму, `0x` не добавляет и не отрезает. Тела у запроса нет.
В TON перевод — это цепочка транзакций, по одной на контракт. `tx_hash` — хэш транзакции, в которой деньги получил адрес приёма, а у токена — жетон-кошелёк этого адреса; это не хэш транзакции, который показывает кошелёк отправителя. Обозреватели TON пишут его шестнадцатеричными цифрами; если API отдаёт его в base64, его нужно перевести.
### Ответ
`200` с массивом `deposits`: переводы транзакции на адреса этого мерчанта по порядку `log_index`. В одной транзакции бывает несколько переводов: нужный из вебхука ищите по `log_index`. Переводы той же транзакции на адреса других мерчантов в список не попадают.
| Поле | Тип | Описание |
| --- | --- | --- |
| `tx_hash` | string | Хэш транзакции в нижнем регистре |
| `log_index` | integer | Номер перевода внутри транзакции: вместе с `tx_hash` определяет перевод. У перевода нативной монеты события нет, и номер отрицательный: `-1` — сумма самой транзакции, а в TRON и EVM-сетях `-2`, `-3` и далее — переводы, сделанные контрактами внутри неё. В TON у перевода токена `0`, у перевода GRAM `-1` |
| `asset` | string | Актив |
| `network` | string | Сеть актива, например `tron` |
| `amount` | string | Сумма перевода со всеми знаками актива |
| `address` | string | Адрес мерчанта для приёма, целиком, в том же виде, что в `POST /v1/addresses` |
| `invoice_id` | string (UUID) или null | Инвойс, который оплачивает перевод; `null` у пополнения `per_user` и у перевода, который не является платежом |
| `external_user_id` | string или null | Пользователь адреса `per_user`; `null` у адреса `per_invoice` |
| `kind` | string | Чем сервис счёл перевод: `payment` — оплата инвойса или пополнение `per_user`; `unexpected_asset` — актив, которого адрес не ждёт; `dust` — меньше минимального депозита и не остаток начатой оплаты инвойса. См. [что приходит на адрес](#что-приходит-на-адрес) |
| `status` | string | `seen` — перевод в блоке, подтверждений ещё не хватает; `confirmed` — набрано столько подтверждений, сколько требует актив; `orphaned` — перевод исчез из цепи при реорганизации или его транзакция оказалась неуспешной |
| `confirmations` | integer | Сколько блоков набрано над блоком перевода, как сервис видел это последний раз |
| `block_number` | string | Номер блока с переводом; в TON — номер (seqno) блока мастерчейна, в который вошла транзакция |
| `block_time` | string | Время этого блока, ISO 8601 в UTC |
| `credited` | boolean | `true`, если реестр держит сумму перевода на балансе мерчанта. Комиссия сервиса удерживается с баланса отдельной проводкой |
| `explorer_url` | string или null | Страница транзакции в обозревателе сети; `null`, если у сети нет ссылки на обозреватель |
- `tx_hash`, `log_index`, `asset`, `amount`, `address`, `invoice_id`, `external_user_id` и `block_time` называются и заполняются так же, как в данных вебхуков `deposit.*`. `confirmations` в ответе — текущая глубина, а не глубина в момент события.
- Зачисляйте пополнение пользователю, только когда у его перевода `kind` = `payment`, `status` = `confirmed` и `credited` = `true`. Товар по заказу отдавайте по статусу инвойса из `GET /v1/invoices/{id}`, а не по отдельному переводу.
- Перевода в блоке, который сервис ещё не просканировал, в ответе нет: ответ `404`, а запрос позже его найдёт.
- Мерчант, который ещё не одобрен, читать свои переводы может: этот маршрут `merchant_not_approved` не возвращает.
### Ошибки
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 400 | `validation_failed` | `tx_hash` — не хэш транзакции: ни 64 шестнадцатеричных символа, ни `0x` и 64 символа |
| 401 | `unauthorized` | Нет ключа API или он неверный — см. [аутентификацию](https://stons.io/ru/docs/authentication.md) |
| 403 | `merchant_disabled` | Мерчант отключён |
| 404 | `deposit_not_found` | В транзакции нет перевода на адрес этого мерчанта: сервис её не знает, она шла только на адреса других мерчантов или её блок ещё не просканирован. Ответ во всех трёх случаях одинаковый |
| 429 | `rate_limited` | Превышен лимит запросов |
| 500 | `internal_error` | Необработанная ошибка сервиса |
### Пример
```sh
curl -s https://api.stons.io/v1/deposits/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c \
-H "X-Api-Key: $API_KEY"
```
`200`:
```json
{
"deposits": [
{
"tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"log_index": 0,
"asset": "USDT_TRON",
"network": "tron",
"amount": "25.500000",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"invoice_id": null,
"external_user_id": "user_42",
"kind": "payment",
"status": "confirmed",
"confirmations": 20,
"block_number": "75231904",
"block_time": "2026-09-11T20:04:38.000Z",
"credited": true,
"explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
}
]
}
```
Хэш, адрес и блок в примере условные.
## Показ адреса
Показывайте адрес целиком, без сокращений вида «первые и последние символы через многоточие». Злоумышленники рассылают переводы с адресов, у которых совпадают первые и последние символы, в расчёте на то, что человек сверит только их (address poisoning).
---
# Страница оплаты
Страница оплаты — собственная страница сервиса, на которой покупатель оплачивает инвойс: она показывает сумму, адрес и статус платежа, а когда у инвойса появляется исход — возвращает покупателя на сайт мерчанта подписанным редиректом. Прочитайте этот документ, если отправляете покупателей по `checkout_url`: в нём — что делает страница, адреса возврата, подпись редиректа и маршруты, которыми пользуется сервер страницы.
## Главное правило
> **Не отдавайте товар по редиректу.** Получив покупателя на `return_url`, проверьте подпись, затем запросите инвойс `GET /v1/invoices/{id}` и отдавайте товар только по ответу API — так же, как после вебхука.
Подпись доказывает одно: адрес собрал сервис для этого инвойса в этом статусе. Она не мешает покупателю открыть ту же ссылку ещё раз, пока её `ts` ещё принимается, и не заменяет проверку статуса.
## Как это работает
1. Продукт создаёт инвойс: `POST /v1/invoices`. Поле ответа `checkout_url` — ссылка на страницу оплаты этого инвойса. Если в установке нет страницы оплаты, поле равно `null` — см. [инвойсы](https://stons.io/ru/docs/invoices.md#создать-инвойс).
2. Продукт отправляет покупателя по `checkout_url`.
3. У инвойса, созданного без `asset`, страница сначала даёт покупателю выбрать сеть, затем монету — см. [выбор токена](#выбор-токена). Затем показывает сумму, адрес и статус и обновляет их, пока покупатель платит.
4. Когда у инвойса появляется исход, страница отправляет покупателя на адрес возврата или отмены мерчанта с параметрами и подписью. Пока инвойс не оплачен, кнопка «Вернуться в магазин» ведёт на адрес отмены с теми же параметрами и подписью.
5. Продукт проверяет подпись и берёт статус из API.
Вебхуки при этом приходят так же, как без страницы.
## Страница для покупателя
Страница открывается по `checkout_url` без входа и без ключей. Другой сайт не может встроить её во фрейм, и страница не передаёт сайту мерчанта `Referer`.
В статусе `created` страница показывает:
- название мерчанта;
- статус «Ожидаем оплату»;
- время до срока оплаты;
- сумму к оплате — `amount_expected` — с тикером и сетью, с кнопкой копирования;
- сеть: её знак по `network_icon_slug`, название `network_name` и стандарт её токенов `token_standard`, например «BNB Smart Chain · Токен BEP-20». У нативной монеты сети — BNB, ETH, TRX, GRAM, SOL — стандарта токенов нет: она не токен, и строки «Токен …» страница не показывает;
- под «Подробнее», свёрнуто: курс котировки; если комиссию сервиса платит покупатель — разбивку «Цена · Комиссия сервиса · Итого»: `amount_requested`, комиссию со ставкой и `amount_expected`; показанная комиссия — `amount_expected` минус `amount_requested`;
- там же — уже полученную сумму, если `amount_received` больше нуля;
- предупреждение отправлять ровно этот актив и в этой сети, одной строкой вместе с тем, что комиссия сети — сверх суммы;
- QR-код;
- адрес целиком, с кнопкой копирования;
- в TON — кнопку «Открыть в кошельке»: ссылку `wallet_url`, которая открывает приложение кошелька с уже заполненными адресом, суммой и токеном;
- кнопку «Вернуться в магазин», если у инвойса или проекта есть адрес отмены.
Сумма, сеть и адрес стоят в одной карточке друг под другом. Адрес занимает свою строку во всю ширину карточки, кнопка копирования рядом с ним — значок; в колонке шириной 512 px адрес до 48 знаков помещается в одну строку, на узком экране переносится. QR-код стоит под адресом. На экране меньше 640 px он скрыт за кнопкой «Показать QR-код»: телефон не сканирует собственный экран; на экране шире код показан сразу. Время до срока — справа в строке с названием мерчанта.
Суммы страница пишет без нулей в конце: `49.900000` показывается и копируется как `49.9`.
Когда на инвойс пришли переводы, страница перечисляет их под заголовком «Полученные переводы»: сумма, подтверждения из нужных, хэш транзакции целиком с кнопкой копирования и ссылка на транзакцию в обозревателе сети. Ссылку страница показывает, только если это адрес `https`; без неё остаётся хэш.
QR-код содержит только адрес. Кошелёк, считавший его, не получает ни суммы, ни токена. Поэтому сумма показывается и копируется отдельно, а страница просит покупателя сверить адрес целиком после вставки. Адрес нигде не сокращается. В TON кнопка «Открыть в кошельке» передаёт ещё и сумму с токеном — ссылкой `ton://transfer` из документации TON; покупатель всё равно сверяет их в кошельке перед подтверждением.
Страница бывает на английском и на русском. Чтобы открыть её на нужном языке, добавьте к `checkout_url` параметр `lang`: `?lang=en` или `?lang=ru`; страница запоминает этот выбор в cookie. Без параметра страница берёт по порядку запомненный выбор, язык браузера и английский. Переключить язык покупатель может внизу страницы.
Страница сама обновляет статус, перезагружать её не нужно:
- первый запрос — через 3 секунды, второй — через 5, дальше каждые 10;
- после смены статуса отсчёт начинается заново с 3 секунд;
- пока вкладка скрыта, запрос идёт раз в 30 секунд;
- когда покупатель возвращается на вкладку, следующий запрос идёт через 3 секунды;
- неудачный запрос не меняет экран, и страница продолжает спрашивать.
Время до срока считается по часам сервера страницы, а не устройства покупателя. Без JavaScript страница не обновляется, не ведёт отсчёт, не копирует и не переводит на сайт.
Что страница показывает по статусам:
| Статус | Что видит покупатель |
| --- | --- |
| `created` | Перечисленное выше |
| `seen` | «Платёж замечен», число подтверждений сети из нужных, оценку оставшегося ожидания по `confirmation_estimate` — «Осталось около N минут», «Осталось меньше минуты» или «Почти готово» — и время до срока. Своего времени для сети страница не пишет: сколько ждать, зависит от числа подтверждений актива и времени блока сети, а их меняет администратор. В сети, которая ждёт ещё и окончательного блока, страница добавляет, что может выйти немного дольше. Страница не пишет, что больше ничего отправлять не нужно: если покупатель отправил не всю сумму, остаток можно дослать на тот же адрес до конца срока, а после подтверждения страница покажет, сколько осталось. Адрес, QR-код и кнопка «Вернуться в магазин» остаются; сумма счёта — для справки, без кнопки копирования и без разбивки, а кнопки «Открыть в кошельке» нет: пришедший перевод может уже покрывать сумму |
| `partially_paid` | «Пришла часть суммы» и время до срока. Вместо суммы к оплате — «Осталось доплатить»: `amount_remaining` с кнопкой копирования; сумма счёта и уже полученная сумма — под «Подробнее». Адрес, QR-код и кнопка «Вернуться в магазин» остаются; в TON «Открыть в кошельке» подставляет `amount_remaining`. Короткая подсказка под предупреждением: при выводе с биржи выводить с запасом на её комиссию за вывод, чтобы на адрес пришёл весь остаток |
| `confirmed`, `settled` | «Оплата получена» |
| `paid_late` | «Платёж пришёл после срока»: что делать с заказом, решает магазин |
| `underpaid` | «Сумма пришла не полностью». Если адреса отмены нет — ещё просьбу не отправлять остаток на этот адрес и связаться с магазином |
| `expired` | «Срок оплаты истёк» и просьбу не отправлять деньги повторно, если перевод уже ушёл, а связаться с магазином |
В статусе с исходом — `confirmed`, `settled`, `paid_late`, `underpaid` или `expired` — страница скрывает сумму, адрес и QR-код и перестаёт обновляться. Список переводов остаётся, а вместо адреса появляется ссылка на адрес оплаты в обозревателе сети. Дальше:
- **Есть адрес нужного вида** (см. [адреса возврата](#адреса-возврата)) — страница отсчитывает 5 секунд и переводит покупателя на сайт мерчанта. Кнопка переводит сразу: «Перейти в магазин» — на адрес возврата, «Вернуться в магазин» — на адрес отмены. Прямо перед переходом страница запрашивает заново подписанный адрес, а не берёт полученный раньше; если запрос не удался, страница останавливается и просит нажать кнопку ещё раз. Так же страница ведёт себя, если покупатель открыл её у инвойса, у которого исход уже есть.
- **Адреса нужного вида нет** — страница показывает исход и оставляет покупателя на месте.
Пока у инвойса нет исхода, страница никуда не переводит покупателя сама. Кнопка «Вернуться в магазин» при нажатии запрашивает заново подписанный адрес отмены. Если за это время у инвойса появился исход, страница показывает его вместо перехода, а затем переводит, как при любом исходе.
`expired` и `underpaid` ещё могут смениться на `paid_late`, если деньги придут позже. Покупатель, открывший страницу снова, увидит новый статус; мерчант получит вебхук `invoice.paid_late`.
Несуществующий инвойс и инвойс отключённого мерчанта показывают одинаковую страницу «Счёт не найден». Страница ограничивает число запросов с одного IP-адреса в минуту и сверх лимита показывает «Слишком много запросов».
## Выбор токена
Инвойс, созданный без `asset` ([инвойсы](https://stons.io/ru/docs/invoices.md#токен-выбирает-покупатель)), пока его статус `created`, открывается экраном выбора. Сверху — название мерчанта и время до срока, под ними одной строкой сумма заказа в `USD`. Дальше:
1. **Сеть** — выпадающий список сетей, в которых API предлагает хотя бы один актив: знак и название из каталога. Сеть не выбирается заранее.
2. **Монета** — второй выпадающий список, доступный после выбора сети: только активы выбранной сети, каждый с каталожной иконкой, тикером и суммой к оплате из API по текущему курсу. Даже единственная монета требует явного выбора. Смена сети сбрасывает выбранную монету; совпадающий тикер не переносит выбор в другую сеть.
3. **Сводка** — одна строка «К оплате»: сумма в выбранной монете и название сети; под ней мелко курс и, если комиссию сервиса платит покупатель, её ставка. Ниже одна строка: отправлять только эту монету в этой сети. Суммы разбивки «Цена · Комиссия сервиса · Итого» страница показывает после выбора, под «Подробнее».
Экран не добавляет нативные монеты или другие активы, которых нет в `choices`: поддержка способа оплаты определяется ядром, а не наличием иконки.
Кнопка «Перейти к оплате» сохраняет выбор, и страница показывает экран оплаты, описанный выше. Кнопка видна на экране любой высоты: если конец формы ниже края экрана, кнопка прилипает к нижнему краю. Под кнопкой — строка о том, что после этого шага монету и сеть сменить нельзя.
- **Показанные и записанные суммы.** Сумму каждой монеты страница показывает по курсу, который API вернул вместе с ней, и отправляет `rate_id` этого курса вместе с выбором. Сервис записывает ровно показанную сумму, пока этот курс ещё свежий; иначе переводит цену по последнему свежему курсу. После выбора страница показывает записанную сумму. В токене, привязанном к доллару США, записанная сумма действует до дедлайна инвойса; в любом другом токене остаток долга может пересчитаться после окна котировки — см. [окно котировки](https://stons.io/ru/docs/invoices.md#окно-котировки).
- **Выбор окончательный.** Повторное нажатие с тем же токеном ничего не меняет. Если уже выбран другой токен, например в другой вкладке, страница показывает экран оплаты в этом токене с пометкой, что сменить его нельзя.
- **Другая вкладка.** Экран выбора раз в 10 секунд проверяет инвойс и перезагружается, если токен выбран или статус уже не `created`.
- **Срок.** Когда время до срока доходит до нуля, экран сообщает, что срок вышел, и выключает кнопку. Когда сервис переводит инвойс в `expired`, страница перезагружается, показывает «Срок оплаты истёк» и переводит на адрес отмены, если он есть.
- **Выбрать нечего.** Если ни один актив нельзя предложить — магазин сейчас не принимает ни одного из них, мерчант не одобрен, платформа приостановила режим адресов магазина или срок вышел, — страница сообщает, что оплатить сейчас нельзя, и просит не отправлять деньги. Разрешённая монета, курс которой сейчас не свежий, не предлагается, пока курс снова не станет свежим; этот случай API отмечает полем `rates_unavailable`.
- **Токен стал недоступен** к моменту нажатия — страница просит выбрать другой. Если сохранить выбор не удалось по другой причине, страница просит попробовать ещё раз через минуту.
- **Без JavaScript** интерактивный селектор скрыт; работает обычная форма с группами по сетям и вариантами монет с суммами внутри каждой группы.
## Адреса возврата
Какой адрес получает покупатель, зависит от статуса инвойса:
| Статус | Куда | Почему |
| --- | --- | --- |
| `created`, `seen`, `partially_paid` | адрес отмены — только кнопкой «Вернуться в магазин» | Инвойс ещё можно оплатить: страница не уводит покупателя сама |
| `confirmed`, `settled`, `paid_late` | адрес возврата | Инвойс оплачен; при `paid_late` платёж пришёл после срока, и решает мерчант |
| `expired`, `underpaid` | адрес отмены | Срок вышел без полной суммы |
Адреса проекта задаются в настройках проекта в кабинете — «После успешной оплаты» это адрес возврата, «Вернуться в магазин / при неудаче» — адрес отмены — или оператором. Пустое поле означает, что адреса нет. Без адреса нужного вида редиректа нет, а без адреса отмены нет и кнопки «Вернуться в магазин».
Для отдельного инвойса адрес можно переопределить полями `return_url` и `cancel_url` в `POST /v1/invoices` или в форме счёта в кабинете. Переопределение допускается, если его origin — схема, хост и порт — совпадает с origin одного из этих адресов проекта:
- адреса того же вида из настроек проекта;
- сайта проекта из заявки на проект.
Например, при сайте проекта `https://shop.example` допустимо `https://shop.example/orders/1001/paid`, а `https://pay.shop.example/…` и `https://shop.example:8443/…` — нет. На общих хостах `t.me` и `telegram.me` под одним origin живут разные боты и каналы, поэтому там совпасть должен и первый сегмент пути — имя бота или канала, без учёта регистра. При сайте проекта `https://t.me/example_shop_bot` допустимо `https://t.me/example_shop_bot?start=paid_1001`, а `https://t.me/other_bot` — нет. Адрес проекта на общем хосте без имени в пути, например `https://t.me`, не допускает ничего.
Если совпадения нет или у проекта нет ни адреса того же вида, ни сайта, ответ — `422 redirect_url_not_allowed`, и сообщение называет допустимые origin. Адреса проекта меняются в кабинете или оператором, но никогда ключом API: утёкший ключ не отправит покупателей на сайт или к боту, которых нет в проекте.
Требования к любому адресу возврата или отмены:
- начинается с `https://` в нижнем регистре;
- без символа `@` и без пробелов — так в адрес не попадают логин и пароль;
- не длиннее 2000 символов;
- без параметров `invoice_id`, `order_id`, `status`, `ts` и `signature`: их добавляет сервис.
Остальные параметры и фрагмент сохраняются как есть. Используется переопределение инвойса, если оно есть, иначе адрес проекта **на момент редиректа**: смена адреса проекта сразу касается инвойсов без переопределения.
### Переход до исхода
На адрес отмены покупатель может прийти, пока инвойс открыт: со `status=created`, `status=seen` или `status=partially_paid`, если нажал «Вернуться в магазин». Такой инвойс ещё может быть оплачен; при `seen` перевод уже замечен в сети, а при `partially_paid` часть суммы уже пришла. **Не отменяйте заказ по такому переходу**: статус берите из `GET /v1/invoices/{id}` и вебхуков, как после любого редиректа.
## Если вы переходите с Heleket
| Heleket | STONS | Как устроено здесь |
| --- | --- | --- |
| `url_success` | `return_url` | Задаётся в настройках проекта или в инвойсе. После оплаты страница переводит покупателя сама через 5 секунд, кнопкой — сразу |
| `url_return` | `cancel_url` | Задаётся там же. Пока инвойс не оплачен — кнопка «Вернуться в магазин»; после срока без полной суммы страница переводит сама |
| `lifetime` | `expires_in` | От 300 до 259200 секунд (72 часа), по умолчанию 3600 — см. [инвойсы](https://stons.io/ru/docs/invoices.md#создать-инвойс) |
| `order_id` | `order_id` | Уникален в пределах проекта: 1–200 видимых ASCII-символов. Повтор с теми же данными возвращает созданный инвойс, с другими — `409 order_id_conflict` |
| `url_callback` | адрес вебхуков проекта | Один на проект, задаётся в настройках проекта, а не в инвойсе — см. [вебхуки](https://stons.io/ru/docs/webhooks.md) |
Адреса в инвойсе допускаются только в пределах сайтов проекта — см. [адреса возврата](#адреса-возврата). К каждому редиректу сервис добавляет параметры и подпись, описанные ниже.
## Параметры редиректа
| Параметр | Значение |
| --- | --- |
| `invoice_id` | Идентификатор инвойса |
| `order_id` | Идентификатор заказа мерчанта |
| `status` | Статус инвойса в момент, когда сервис собрал адрес; после кнопки «Вернуться в магазин» — `created`, `seen` или `partially_paid` |
| `ts` | Момент, когда сервис собрал адрес, целые секунды Unix |
| `signature` | Подпись, 64 шестнадцатеричных символа в нижнем регистре |
Значения закодированы по правилам query-строки: читайте их через `URLSearchParams` или аналог, а не разбором строки вручную.
## Подпись
Подпись — HMAC-SHA256 в hex с секретом вебхуков мерчанта в качестве ключа. Подписывается строка из пяти строк, соединённых символом перевода строки `\n` (байт `0x0A`), без перевода строки в конце:
```text
redirect
```
Разделитель — перевод строки, а не точка, потому что `order_id` может содержать точки, а перевод строки не может содержать ни одно поле. Первая строка `redirect` не даёт подписи редиректа сойти за подпись вебхука тем же секретом.
Получатель обязан:
- отклонять редирект, если `ts` отличается от текущего времени больше чем на 300 секунд (5 минут);
- сравнивать подписи за постоянное время;
- проверять, что `invoice_id` — это инвойс заказа `order_id`, и брать статус из API, а не из параметра.
Проверка на Node.js:
```js
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
// query — URLSearchParams запроса, пришедшего на return_url или cancel_url.
export function verifyRedirect({ query, secret, nowMs = Date.now() }) {
const fields = ["ts", "invoice_id", "status", "order_id"].map((name) => query.get(name));
const signature = query.get("signature");
if (fields.some((value) => value === null || value.includes("\n")) || signature === null) return false;
const [ts, invoiceId, status, orderId] = fields;
if (!/^\d{1,12}$/.test(ts) || !/^[0-9a-f]{64}$/.test(signature)) return false;
if (Math.abs(nowMs / 1000 - Number(ts)) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret)
.update(["redirect", ts, invoiceId, status, orderId].join("\n"))
.digest();
return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}
```
Функция возвращает `true` только для неизменённого адреса, подписанного этим секретом не раньше чем 300 секунд назад. Секрет вебхуков показывается один раз, при создании проекта в кабинете, — см. [вебхуки](https://stons.io/ru/docs/webhooks.md#подпись).
Пример для проверки своей реализации. При секрете `example-webhook-secret` и параметрах `ts=1789300000`, `invoice_id=6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44`, `status=confirmed`, `order_id=ord_1001` подпись равна `2af98109e8fad432cc8e511f5e73f542ed3be8913b8d2863fe360f5f31f8e651`.
## Маршруты страницы оплаты
Два маршрута ниже — не для мерчантов. Их вызывает сервер страницы оплаты по приватной сети: из интернета они недоступны и в описании OpenAPI их нет.
- **Без ключа API.** Страницу открывает покупатель, у которого ключа нет; инвойс открывается его идентификатором — случайным UUID, который доходит до покупателя внутри `checkout_url`.
- **Отключённый мерчант.** Его инвойсов для этих маршрутов не существует (`404 invoice_not_found`): страница не должна продолжать собирать деньги мерчанту, которого выключил оператор или владелец.
- **Кэширование.** Успешный ответ содержит `Cache-Control: no-store`: в нём подписанные адреса.
- **Лимит запросов.** Запросы с одного IP-адреса ограничены в окне 60 секунд тем же значением, что лимит по IP у `/v1`, но отдельным счётчиком. Все запросы страницы приходят с адреса её сервера, поэтому этот лимит защищает API, но не разделяет покупателей: покупателей ограничивает сама страница.
## Прочитать инвойс для страницы
`GET /checkout/v1/invoices/{id}` возвращает инвойс в том виде, в каком его показывает страница оплаты.
### Запрос
| Параметр | Где | Тип | Обязательно | Описание |
| --- | --- | --- | --- | --- |
| `id` | путь | string (UUID) | да | `invoice_id` |
### Ответ
`200`. Все поля присутствуют всегда; поле, которое не применимо, равно `null`.
| Поле | Тип | Описание |
| --- | --- | --- |
| `invoice_id` | string (UUID) | Идентификатор инвойса |
| `merchant_name` | string | Название мерчанта, которое видит покупатель |
| `status` | string | Статус инвойса — см. [инвойсы](https://stons.io/ru/docs/invoices.md#статусы) |
| `asset` | string или null | Актив, например `USDT_TRON`; `null`, пока покупатель не выбрал токен |
| `asset_icon_slug` | string или null | Slug знака актива из `assets.icon_slug`, например `usdt0`; не выводится из тикера. `null` до выбора или если знак не задан |
| `symbol` | string или null | Тикер актива для показа: `USDT`; `null`, пока покупатель не выбрал токен |
| `network` | string или null | Идентификатор сети актива, например `tron`; `null`, пока покупатель не выбрал токен |
| `network_name` | string или null | Название сети для покупателя из реестра сетей: `TRON`; `null`, пока покупатель не выбрал токен |
| `network_icon_slug` | string или null | Slug знака сети из реестра сетей: `tron`. `null`, пока покупатель не выбрал токен или у сети нет знака; тогда страница рисует нейтральный знак с первой буквой названия |
| `token_standard` | string или null | Как кошельки называют токены сети: `TRC-20`; `null`, пока покупатель не выбрал токен, и у нативной монеты сети (у актива без контракта), которая не токен. Страница показывает сеть как «TRON (TRC-20)», а при `null` — одним названием |
| `address` | string или null | Адрес для платежа, целиком, в том виде, в каком его видит покупатель: в EVM-сетях — с контрольной суммой EIP-55, в TON — user-friendly без флага bounce, начинается с `UQ` (`0Q` в тестовой сети), см. [адреса в TON](https://stons.io/ru/docs/addresses.md#адреса-в-ton), в Solana — base58 из 32–44 знаков, регистр которых нужно сохранять, см. [адреса в Solana](https://stons.io/ru/docs/addresses.md#адреса-в-solana). `null`, пока покупатель не выбрал токен |
| `wallet_url` | string или null | Ссылка, которая открывает приложение кошелька с заполненным переводом. Только в TON: `ton://transfer/<адрес>?amount=<сумма>`, а у токена ещё `&jetton=<мастер-контракт жетона>`, как ссылку определяет документация TON (). `amount` — сколько осталось заплатить, в минимальных единицах актива: нанограммах GRAM, миллионных долях USDT; мастер-контракт — в user-friendly форме с флагом bounce. **В Solana сервис ссылки на кошелёк не отдаёт**: поле там остаётся `null`, покупатель копирует адрес и сумму со страницы, а кнопки «Открыть в кошельке» нет. `null` в других сетях, пока покупатель не выбрал токен, пока перевод ждёт подтверждений (`seen`), в статусе с исходом, после срока оплаты и пока котировка обновляется — всегда, когда точная сумма к отправке неизвестна |
| `address_explorer_url` | string или null | Страница адреса в обозревателе сети; `null`, пока покупатель не выбрал токен или у сети нет ссылки на обозреватель |
| `price` | object или null | Цена инвойса, созданного без `asset`: `amount` в долларах США с 6 знаками после точки, `currency` — `USD`. `null` у инвойса, созданного с `asset` |
| `choices` | array или null | У инвойса без токена — активы, которые можно выбрать сейчас, в порядке каталога, — см. таблицу ниже. Пустой массив — выбрать сейчас нечего: статус не `created`, срок вышел, мерчант не одобрен, платформа приостановила режим адресов мерчанта или ни один разрешённый актив не проходит проверки. `null` у инвойса с токеном |
| `rates_unavailable` | boolean | `true`, когда разрешённый актив не попал в `choices` только потому, что его курс сейчас не свежий; такой актив вернётся, как только курс станет свежим. У инвойса с токеном всегда `false` |
| `quote` | object или null | У инвойса, токен которого выбран: `rate` — курс сумм, долларов США за целую единицу токена с 18 знаками после точки; `currency` — `USD`; `expires_at` — до какого момента действуют суммы, см. [окно котировки](https://stons.io/ru/docs/invoices.md#окно-котировки). `null` у инвойса, созданного с `asset`, до выбора и у инвойса, токен которого выбран до появления курсов |
| `amount_requested` | string или null | Цена мерчанта в единицах актива; `null`, пока покупатель не выбрал токен |
| `amount_expected` | string или null | Сумма к оплате со всеми знаками актива: цена и комиссия сервиса, если её платит покупатель; `null`, пока покупатель не выбрал токен |
| `amount_received` | string или null | Сумма подтверждённых платежей; `null`, пока покупатель не выбрал токен |
| `amount_remaining` | string или null | `amount_expected` минус `amount_received`, но не меньше нуля, как в [инвойсе](https://stons.io/ru/docs/invoices.md#частичная-оплата). Страница просит его доплатить в статусе `partially_paid`; `null`, пока покупатель не выбрал токен |
| `fees` | object | Комиссии инвойса, как в [инвойсе](https://stons.io/ru/docs/invoices.md#комиссии). Разбивку страница показывает при `fees.service.payer`, равном `customer`, а `fees.network` не читает |
| `expires_at` | string | Срок оплаты |
| `confirmations_required` | integer или null | Сколько подтверждений нужно платежу в этом активе: 20 в боевой сети TRON, 1 в TON, 1 в Solana; `null`, пока покупатель не выбрал токен |
| `confirmation_estimate` | object или null | Пока перевод ждёт подтверждений (`status` у него `seen`): сколько ещё ждать самому медленному из таких переводов. `remaining_blocks` — сколько блоков не хватает до `confirmations_required`; `remaining_seconds` — эти блоки, умноженные на время блока сети из реестра сетей, с округлением вверх до целой секунды; `waits_for_finality` — `true`, если сеть засчитывает перевод, только когда его блок ещё и окончательный (тег `finalized`), и тогда ожидание может выйти дольше оценки. После нужной глубины оба числа — `0`. Это оценка: блоки идут не строго по расписанию, а сканер читает сеть проходами. `null`, когда ни один перевод подтверждений не ждёт |
| `deposits` | array | Платежи по инвойсу: `tx_hash`, `amount`, `confirmations`, `status` (`seen` или `confirmed`) и `explorer_url`, как в [инвойсе](https://stons.io/ru/docs/invoices.md#получить-инвойс) |
| `redirect` | object или null | В статусе с исходом, если у инвойса или проекта есть адрес этого вида: `kind` — `return` или `cancel`, `url` — подписанный адрес. Иначе `null` |
| `back_url` | string или null | В `created`, `seen` и `partially_paid`, если у инвойса или проекта есть адрес отмены: подписанный адрес отмены для кнопки «Вернуться в магазин». Иначе `null` |
Поля элемента `choices`:
| Поле | Тип | Описание |
| --- | --- | --- |
| `asset` | string | Идентификатор актива: значение для [сохранения выбора](#сохранить-выбор-токена) |
| `asset_icon_slug` | string или null | Slug знака актива из каталога, независимо от `symbol`; `null`, если знак не задан |
| `network` | string | Сеть актива |
| `network_name` | string | Название сети для покупателя из реестра сетей |
| `network_icon_slug` | string или null | Slug знака сети; `null`, если у сети знака нет |
| `token_standard` | string или null | Как кошельки называют токены сети; `null` у нативной монеты сети |
| `symbol` | string | Тикер актива |
| `decimals` | integer | Число знаков актива |
| `amount_requested` | string | Цена, переведённая в этот актив по курсу `rate` с округлением вверх |
| `amount_expected` | string | Сумма к оплате в этом активе по курсу `rate`, с комиссией сервиса, если её платит покупатель. Совпадает с `amount_expected` инвойса после выбора по этому курсу, если фиксированная часть комиссии за это время не изменилась |
| `min_amount` | string | Минимальный депозит актива |
| `rate` | string | Курс сумм: долларов США за целую единицу актива, с 18 знаками после точки. У актива, не привязанного к доллару США, наценка платформы уже учтена |
| `rate_id` | string | Идентификатор этого курса: его отправляют вместе с [выбором](#сохранить-выбор-токена), чтобы записались показанные суммы |
| `rate_expires_at` | string | До какого момента выбор с этим `rate_id` записывается по этому курсу. Выбор, отправленный позже, переводится по последнему свежему курсу, поэтому к этому моменту страница перечитывает инвойс |
Знаки актива и сети независимы. Страница не угадывает знак токена по `symbol`: неизвестный slug даёт нейтральный знак. Отсутствующий в ответе старого API или некорректный `asset_icon_slug` страница читает как `null`, не блокируя оплату.
Подписанные адреса в `redirect` и `back_url` собираются в момент запроса, и `ts` в них — этот момент. В ответ не попадают идентификатор мерчанта, его секреты, `external_user_id`, `order_id` отдельным полем и `amount_credited`: номер заказа доходит до покупателя только внутри подписанного адреса.
### Ошибки
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 400 | `validation_failed` | `id` — не UUID |
| 404 | `invoice_not_found` | Инвойса нет, или его мерчант отключён |
| 429 | `rate_limited` | Превышен лимит запросов с IP-адреса |
| 500 | `internal_error` | Необработанная ошибка сервиса |
### Пример
```http
GET /checkout/v1/invoices/6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44
```
`200` — оплаченный инвойс, у проекта которого есть адрес возврата:
```json
{
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"merchant_name": "Shop",
"status": "confirmed",
"asset": "USDT_TRON",
"asset_icon_slug": "usdt",
"symbol": "USDT",
"network": "tron",
"network_name": "TRON",
"network_icon_slug": "tron",
"token_standard": "TRC-20",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"address_explorer_url": "https://tronscan.org/#/address/TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"wallet_url": null,
"price": null,
"choices": null,
"rates_unavailable": false,
"quote": null,
"amount_requested": "49.900000",
"amount_expected": "49.900000",
"amount_received": "49.900000",
"amount_remaining": "0.000000",
"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" }
},
"expires_at": "2026-09-11T21:03:05.531Z",
"confirmations_required": 20,
"confirmation_estimate": null,
"deposits": [
{
"tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"amount": "49.900000",
"confirmations": 20,
"status": "confirmed",
"explorer_url": "https://tronscan.org/#/transaction/50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c"
}
],
"redirect": {
"kind": "return",
"url": "https://shop.example/thanks?invoice_id=6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44&order_id=ord_1001&status=confirmed&ts=1789300000&signature=2af98109e8fad432cc8e511f5e73f542ed3be8913b8d2863fe360f5f31f8e651"
},
"back_url": null
}
```
`200` — инвойс без токена, покупатель которого платит комиссию сервиса 1%, а у проекта нет адреса отмены, при курсе USDT 0.9991 USD:
```json
{
"invoice_id": "3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63",
"merchant_name": "Shop",
"status": "created",
"asset": null,
"asset_icon_slug": null,
"symbol": null,
"network": null,
"network_name": null,
"network_icon_slug": null,
"token_standard": null,
"address": null,
"address_explorer_url": null,
"wallet_url": null,
"price": { "amount": "100.000000", "currency": "USD" },
"choices": [
{
"asset": "USDT_TRON",
"asset_icon_slug": "usdt",
"network": "tron",
"network_name": "TRON",
"network_icon_slug": "tron",
"token_standard": "TRC-20",
"symbol": "USDT",
"decimals": 6,
"amount_requested": "100.090082",
"amount_expected": "101.090983",
"min_amount": "1.000000",
"rate": "0.999100000000000000",
"rate_id": "48213",
"rate_expires_at": "2026-09-16T12:05:00.000Z"
}
],
"rates_unavailable": false,
"quote": null,
"amount_requested": null,
"amount_expected": null,
"amount_received": null,
"amount_remaining": null,
"fees": {
"service": { "payer": "customer", "percent": "1.00", "fixed": null, "amount": null },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"expires_at": "2026-09-11T21:03:05.531Z",
"confirmations_required": null,
"confirmation_estimate": null,
"deposits": [],
"redirect": null,
"back_url": null
}
```
API отдаёт плоский список активов, у каждого своя сеть; группировать их по монете — дело страницы.
## Сохранить выбор токена
`POST /checkout/v1/invoices/{id}/select` записывает токен, который покупатель выбрал для инвойса, созданного без `asset`. Сервер страницы вызывает его, когда покупатель нажимает «Перейти к оплате».
### Запрос
`id` в пути — `invoice_id`. Тело — JSON-объект с `Content-Type: application/json`; поле, которого нет в таблице, отклоняется с `400 validation_failed`.
| Поле | Тип | Обязательно | Описание |
| --- | --- | --- | --- |
| `asset` | string | да | Один из `choices[].asset`, например `USDT_TRON` |
| `rate_id` | string | нет | `rate_id` того же элемента `choices`: курс, суммы по которому видел покупатель |
### Ответ
`200` с инвойсом в формате [чтения инвойса для страницы](#прочитать-инвойс-для-страницы) — уже с токеном, адресом, суммами и `quote` и с `choices`, равным `null`.
- **Выбор окончательный.** Повторный запрос с тем же активом возвращает инвойс без изменений, даже после срока, поэтому запрос, ответ на который потерялся, безопасно повторить. Другой актив получает `409 asset_already_selected`. То же у инвойса, созданного с `asset`.
- **Одновременные запросы** обрабатываются по одному: первый записывает свой актив, остальные получают `200` или `409 asset_already_selected` по правилу выше.
- **Когда выбор возможен:** статус `created`, и часы сервиса не дошли до `expires_at`.
- **Проверки** — по порядку, с теми же кодами, что при создании инвойса: мерчант одобрен; платформа не приостановила его режим адресов; актив есть в `currencies` инвойса, если у инвойса есть список; актив принимается к оплате; включён оператором и в настройках проекта; у него есть курс, и курс свежий; цена, переведённая по этому курсу, не меньше его минимального депозита.
- **Курс.** С `rate_id` цена переводится по этому курсу, если это принятый курс монеты этого актива и он ещё свежий. Иначе, как и без `rate_id`, — по последнему свежему курсу монеты. `rate_id` другой монеты или старый не ошибка: он просто не используется. Выбор записывает первую котировку, её показывает `quote` ответа, — см. [цену и курс](https://stons.io/ru/docs/invoices.md#цена-и-курс).
- **Условия.** Ставка комиссии и плательщики — записанные при создании инвойса; фиксированная часть комиссии — та, что у актива в момент выбора. `amount_expected` считается от переведённой цены так же, как у инвойса, созданного в этом активе.
- **Адрес.** Мерчанту `per_invoice` выводится новый адрес, мерчанту `per_user` — постоянный адрес пользователя из `external_user_id`.
### Ошибки
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 400 | `validation_failed` | `id` — не UUID; в теле нет `asset`, есть лишнее поле, `asset` — не идентификатор актива или `rate_id` — не идентификатор курса; сумма вместе с комиссией сервиса длиннее 78 цифр в минимальных единицах |
| 400, 413, 415 | `bad_request` | Тело — не JSON или пустое (400), больше 16 КиБ (413) или с неподдерживаемым `Content-Type` (415) |
| 403 | `merchant_not_approved` | Мерчант ещё не одобрен после проверки или отклонён |
| 404 | `invoice_not_found` | Инвойса нет, или его мерчант отключён |
| 409 | `asset_already_selected` | У инвойса уже другой актив |
| 409 | `invoice_expired` | Срок вышел, или инвойс уже не в статусе `created` |
| 422 | `address_mode_disabled` | Платформа временно приостановила режим адресов мерчанта — см. [режим на паузе](https://stons.io/ru/docs/addresses.md#режим-на-паузе) |
| 422 | `asset_not_offered` | Актива нет в `currencies` инвойса, или у него нет курса |
| 422 | `asset_not_supported` | Актив неизвестен или не принимается к оплате |
| 422 | `asset_payments_disabled` | Оператор временно выключил приём в активе |
| 422 | `asset_disabled_by_merchant` | Приём в активе выключен в настройках проекта |
| 422 | `amount_below_minimum` | Цена в активе по использованному курсу меньше его минимального депозита |
| 429 | `rate_limited` | Превышен лимит запросов с IP-адреса |
| 500 | `internal_error` | Необработанная ошибка сервиса |
| 503 | `rate_unavailable` | У монеты актива сейчас нет свежего курса. Ничего не записывается; покупатель может выбрать другой актив или попробовать позже |
| 503 | `addresses_exhausted` | Закончились индексы деривации адресов. Практически недостижимо |
| 503 | `service_unavailable` | Сервис не может вывести новый адрес в текущей конфигурации; сообщение называет причину |
### Пример
```http
POST /checkout/v1/invoices/3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63/select
Content-Type: application/json
{"asset": "USDT_TRON", "rate_id": "48213"}
```
`200` — инвойс без токена из предыдущего примера после выбора по показанному курсу:
```json
{
"invoice_id": "3b9e5c2a-7d41-4f6b-8a2e-5c1d9f0b7e63",
"merchant_name": "Shop",
"status": "created",
"asset": "USDT_TRON",
"asset_icon_slug": "usdt",
"symbol": "USDT",
"network": "tron",
"network_name": "TRON",
"network_icon_slug": "tron",
"token_standard": "TRC-20",
"address": "TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK",
"address_explorer_url": "https://tronscan.org/#/address/TSeJkUh4Qv67VNFwY8LaAxERygNdy6NQZK",
"wallet_url": null,
"price": { "amount": "100.000000", "currency": "USD" },
"choices": null,
"rates_unavailable": false,
"quote": { "rate": "0.999100000000000000", "currency": "USD", "expires_at": "2026-09-11T21:03:05.531Z" },
"amount_requested": "100.090082",
"amount_expected": "101.090983",
"amount_received": "0.000000",
"amount_remaining": "101.090983",
"fees": {
"service": { "payer": "customer", "percent": "1.00", "fixed": "0.000000", "amount": "0.000000" },
"network": { "payer": "merchant", "quote": null, "quote_rate": null, "actual": null, "status": "included" }
},
"expires_at": "2026-09-11T21:03:05.531Z",
"confirmations_required": 20,
"confirmation_estimate": null,
"deposits": [],
"redirect": null,
"back_url": null
}
```
---
# Вебхуки
Сервис сообщает мерчанту о событиях — платёж замечен, платёж подтверждён, инвойс просрочен — запросом `POST` на адрес вебхуков из настроек проекта. Событие записывается в той же транзакции базы, что и изменение, о котором оно сообщает, и отправляется после того, как эта транзакция зафиксирована.
## Главное правило
> **Не доверяйте телу вебхука.** Получив вебхук, проверьте подпись, затем запросите актуальное состояние объекта у API и действуйте по ответу API. Тело вебхука — сигнал «сходи проверь», а не источник истины.
Причина: если секрет вебхуков утечёт или в проверке подписи окажется ошибка, злоумышленник сможет прислать поддельное «оплачено». Если товар выдаётся только по ответу API, подделка ничего не даст.
Каким запросом проверять событие:
| Событие | Запрос |
| --- | --- |
| `invoice.*` | `GET /v1/invoices/{id}` с `data.invoice_id` — см. [получение инвойса](https://stons.io/ru/docs/invoices.md#получить-инвойс) |
| `deposit.*` с `data.invoice_id` | `GET /v1/invoices/{id}` с `data.invoice_id`: подтверждённый перевод есть в `deposits` с тем же `tx_hash`. Сам перевод — в `GET /v1/deposits/{tx_hash}` с `data.tx_hash` |
| `deposit.*` с `data.invoice_id` = `null` | `GET /v1/deposits/{tx_hash}` с `data.tx_hash`: найдите перевод по `data.log_index` и зачисляйте пополнение, только если у него `status` = `confirmed` и `credited` = `true`, — см. [проверку перевода](https://stons.io/ru/docs/addresses.md#проверить-перевод) |
| `payout.*` | `GET /v1/payouts/{id}` с `data.payout_id` — см. [получение выплаты](https://stons.io/ru/docs/payouts.md#получить-выплату) |
## События
| Событие | Когда приходит |
| --- | --- |
| `invoice.seen` | Статус инвойса стал `seen`: перевод, который оплачивает инвойс, попал в блок до дедлайна. Подтверждений ещё нет: товар не отдавать. После реорганизации и после `invoice.partially_paid`, когда пришёл следующий перевод, тот же инвойс может получить `invoice.seen` ещё раз — новым событием с новым `id` |
| `invoice.partially_paid` | Статус инвойса стал `partially_paid`: до дедлайна подтверждённые переводы покрыли только часть `amount_expected`, и ни один перевод не ждёт подтверждений. Товар не отдавать: покупатель может дослать остаток на тот же адрес до дедлайна — см. [частичную оплату](https://stons.io/ru/docs/invoices.md#частичная-оплата). Приходит при каждом переходе в этот статус, в том числе снова после `invoice.seen` |
| `invoice.confirmed` | Подтверждённые переводы, сделанные до дедлайна — по времени их блока, — покрыли `amount_expected`. Отдать товар, проверив статус через API |
| `invoice.overpaid` | Вместе с `invoice.confirmed`, отдельным событием с теми же `data`, если в момент подтверждения `amount_received` больше `amount_expected`. Перевод на инвойс, который уже `confirmed`, порождает только `deposit.confirmed` |
| `invoice.underpaid` | Дедлайн прошёл; подтверждённые переводы, сделанные до него, покрыли только часть `amount_expected`, а подтверждённых переводов после дедлайна нет |
| `invoice.expired` | Дедлайн прошёл без подтверждённых переводов. Приходит и для инвойса, покупатель которого не выбрал токен до дедлайна |
| `invoice.paid_late` | Дедлайн прошёл, переводы до него не покрыли `amount_expected`, а перевод, сделанный после дедлайна, подтвердился. Обычно приходит после `invoice.expired` или `invoice.underpaid` |
| `deposit.confirmed` | Платёж набрал число подтверждений, которое требует его актив. Одно событие на перевод. В режиме `per_user` приходит и для пополнения без инвойса: тогда `invoice_id` равен `null` |
| `deposit.orphaned` | Замеченный и ещё не подтверждённый платёж исчез из цепи при реорганизации или его транзакция оказалась неуспешной. Перевод перестаёт засчитываться инвойсу. Если он снова попадёт в цепь, он снова засчитывается, а при подтверждении придёт `deposit.confirmed` |
| `payout.confirmed` | Выплата подтверждена сетью: средства ушли получателю. Окончательное состояние |
| `payout.failed` | Выплата перешла в `failed`: средства **не** ушли. Либо её транзакция попала в блок неуспешной, либо сервер подписи отказался её подписывать, либо исчерпал попытки, и ни одна его транзакция уже не может попасть в блок. Выплата не повторяется автоматически — новая попытка требует нового `reference`. См. [статусы выплат](https://stons.io/ru/docs/payouts.md#статусы) |
Статусы инвойса и переходы между ними — в [описании инвойсов](https://stons.io/ru/docs/invoices.md#статусы). События `invoice.*` приходят только при смене статуса инвойса: перевод, который статус не меняет, их не порождает, а о каждом подтверждённом платеже сообщает `deposit.confirmed`. У перехода в `settled` события нет: для мерчанта это то же, что `confirmed`. Возврат в `created` после реорганизации отдельного события `invoice.*` не имеет — об исчезнувшем переводе сообщает `deposit.orphaned`. У создания инвойса и у выбора токена покупателем событий тоже нет.
Пыль — перевод меньше минимального депозита своего актива, кроме остатка начатой оплаты инвойса (см. [частичная оплата](https://stons.io/ru/docs/invoices.md#частичная-оплата)), — перевод другого актива и токен вне каталога сервиса событий не порождают.
О новых типах событий сообщает [журнал изменений](https://stons.io/ru/docs/changelog.md). На тип события, который обработчик не обрабатывает, отвечайте `2xx`: любой другой ответ заставит сервис повторять доставку, пока не кончатся попытки.
## Тело
```json
{
"id": "0b7e6f2a-8c1d-4e5f-9a3b-2c4d6e8f0a1b",
"type": "invoice.confirmed",
"created_at": "2026-09-11T20:05:41.118Z",
"data": {
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"order_id": "ord_1001",
"status": "confirmed",
"asset": "USDT_TRON",
"network": "tron",
"amount_requested": "49.900000",
"amount_expected": "49.900000",
"amount_received": "49.900000",
"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
}
}
```
Примеры в этом документе отформатированы для чтения. Приходящее тело — тот же объект JSON, но порядок ключей и пробелы в нём могут отличаться: подпись проверяйте по полученным байтам, а поля читайте по именам.
| Поле | Тип | Описание |
| --- | --- | --- |
| `id` | string (UUID) | Идентификатор события. Повторная доставка события приходит с тем же `id`: по нему обработчик отбрасывает дубли. `invoice.confirmed` и `invoice.overpaid` одного изменения — два события с разными `id` |
| `type` | string | Событие из таблицы выше |
| `created_at` | string | Когда событие записано, ISO 8601 в UTC. При повторной доставке не меняется |
| `data` | object | Объект события, поля ниже |
Поля `data` у событий `invoice.*`:
| Поле | Тип | Описание |
| --- | --- | --- |
| `invoice_id` | string (UUID) | Идентификатор инвойса |
| `order_id` | string | Идентификатор заказа мерчанта |
| `status` | string | Статус инвойса после изменения: `seen`, `partially_paid`, `confirmed`, `underpaid`, `expired` или `paid_late`. У `invoice.overpaid` — `confirmed` |
| `asset` | string или null | Актив; `null` у инвойса, срок которого вышел без выбора токена |
| `network` | string или null | Сеть актива: идентификатор сети, например `tron`, как в `network` инвойса; `null` у инвойса, срок которого вышел без выбора токена |
| `amount_requested` | string или null | Цена мерчанта со всеми знаками актива |
| `amount_expected` | string или null | Сумма, которую должен заплатить покупатель, со всеми знаками актива: цена и комиссия сервиса, если её платит покупатель |
| `amount_received` | string или null | Сумма подтверждённых переводов — сделанных до дедлайна и после — на момент события |
| `amount_credited` | string или null | `amount_received` за вычетом `fees.service.amount` |
| `fees` | object | Комиссия сервиса — `payer`, `percent`, `fixed` и удержанное `amount` — и строка комиссии сети, как в [инвойсе](https://stons.io/ru/docs/invoices.md#комиссии) |
| `external_user_id` | string или null | Пользователь мерчанта или `null` |
В данных нет `address`, `price`, `currencies`, `amount_remaining`, `checkout_url`, `expires_at` и `deposits`: они есть в `GET /v1/invoices/{id}`.
Инвойс, токен которого выбирает покупатель ([инвойсы](https://stons.io/ru/docs/invoices.md#токен-выбирает-покупатель)), до выбора может получить только `invoice.expired`. В нём `asset`, `network`, `amount_requested`, `amount_expected`, `amount_received`, `amount_credited`, `fees.service.fixed` и `fees.service.amount` — `null`. Отдельного события о выборе токена нет: выбранный токен виден в `GET /v1/invoices/{id}` и в следующих событиях.
Комиссия платежа проводится в той же транзакции, что его подтверждение, поэтому `invoice.confirmed` уже несёт удержанную комиссию и `amount_credited` после неё.
Поля `data` у событий `deposit.*`:
| Поле | Тип | Описание |
| --- | --- | --- |
| `tx_hash` | string | Хэш транзакции: в TRON и TON — 64 шестнадцатеричных символа, в EVM-сетях — `0x` и 64, и то и другое в нижнем регистре. **В Solana это первая подпись транзакции: 86–88 знаков base58, у которых регистр несёт смысл и сохраняется ровно таким, как его пишет сеть** — приведение к нижнему регистру уничтожает значение, см. [адреса в Solana](https://stons.io/ru/docs/addresses.md#адреса-в-solana). В TON — транзакция, в которой деньги получил адрес приёма или его жетон-кошелёк, см. [проверить перевод](https://stons.io/ru/docs/addresses.md#проверить-перевод) |
| `log_index` | integer | Номер перевода в транзакции: вместе с `tx_hash` и `network` однозначно определяет платёж. У перевода нативной монеты отрицательный: `-1` — сумма самой транзакции, а в TRON и EVM-сетях `-2`, `-3` и далее — переводы, сделанные контрактами внутри неё. В TON у токена `0`, у GRAM `-1`. Считайте пару `tx_hash` + `log_index` непрозрачным ключом: смысл числа различается от сети к сети, и в Solana перевод токена различается по токен-аккаунту, баланс которого изменился, а не по позиции в списке логов |
| `asset` | string | Актив |
| `network` | string | Сеть перевода, например `tron`. Одинаковый хэш транзакции может быть в двух сетях |
| `amount` | string | Сумма перевода со всеми знаками актива |
| `address` | string | Адрес приёма, целиком, в той же форме, что в `POST /v1/addresses` и в инвойсе: в TON — user-friendly без флага bounce, см. [адреса в TON](https://stons.io/ru/docs/addresses.md#адреса-в-ton) |
| `invoice_id` | string (UUID) или null | Инвойс, который оплачивает перевод, или `null` для пополнения в режиме `per_user` |
| `external_user_id` | string или null | Пользователь адреса `per_user`; у адреса `per_invoice` всегда `null` |
| `block_time` | string | Время блока с переводом, ISO 8601 в UTC |
| `confirmations` | integer | Сколько блоков набрано над блоком перевода: в `deposit.confirmed` — не меньше, чем требует актив; в `deposit.orphaned` — последняя глубина, которую видел сервис |
Полей комиссии у событий `deposit.*` нет: комиссия платежа по инвойсу — в событиях `invoice.*` и в инвойсе, а комиссия пополнения видна в балансе проекта. Те же поля — с текущим `confirmations` и с `kind`, `status`, `block_number` и `credited` — есть в ответе `GET /v1/deposits/{tx_hash}`.
Пример `deposit.confirmed`:
```json
{
"id": "5d2c9e41-7b3a-4c8f-a1e6-9f0b2d4c6e8a",
"type": "deposit.confirmed",
"created_at": "2026-09-11T20:05:41.118Z",
"data": {
"tx_hash": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"log_index": 0,
"asset": "USDT_TRON",
"network": "tron",
"amount": "49.900000",
"address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"invoice_id": "6f1d3c1e-2f7a-4f0e-9a51-0c8b3e2d7a44",
"external_user_id": null,
"block_time": "2026-09-11T20:04:38.000Z",
"confirmations": 20
}
}
```
Поля `data` у событий `payout.*`:
| Поле | Описание |
| --- | --- |
| `payout_id` | Идентификатор выплаты |
| `reference` | Идентификатор выплаты на стороне мерчанта |
| `status` | `confirmed` или `failed` |
| `asset` | Актив |
| `amount` | Сумма выплаты со всеми знаками актива |
| `fees` | Комиссия за выплату: `network.payer`, `network.percent`, `network.fixed`, `network.amount`, `network.charged` — как в [выплате](https://stons.io/ru/docs/payouts.md#комиссия-за-выплату). У `payout.confirmed` `network.charged` равен `network.amount`, у `payout.failed` — нулю |
| `to_address` | Адрес получателя, целиком |
| `tx_hash` | Хэш транзакции. У `payout.failed` — последней транзакции выплаты, средств она не перевела; `null`, если сервер подписи ничего не подписал |
| `reason` | Только у `payout.failed`: что именно произошло, текстом на английском для человека. Разбирать его программно не нужно |
Пример `payout.confirmed`:
```json
{
"id": "9c4b1a2e-7d3f-4a5b-8c6d-0e1f2a3b4c5d",
"type": "payout.confirmed",
"created_at": "2026-09-12T12:15:03.402Z",
"data": {
"payout_id": "4c2f1a7e-93d5-4c0b-8a2e-5f6b7c8d9e01",
"reference": "wd_501",
"status": "confirmed",
"asset": "USDT_TRON",
"amount": "100.500000",
"fees": { "network": { "payer": "merchant", "percent": "1.00", "fixed": "1.700000", "amount": "2.705000", "charged": "2.705000" } },
"to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"tx_hash": "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0"
}
}
```
Из суммы выплаты ничего не вычитается: получатель получает ровно `amount`. Комиссия за выплату — процент и фиксированная сумма — списывается с баланса проекта в той же операции, что подтверждение, поэтому `payout.confirmed` уже несёт её в `fees.network.charged`.
## Подпись
Каждый вебхук содержит заголовки:
```text
Content-Type: application/json
User-Agent: STONS webhooks
X-Timestamp: <время отправки этой попытки, секунды Unix>
X-Signature: sha256=
```
Подпись — это HMAC-SHA256 по строке `.<сырое тело запроса>` с секретом вебхуков проекта в качестве ключа. Ключ — строка секрета ровно в том виде, в каком её показал кабинет: из base64url её не декодируйте. Подписывается именно сырое тело: байты в том виде, в каком они пришли, до разбора JSON. Разобранный и заново сериализованный JSON может отличаться порядком ключей и пробелами, и подпись не сойдётся.
Каждая попытка подписывается заново: у повторной доставки те же тело и `id`, но свои `X-Timestamp` и `X-Signature`.
Получатель обязан:
- отклонять вебхук, если `X-Timestamp` отличается от текущего времени больше чем на 5 минут: так перехваченный вебхук нельзя отправить повторно позже;
- сравнивать подписи за постоянное время, а не обычным сравнением строк.
Проверка на Node.js:
```js
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
// rawBody — Buffer с сырым телом запроса, до JSON.parse.
export function verifyWebhook({ rawBody, signatureHeader, timestampHeader, secret, nowMs = Date.now() }) {
if (typeof timestampHeader !== "string" || !/^\d{1,12}$/.test(timestampHeader)) return false;
if (Math.abs(nowMs / 1000 - Number(timestampHeader)) > TOLERANCE_SECONDS) return false;
const match = /^sha256=([0-9a-f]{64})$/.exec(signatureHeader ?? "");
if (match === null) return false;
const expected = createHmac("sha256", secret).update(`${timestampHeader}.`).update(rawBody).digest();
return timingSafeEqual(Buffer.from(match[1], "hex"), expected);
}
```
Проверка на Python:
```python
import hashlib
import hmac
import re
import time
TOLERANCE_SECONDS = 300
# raw_body — bytes с сырым телом запроса, до json.loads.
def verify_webhook(raw_body, signature_header, timestamp_header, secret, now=None):
if timestamp_header is None or re.fullmatch(r"[0-9]{1,12}", timestamp_header) is None:
return False
current = time.time() if now is None else now
if abs(current - int(timestamp_header)) > TOLERANCE_SECONDS:
return False
match = re.fullmatch(r"sha256=([0-9a-f]{64})", signature_header or "")
if match is None:
return False
signed = timestamp_header.encode("ascii") + b"." + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, match.group(1))
```
Пример для проверки своей реализации. С секретом `example-webhook-secret`, `X-Timestamp: 1789300000` и телом `{"id":"0b7e6f2a-8c1d-4e5f-9a3b-2c4d6e8f0a1b","type":"invoice.confirmed"}` без перевода строки в конце заголовок — `X-Signature: sha256=de0f803a28ef65326019f2500bd933b61882216dedb6e22ea2a03c32c9b272c0`. Этот пример проверяет тест сервиса.
## Доставка
- Вебхук считается доставленным, если URL мерчанта ответил любым кодом `2xx` за 10 секунд. Тело ответа не читается.
- Иначе отправка повторяется через 10 секунд, 1 минуту, 5 минут, 30 минут, 2 часа и 6 часов после предыдущей попытки: всего 7 попыток, последняя — не раньше чем через 8 часов 36 минут после первой. Попытка неудачна при ответе `3xx`, `4xx` или `5xx`, при сетевой ошибке и когда ответа нет 10 секунд. После последней неудачи вебхук помечается недоставленным, и оператор получает сигнал.
- Доставка «хотя бы один раз»: одно и то же событие может прийти несколько раз. Обработчик должен быть идемпотентным — повторная обработка события не должна повторно выдавать товар.
- Событие записывается в той же транзакции базы, что и изменение, о котором оно сообщает. Поэтому событие не теряется при сбое между изменением и отправкой. События отправляются параллельно, и порядок доставки не гарантирован: `invoice.confirmed` может прийти раньше `invoice.seen`. Ориентируйтесь на состояние из API.
- Каждая попытка уходит на адрес вебхуков, действующий в момент попытки: после смены адреса владельцем ожидающие повторы уйдут на новый.
- Проекту без адреса вебхуков — например, заведённому в кабинете для счетов, выставляемых вручную, — события не записываются вовсе. Инвойсы при этом оплачиваются и зачисляются как обычно. События, случившиеся до того, как адрес задан, после его появления не досылаются: состояние берите из API. Заданный адрес можно заменить, но не удалить.
Отвечайте `2xx` сразу после проверки подписи, а долгую обработку выполняйте асинхронно: иначе таймаут в 10 секунд приведёт к лишним повторам.
Ответ `3xx` успехом не считается, и сервис по нему не переходит: подписанное тело не должно уходить на другой адрес. Меняете URL — поменяйте его в настройках проекта в кабинете.
Каждая попытка доставки записывается: код ответа, длительность и ошибка. Владелец проекта видит события счёта на его странице в кабинете. Доставку со статусом «Не доставлен» можно отправить ещё раз кнопкой «Отправить снова»: это ещё одна попытка с тем же телом и `id`, и если она неудачна, доставка снова помечается недоставленной. Доставленное событие повторно не отправляется. Недоставленное событие может отправить заново и оператор.
---
# Баланс
`GET /v1/balance` отдаёт деньги проекта: баланс каждого актива, в котором у него что-то есть, и те же деньги в долларах США. Читайте его для своей панели, перед запросом вывода и для сверки своего учёта с сервисом.
У проекта, которому оператор включил прямые сборы, этот баланс остаётся нулевым: деньги каждого платежа пересылаются на собственный кошелёк проекта, а не хранятся, — см. [куда уходят деньги](https://stons.io/ru/docs/invoices.md#куда-уходят-деньги). Ноль здесь значит, что деньги уже пришли, а не что проекту не платили.
## Запрос
У запроса нет параметров и тела.
В ответе есть `ETag`, поэтому продукт, который держит баланс на экране, платит за один `304`, пока ничего не менялось, — см. [условные чтения](https://stons.io/ru/docs/authentication.md#условные-чтения).
## Ответ
`200` с `balances` — по строке на актив — и `total`: те же строки, сложенные в долларах. Актива, в котором у проекта нет денег, в ответе нет: проект, которому ещё не платили, получает пустой массив и нулевые итоги.
| Поле | Тип | Описание |
| --- | --- | --- |
| `balances[].asset` | string | Идентификатор актива, например `USDT_TRON`, — см. [активы](https://stons.io/ru/docs/invoices.md#активы) |
| `balances[].network` | string | Идентификатор сети актива, например `tron` |
| `balances[].balance` | string | Баланс в единицах актива, со всеми его знаками после точки. Со знаком минус, если выплаты увели его ниже нуля |
| `balances[].unconfirmed` | string | Платежи, увиденные в сети и ещё не подтверждённые, в единицах актива. В `balance` **не входят** |
| `balances[].balance_usd` | string или null | `balance` по курсу, в долларах и центах; `null`, если у сервиса нет свежего курса монеты |
| `balances[].unconfirmed_usd` | string или null | `unconfirmed` по тому же курсу; `null` при том же условии |
| `total.balance_usd` | string | Балансы активов, у которых есть свежий курс, сложенные |
| `total.unconfirmed_usd` | string | Неподтверждённые платежи этих активов, сложенные |
| `total.unpriced_assets` | array of string | Активы, в которых деньги есть, а свежего курса нет: они есть в `balances` и не входят в суммы |
| `total.rates_observed_at` | string или null | Когда принят самый старый курс из сумм; `null`, если в суммах нет ни одного курса |
Строки идут от большей долларовой стоимости к меньшей, активы без курса — последними.
### Откуда берутся числа
- **`balance` — это баланс по реестру**, сумма журнала проекта, а не хранимое число, которое что-то могло забыть обновить. Платёж добавляет к нему, когда у перевода достаточно подтверждений; выплата и вывод — забирают. Исправление делается сторнирующей проводкой, поэтому баланс всегда равен истории, которая его дала.
- **`unconfirmed` стоит рядом с балансом, а не внутри.** Перевод, у которого мало подтверждений, может исчезнуть при реорганизации цепи, то есть это ещё не деньги. Он войдёт в `balance`, когда подтвердится, — см. [статусы](https://stons.io/ru/docs/invoices.md#статусы).
- **В суммах все знаки после точки их актива**, как и везде в этом API: считайте их в строке, а не вшивайте число 6 в свой код, — см. [суммы](https://stons.io/ru/docs/invoices.md#суммы).
- **Доллары — справочные.** Это суммы по последнему принятому сервисом курсу, центы округлены к нулю, поэтому итог может отличаться от суммы округлённых строк на цент на строку. Актив, у монеты которого нет свежего курса, никогда не считается по устаревшему: вместо этого он назван в `unpriced_assets`.
## Ошибки
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 401 | `unauthorized` | Ключ API отсутствует или неверен |
| 403 | `merchant_disabled` | Мерчант отключён |
| 429 | `rate_limited` | Превышен лимит запросов |
| 500 | `internal_error` | Необработанная ошибка сервиса |
Мерчант, не прошедший проверку на ревью, свой баланс читать может.
## Пример
```sh
curl -s https://api.stons.io/v1/balance -H "X-Api-Key: $API_KEY"
```
`200`, проект, которому зачислено 100 USDT в TRON, и ещё 5 USDT набирают подтверждения:
```json
{
"balances": [
{
"asset": "USDT_TRON",
"network": "tron",
"balance": "100.000000",
"unconfirmed": "5.000000",
"balance_usd": "99.96",
"unconfirmed_usd": "4.99"
}
],
"total": {
"balance_usd": "99.96",
"unconfirmed_usd": "4.99",
"unpriced_assets": [],
"rates_observed_at": "2026-10-04T17:31:05.531Z"
}
}
```
---
# Выплаты
Выплата — перевод средств мерчанта на адрес, который заранее внесён в его вайтлист. Приём ошибается в пользу сервиса, вывод — в чужую, поэтому контроль на выводе строже: адрес только из вайтлиста и только после задержки, три лимита и порог автоподтверждения, а выплату, которая перешла любой из них, подтверждает человек.
## Откуда платятся выплаты
Только с отдельного горячего кошелька. Не с адресов приёма и не из холодного хранилища. Баланс горячего кошелька ограничен сверху и пополняется вручную из холодного хранилища: это потолок потерь, если сервер подписи будет скомпрометирован.
## Создать выплату
`POST /v1/payouts`
### Запрос
| Поле | Тип | Обязательно | Описание |
| --- | --- | --- | --- |
| `reference` | string | да | Идентификатор выплаты на стороне мерчанта, уникальный в пределах мерчанта: 1–200 видимых ASCII-символов без пробелов. Ключ идемпотентности |
| `asset` | string | да | Актив, например `USDT_TRON`. Сервис платит не во всех активах, в которых принимает: см. столбец «Выплаты» в [активах](https://stons.io/ru/docs/invoices.md#активы) |
| `to_address` | string | да | Адрес получателя целиком, 20–100 букв и цифр. Должен быть в вайтлисте мерчанта по этому активу |
| `amount` | string | да | Сумма в единицах актива: `"100.5"`, до 100 символов. Не больше знаков после запятой, чем у актива, без знака, экспоненты и пробелов. Ноль не принимается |
Других полей тело не принимает: неизвестное поле получает `400 validation_failed`.
### Ответ
`202` — выплата создана, `200` — выплата с этим `reference` уже была создана раньше. Тело в обоих случаях одинаковое:
| Поле | Тип | Описание |
| --- | --- | --- |
| `payout_id` | string (UUID) | Идентификатор выплаты |
| `reference` | string | Идентификатор выплаты на стороне мерчанта |
| `status` | string | Статус, см. [ниже](#статусы) |
| `asset` | string | Актив |
| `network` | string | Сеть актива, например `tron` |
| `network_name` | string | Название сети для людей из реестра сетей сервиса, например `TRON` |
| `to_address` | string | Адрес получателя целиком |
| `amount` | string | Сумма со всеми знаками актива: `"100.500000"`. Её получает получатель целиком |
| `fees` | object | Комиссия за выплату: `fees.network.payer`, `fees.network.percent`, `fees.network.fixed`, `fees.network.amount`, `fees.network.charged` — см. [комиссию за выплату](#комиссия-за-выплату) |
| `review_reasons` | array of string | Какие правила выплата перешла и потому ждёт человека — значения в [лимитах](#лимиты). Пусто у выплаты, прошедшей по правилам |
| `tx_hash` | string или null | Хэш транзакции, когда она отправлена |
| `explorer_url` | string или null | Страница транзакции в обозревателе сети; `null`, пока транзакция не отправлена или если у сети нет ссылки на обозреватель |
| `created_at` | string | Время создания, ISO 8601 в UTC |
| `updated_at` | string | Время последнего изменения, ISO 8601 в UTC |
Созданная выплата получает статус `queued`, если прошла все правила, и `pending_review`, если нет. Второй случай — не ошибка: выплата принята и ждёт решения человека, а `review_reasons` говорит, чего именно.
## Получить выплату
`GET /v1/payouts/{id}`
Отвечает тем же телом. Выплаты других мерчантов для вызывающего не существуют: ответ `404 payout_not_found`. Идентификатор, который не является UUID, получает `400 validation_failed`.
## Статусы
```text
pending_review → approved → queued → signing → broadcast → confirmed
↓ ↓
rejected failed
```
| Статус | Смысл |
| --- | --- |
| `pending_review` | Выплата перешла лимит или порог и ждёт подтверждения человека |
| `approved` | Человек подтвердил выплату. Ставится только ручным подтверждением: выплата в пределах правил через этот статус не проходит |
| `queued` | Выплата принята к исполнению и ждёт подписи |
| `signing` | Сервер подписи взял выплату в работу |
| `broadcast` | Транзакция отправлена в сеть и ждёт подтверждений |
| `confirmed` | Транзакция подтверждена сетью. Окончательный статус |
| `rejected` | Человек отклонил выплату. Окончательный статус: повтор требует нового `reference` |
| `failed` | Выплата не исполнена, и сервис больше не пытается её отправить. Окончательный статус: автоматических повторов нет |
Выплата становится `failed` в трёх случаях:
- её транзакция попала в блок неуспешной;
- сервер подписи отказался её подписывать — например, адрес не прошёл его независимую проверку по вайтлисту;
- сервер подписи исчерпал попытки, и ни одна подписанная им транзакция этой выплаты уже не может попасть в блок.
Выплата в `failed` **не повторяется автоматически**: повторная отправка без уверенности, что прежняя транзакция не попала в сеть, означала бы двойную выплату. Новая попытка — новая выплата с новым `reference`.
Если узел сети ответил на отправку ошибкой или не ответил вовсе, выплата в `failed` не переходит: деньги могли уйти. Она возвращается в `queued` с тем же `tx_hash`. Сервер подписи ждёт, пока найдёт транзакцию в цепи или убедится, что её срок истёк и в блок она не попала. Только после этого выплата становится `broadcast`, `failed` или получает новую транзакцию.
Переход в `confirmed` и в `failed` сопровождается вебхуком `payout.confirmed` или `payout.failed`, см. [события](https://stons.io/ru/docs/webhooks.md#события). Событие пишется в той же транзакции базы, что и смена статуса, и приходит одно на выплату, какой бы сервис её ни перевёл. Сеть сервис оплачивает с горячего кошелька, и из суммы выплаты ничего не вычитается: получатель получает ровно `amount` (ADR-0023). С баланса проекта при подтверждении списывается [комиссия за выплату](#комиссия-за-выплату).
## Комиссия за выплату
За каждую подтверждённую выплату STONS списывает с баланса проекта комиссию — процент от суммы выплаты и фиксированную сумму, известные заранее и свои у каждого актива, как принято на рынке. Сколько на самом деле сожгла сеть, на комиссию не влияет: разницу несёт платформа. Поля комиссии по-прежнему лежат в `fees.network`: это имя осталось с тех пор, когда она была фиксированной комиссией сети ([журнал изменений](https://stons.io/ru/docs/changelog.md#2026-09-13-комиссия-сети)).
- **Комиссия = фиксированная сумма + ⌊`amount` × процент⌋.** Процент округляется вниз до последнего знака актива, в пользу проекта.
- **Комиссия записывается в выплату при её создании** — `fees.network.amount`, а рядом процент и фиксированная сумма, из которых она сложена. Смена тарифа не меняет уже созданные выплаты.
- **Списывается при подтверждении** выплаты, в той же операции, что сама выплата. До подтверждения `fees.network.charged` равен нулю.
- **Выплата в `failed` и отклонённая выплата** комиссию не несут: комиссия списывается только при подтверждении.
- **Платит всегда проект.** Получатель получает ровно `amount`: вариант «комиссию платит получатель» не поддерживается.
- **Процент и сумму назначает оператор**. Пока он их не задал, комиссия равна нулю.
- **Баланс при создании выплаты не проверяется**, как и раньше: выплату ограничивают вайтлист и лимиты. Комиссия, как и сама выплата, списывается при подтверждении и может увести баланс проекта ниже нуля. Лимиты сравнивают только `amount`: комиссия получателю не уходит.
| Поле | Описание |
| --- | --- |
| `fees.network.payer` | Всегда `merchant` |
| `fees.network.percent` | Процент, записанный в выплату при создании, с двумя знаками: `"1.00"` |
| `fees.network.fixed` | Фиксированная сумма, записанная в выплату при создании, со всеми знаками актива |
| `fees.network.amount` | Вся комиссия, записанная в выплату при создании: фиксированная сумма плюс процент от `amount`, со всеми знаками актива |
| `fees.network.charged` | Сколько комиссии списано с баланса проекта за эту выплату, без самой суммы выплаты: `fees.network.amount` после подтверждения, иначе ноль |
Пример: выплата 100.5 USDT при комиссии 1% и 1.70 USDT. Комиссия — 1.70 + 1.005 = 2.705 USDT. Получатель получает 100.5 USDT, баланс проекта после подтверждения уменьшается на 103.205 USDT.
## Куда можно платить
Только на адреса из вайтлиста мерчанта. Вайтлист ведёт оператор STONS: ни API, ни кабинет добавить в него адрес не могут.
- Адрес, добавленный в вайтлист, можно использовать только после задержки: `WHITELIST_COOLDOWN_HOURS`, по умолчанию 24 часа. Момент «использовать с» считается при добавлении и хранится в строке, поэтому смена настройки не ускоряет уже добавленные адреса. До этого момента выплата отклоняется кодом `address_not_whitelisted`, и сообщение называет момент, с которого адрес станет годен.
- **Добавление подписывается операторским ключом офлайн.** Подпись покрывает мерчанта, актив, адрес целиком и момент добавления, поэтому её нельзя переставить на другой адрес или другого мерчанта. Строку без верной подписи не примет ни команда, ни сервер подписи.
- Добавление адреса, у которого первые 4 и последние 4 символа совпадают с адресом, уже внесённым в вайтлист мерчанта, блокируется до явного подтверждения с показом обоих адресов целиком. Так отсекается подмена адреса похожим (address poisoning).
- Адрес для выплаты никогда не берётся из истории транзакций.
- Адрес сверяется с вайтлистом дважды: при создании выплаты в API и ещё раз, независимо, сервером подписи перед подписью. Выплата хранит ссылку на запись вайтлиста, которая разрешила её при создании, и сервер подписи проверяет именно эту запись: адрес в ней совпадает с адресом выплаты целиком, запись не отозвана, задержка прошла, а подпись оператора под ней верна.
- Отзыв адреса подписи не требует: он только убирает разрешение. Отозванный адрес можно добавить заново, и задержка начнётся сначала.
## Лимиты
Действуют четыре значения. Все они заданы в одной учётной единице — в долларах, — потому что суточный лимит сервиса складывает выплаты по всем активам, а складывать минимальные единицы активов с разными `decimals` нельзя. Отсюда правило: сервис платит только в активах, привязанных к доллару.
| Переменная | Что ограничивает |
| --- | --- |
| `PAYOUT_MAX_AMOUNT` | Одну выплату |
| `PAYOUT_AUTO_APPROVE_LIMIT` | Порог, ниже которого подтверждение человека не нужно |
| `PAYOUT_DAILY_LIMIT_MERCHANT` | Сумму выплат одного мерчанта за сутки |
| `PAYOUT_DAILY_LIMIT_TOTAL` | Сумму выплат всего сервиса за сутки |
Значения задаёт оператор, API их не возвращает. Суточные лимиты считаются по скользящему окну в 24 часа, а не по календарным суткам: полночь ничего не обнуляет. В сумму входят выплаты, деньги по которым ещё могут уйти или уже ушли, включая ожидающие подтверждения; отклонённые и неудавшиеся не входят. Лимит ограничивает сумму вместе с создаваемой выплатой, то есть общий поток наружу, а не каждую выплату по отдельности.
Переход лимита **не отклоняет** выплату — он переводит решение человеку. В `review_reasons` появляется одно или несколько значений:
| Значение | Когда |
| --- | --- |
| `above_transaction_limit` | Сумма больше `PAYOUT_MAX_AMOUNT` |
| `above_auto_approve_limit` | Сумма больше `PAYOUT_AUTO_APPROVE_LIMIT` |
| `above_merchant_daily_limit` | Вместе с этой выплатой мерчант превысит свой суточный лимит |
| `above_service_daily_limit` | Вместе с этой выплатой сервис превысит суточный лимит целиком |
## Ручное подтверждение
Подтверждает и отклоняет выплату человек, командой на сервере: в административном API и в админке маршрутов для выплат пока нет. Коротко:
1. Оператор смотрит выплаты в `pending_review`: `payout:list`. Вывод показывает адрес целиком, сумму в минимальных единицах актива и действующие лимиты.
2. Он сверяет адрес получателя символ за символом с тем, что ожидает мерчант, и проверяет причину ожидания.
3. `payout:approve --actor <кто> --id <выплата>` переводит выплату в `approved`. `payout:reject --actor <кто> --id <выплата> --note <почему>` — в `rejected`, окончательно.
**Разделение полномочий.** Тот, кто добавил адрес в вайтлист, не может подтвердить выплату на этот адрес: команда откажет и назовёт, кто добавлял. Одна учётная запись не должна уметь и выбрать адрес, и отпустить на него деньги. Отклонение такого ограничения не имеет: остановить выплату может кто угодно.
Каждое подтверждение, отклонение и добавление в вайтлист попадает в неизменяемый журнал аудита вместе с адресом целиком.
## Идемпотентность
Выплата создаётся с идентификатором мерчанта `reference`, уникальным в пределах мерчанта. Повторный запрос с тем же `reference`, активом, адресом и суммой не создаёт вторую выплату и не возвращает ошибку: ответом будет уже созданная выплата с кодом `200`. Поэтому запрос можно безопасно повторять после таймаута, и повтор работает даже когда выплаты остановлены. Одновременные запросы с одним `reference` тоже создают одну выплату.
Сумма сравнивается по значению: `"100.5"` и `"100.500000"` — одна и та же сумма. Адрес сравнивается точно, посимвольно.
Прежде чем искать уже созданную выплату, сервис проверяет повтор так же, как первый запрос: форму полей, платит ли сервис в этом активе (`422 asset_not_payable`) и формат суммы (`400 validation_failed`). Повтор, который не прошёл эти проверки, получает ошибку, а не выплату. Остальные проверки — выключатель выплат актива и вайтлист — идут после поиска и повтору уже созданной выплаты не мешают.
Запрос с тем же `reference`, но другой суммой, активом или адресом отклоняется кодом `409 reference_conflict`: это другая выплата, а не повтор.
## Остановка выплат
Остановка действует на выплаты одного актива или на все сразу. Выплаты актива останавливает сверка, если реестр по этому активу разошёлся с сетью или его проводки не сходятся. Все выплаты останавливает предохранитель, если за короткое время не удалось слишком много выплат или их объём резко превысил обычный. Вручную оператор останавливает выплаты аварийной командой — по одному активу или все. Пока остановка действует, новые выплаты в затронутых активах не принимаются — код `503 payouts_halted`, — а созданные раньше не подписываются и ждут её снятия; повтор с тем же `reference` после снятия остановки создаст выплату. Причина остановки наружу не раскрывается: она описывает внутренний инцидент. Снять остановку можно только вручную, с записью в журнал аудита — см. остановку выплат.
## Ошибки
| HTTP | `error.code` | Когда |
| --- | --- | --- |
| 400 | `validation_failed` | Поле отсутствует, лишнее или неверного формата; у суммы слишком много знаков после запятой; сумма не больше нуля; идентификатор выплаты в `GET /v1/payouts/{id}` не является UUID |
| 401 | `unauthorized` | Нет ключа API или он неверный |
| 403 | `merchant_disabled` | Мерчант отключён |
| 404 | `payout_not_found` | Выплаты нет или она принадлежит другому мерчанту |
| 409 | `reference_conflict` | Выплата с этим `reference` уже есть и описывает другую выплату |
| 422 | `asset_not_payable` | В этом активе сервис не делает выплат |
| 422 | `asset_payouts_disabled` | Оператор временно выключил выплаты в этом активе. Повтор с существующим `reference` возвращает выплату |
| 422 | `address_not_whitelisted` | Адреса нет в вайтлисте по этому активу или его задержка не прошла |
| 429 | `rate_limited` | Превышен лимит запросов |
| 503 | `payouts_halted` | Выплаты остановлены |
## Пример
```sh
curl -s -X POST https://api.stons.io/v1/payouts \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"reference": "wd_501", "asset": "USDT_TRON", "to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH", "amount": "100.5"}'
```
```json
{
"payout_id": "4c2f1a7e-93d5-4c0b-8a2e-5f6b7c8d9e01",
"reference": "wd_501",
"status": "queued",
"asset": "USDT_TRON",
"network": "tron",
"network_name": "TRON",
"to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"amount": "100.500000",
"fees": { "network": { "payer": "merchant", "percent": "1.00", "fixed": "1.700000", "amount": "2.705000", "charged": "0.000000" } },
"review_reasons": [],
"tx_hash": null,
"explorer_url": null,
"created_at": "2026-09-12T12:00:00.000Z",
"updated_at": "2026-09-12T12:00:00.000Z"
}
```
Выплата, которая ждёт человека, отвечает тем же кодом `202`:
```json
{
"payout_id": "7e1b2c3d-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
"reference": "wd_502",
"status": "pending_review",
"asset": "USDT_TRON",
"network": "tron",
"network_name": "TRON",
"to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"amount": "900.000000",
"fees": { "network": { "payer": "merchant", "percent": "1.00", "fixed": "1.700000", "amount": "10.700000", "charged": "0.000000" } },
"review_reasons": ["above_auto_approve_limit"],
"tx_hash": null,
"explorer_url": null,
"created_at": "2026-09-12T12:05:00.000Z",
"updated_at": "2026-09-12T12:05:00.000Z"
}
```
---
# Коды ошибок
Этот документ перечисляет все `error.code`, которые возвращают публичные маршруты `/v1` и `/checkout/v1`, с HTTP-статусом, который использует сервис, и тем, что делать клиенту. Прочитайте его, когда пишете обработку ошибок интеграции.
## Тело ошибки
Любой ответ API с ошибкой имеет одно и то же тело:
```json
{ "error": { "code": "route_not_found", "message": "Route not found" } }
```
| Поле | Тип | Смысл |
| --- | --- | --- |
| `error.code` | string | Машиночитаемый код из [перечня](#перечень). Сервис не переименовывает и не удаляет коды; новые коды появляются в [журнале изменений](https://stons.io/ru/docs/changelog.md) |
| `error.message` | string | Пояснение для человека на английском. Текст может меняться: не разбирайте его в коде |
| `error.details` | array | Есть, только если запрос не прошёл проверку по схеме: по элементу на каждую проблему, с `path` — где проблема (`body.amount`, `params.id` или `body` для лишнего поля) — и `message`. У `validation_failed` из более поздней проверки, например числа знаков суммы, `details` нет; её `message` начинается с имени поля |
Каждый ответ, включая ошибки, содержит заголовок `X-Request-Id`. Сообщайте его значение оператору, когда сообщаете о проблеме: по нему находится запрос в логах. Если клиент сам передал `X-Request-Id` из 1–128 латинских букв, цифр и символов `.`, `_`, `:`, `-`, сервис использует его, иначе генерирует UUID.
Описание OpenAPI, `GET /v1/openapi.json`, объявляет один список кодов для всех маршрутов сервиса, включая внутренние. Публичный маршрут возвращает только коды из перечня ниже.
Два вида ответов такого тела не имеют:
- HTTPS-прокси отвечает на путь вне `/v1/` кодом `404` с пустым телом;
- во время остановки API отвечает `503` с телом в другом формате.
Ответ, тело которого не в формате выше, обрабатывайте по HTTP-статусу.
## Как обрабатывать ошибки
- **`400`, `413`, `415`** — запрос неверный. Исправьте его: тот же запрос получит ту же ошибку.
- **`401`** — проверьте ключ и имя заголовка.
- **`403`** — мерчант отключён или не одобрен. Запросы будут отклоняться, пока это не изменит владелец или оператор.
- **`404`** — проверьте путь и идентификатор.
- **`409`** — запрос конфликтует с существующим объектом: ключ идемпотентности (`order_id`, `reference`) использован повторно с другими данными, или выбор токена на странице оплаты уже сделан либо уже невозможен. Не повторяйте тот же запрос.
- **`422`** — запрос корректен, но его отклоняет бизнес-правило; код называет правило. Тот же запрос получит ту же ошибку, пока не изменится настройка, стоящая за правилом.
- **`429`** — повторите через число секунд из заголовка `retry-after`.
- **`500`, `503`** — проблема сервиса. Повторите позже с растущей задержкой и тем же ключом идемпотентности: `order_id` для инвойса, `reference` для выплаты. Повтор не создаёт второй объект.
- **Кода нет в перечне** — обработайте ответ по классу HTTP-статуса: `4xx` — ошибка в запросе, `5xx` — проблема сервиса.
## Перечень
| HTTP | `error.code` | Маршруты | Когда | Что делать клиенту |
| --- | --- | --- | --- | --- |
| 400 | `validation_failed` | все | Тело, путь или параметры не проходят проверку: поле отсутствует, лишнее, `null` или неверного формата. Или не проходит более поздняя проверка поля: формат или число знаков суммы, точность цены, `external_user_id` в режиме `per_user` | Исправить поле, названное в `details` или в начале `message` |
| 400, 413, 415 | `bad_request` | маршруты с телом запроса | Тело — не JSON или пустое при `Content-Type: application/json` (400), больше 16 КиБ (413) или с неподдерживаемым `Content-Type` (415) | Отправлять JSON-объект с `Content-Type: application/json` |
| 401 | `unauthorized` | `/v1` | Нет заголовка `X-Api-Key`, либо ключ неверного формата, неизвестен, отозван или его секрет не совпадает | Проверить ключ. Причина намеренно не уточняется — см. [аутентификацию](https://stons.io/ru/docs/authentication.md) |
| 403 | `merchant_disabled` | `/v1` | Мерчант отключён — оператором или владельцем в кабинете | Включить проект в кабинете. Если проект выключил оператор, включить его может только оператор |
| 403 | `merchant_not_approved` | `POST /v1/invoices`, `POST /v1/addresses`, `POST /checkout/v1/invoices/{id}/select` | Мерчант ещё не одобрен после проверки или отклонён | Дождаться проверки. Причину отклонения владелец видит в кабинете и может исправить заявку и отправить её снова. Повтор с `order_id` существующего инвойса возвращает этот инвойс |
| 404 | `route_not_found` | любой путь под `/v1` или `/checkout/v1` | Нет маршрута с таким методом и путём | Проверить метод и путь |
| 404 | `invoice_not_found` | `GET /v1/invoices/{id}`, `GET /checkout/v1/invoices/{id}`, `POST /checkout/v1/invoices/{id}/select` | Инвойса нет или он принадлежит другому мерчанту. На маршрутах `/checkout/v1` ещё: мерчант отключён | Проверить идентификатор |
| 404 | `payout_not_found` | `GET /v1/payouts/{id}` | Выплаты нет или она принадлежит другому мерчанту | Проверить идентификатор |
| 404 | `deposit_not_found` | `GET /v1/deposits/{tx_hash}` | В транзакции нет перевода на адрес мерчанта: сервис её не знает, она шла только другим мерчантам или её блок ещё не просканирован | Проверить хэш. Для только что отправленной транзакции повторить позже — см. [проверку перевода](https://stons.io/ru/docs/addresses.md#проверить-перевод) |
| 409 | `address_mode_mismatch` | `POST /v1/addresses` | Мерчант работает в режиме `per_invoice` | Брать адрес из инвойса — см. [режимы](https://stons.io/ru/docs/addresses.md#режимы) |
| 409 | `order_id_conflict` | `POST /v1/invoices` | Инвойс с этим `order_id` есть с другим активом, суммой, `currencies`, `external_user_id`, `return_url` или `cancel_url` | Проверить, не используется ли `order_id` повторно для другого заказа, — см. [идемпотентность](https://stons.io/ru/docs/invoices.md#идемпотентность) |
| 409 | `reference_conflict` | `POST /v1/payouts` | Выплата с этим `reference` есть с другим активом, адресом или суммой | Проверить, не используется ли `reference` повторно для другой выплаты |
| 409 | `asset_already_selected` | `POST /checkout/v1/invoices/{id}/select` | У инвойса уже другой актив. Выбор окончательный | Показать покупателю инвойс с его токеном |
| 409 | `invoice_expired` | `POST /checkout/v1/invoices/{id}/select` | Дедлайн инвойса прошёл или инвойс уже не в статусе `created` | Показать покупателю состояние инвойса |
| 422 | `address_mode_disabled` | `POST /v1/addresses`, `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Платформа временно приостановила режим адресов мерчанта | Сообщить покупателю, что оплата сейчас недоступна, и повторить позже. Повтор с `order_id` существующего инвойса возвращает этот инвойс; переводы на уже выданные адреса зачисляются как обычно — см. [режим на паузе](https://stons.io/ru/docs/addresses.md#режим-на-паузе) |
| 422 | `mode_not_allowed` | `POST /v1/addresses`, `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Режим адресов этого проекта ему не разрешён: оператор разрешает режим по проектам (ADR-0066) | Обратиться к оператору. Переводы на уже выданные адреса зачисляются как обычно |
| 422 | `asset_not_supported` | `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Актив неизвестен или не принимается к оплате | Использовать актив из [таблицы активов](https://stons.io/ru/docs/invoices.md#активы), который оплачивает инвойсы |
| 422 | `asset_not_payable` | `POST /v1/payouts` | В этом активе сервис не делает выплат: он неизвестен, неактивен или только для приёма | Использовать актив из [таблицы активов](https://stons.io/ru/docs/invoices.md#активы), в котором есть выплаты |
| 422 | `asset_payments_disabled` | `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Оператор временно выключил приём в активе | Предложить другой актив или повторить позже. Повтор с `order_id` существующего инвойса возвращает этот инвойс |
| 422 | `asset_disabled_by_merchant` | `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Приём в активе выключен в настройках проекта | Предложить другой актив или включить токен в разделе проекта «Токены» в кабинете. Повтор с `order_id` существующего инвойса возвращает этот инвойс |
| 422 | `asset_payouts_disabled` | `POST /v1/payouts` | Оператор временно выключил выплаты в активе | Повторить позже с тем же `reference`. Выплаты, созданные раньше, исполняются как обычно |
| 422 | `amount_below_minimum` | `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Сумма или цена меньше минимального депозита актива | Увеличить сумму или выбрать другой актив |
| 422 | `asset_required` | `POST /v1/invoices` | Нет `asset`, а в установке нет страницы оплаты, где покупатель выбрал бы токен | Передать `asset` |
| 422 | `no_payable_assets` | `POST /v1/invoices` | Нет ни `asset`, ни `currencies`, а мерчант сейчас не принимает ни одного актива, у которого есть курс | Включить токен в настройках проекта или передать `asset` |
| 422 | `asset_not_offered` | `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Актив нельзя предложить для цены в долларах США: у сервиса нет для него курса; на маршруте выбора ещё: его нет в `currencies` инвойса | Выбрать другой актив |
| 422 | `redirect_url_not_allowed` | `POST /v1/invoices` | `return_url` или `cancel_url` не на сайте мерчанта | Использовать адрес на сайте проекта или на сайте адреса проекта того же вида — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата) |
| 422 | `address_not_whitelisted` | `POST /v1/payouts` | Адреса нет в вайтлисте мерчанта по этому активу, или задержка перед его первым использованием ещё не прошла. Сообщение называет, какой из двух случаев | См. [куда можно платить](https://stons.io/ru/docs/payouts.md#куда-можно-платить). Ответ не зависит от суммы |
| 429 | `rate_limited` | `/v1`, `/checkout/v1` | Превышен лимит запросов | Повторить через число секунд из заголовка `retry-after` — см. [лимиты запросов](https://stons.io/ru/docs/authentication.md#лимиты-запросов) |
| 500 | `internal_error` | все | Необработанная ошибка сервиса. Подробности пишутся в лог сервиса, а не в ответ | Повторить позже с тем же ключом идемпотентности. При повторении сообщить `X-Request-Id` |
| 503 | `service_unavailable` | `POST /v1/addresses`, `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Сервис не может вывести новый адрес в текущей конфигурации; сообщение называет причину | Повторить позже. Если не проходит, обратиться к оператору |
| 503 | `addresses_exhausted` | `POST /v1/addresses`, `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Закончились индексы деривации адресов: заняты все 2^31. Практически недостижимо | Обратиться к оператору |
| 503 | `network_fee_quote_unavailable` | `POST /v1/invoices` | У проекта осталась прежняя настройка «комиссию сети платит покупатель», поэтому новый инвойс не создаётся. Повтор с `order_id` существующего инвойса возвращает этот инвойс | Владелец сохраняет «Кто платит комиссии» в настройках проекта в кабинете; затем повторить |
| 503 | `rate_unavailable` | `POST /v1/invoices`, `POST /checkout/v1/invoices/{id}/select` | Сейчас нет свежего курса: при создании инвойса без `asset` — ни у одного разрешённого токена, при выборе — у монеты выбранного актива. Ничего не создаётся и не записывается. Повтор с `order_id` существующего инвойса возвращает этот инвойс | Повторить позже с растущей задержкой и тем же `order_id`. На странице оплаты — предложить другой токен или попросить покупателя попробовать позже. Если не проходит, обратиться к оператору |
| 503 | `payouts_halted` | `POST /v1/payouts` | Выплаты остановлены — по этому активу или целиком; сообщение говорит, какой из двух случаев. Причина не раскрывается | Повторить позже с тем же `reference`. Повтор существующего `reference` возвращает эту выплату и во время остановки |
## Примеры
Несуществующий путь:
```sh
curl -s https://api.stons.io/v1/unknown
```
`404`:
```json
{"error":{"code":"route_not_found","message":"Route not found"}}
```
Запрос к `POST /v1/invoices` с `"expires_in": 60` не проходит проверку по схеме, и `details` перечисляет все проблемные поля:
```json
{
"error": {
"code": "validation_failed",
"message": "Request validation failed",
"details": [{ "path": "body.expires_in", "message": "Too small: expected number to be >=300" }]
}
}
```
Запрос с `"asset": "USDT_TRON"` и `"amount": "49.9000001"` проходит проверку по схеме, но не проходит проверку числа знаков, поэтому `details` нет:
```json
{
"error": {
"code": "validation_failed",
"message": "amount: amount has more than 6 digits after the decimal point (USDT_TRON has 6 decimals)"
}
}
```
---
# Чек-лист перед продом
Пройдите его, прежде чем принимать реальные платежи. Каждый пункт закрывает известный способ потерять деньги. Порядок работ по интеграции — в [интеграции под ключ](https://stons.io/ru/docs/ai-integration.md).
## Ключи и доступ
- [ ] Ключ API хранится в менеджере секретов и используется только на сервере. Его нет в браузере, мобильном приложении, репозитории и логах.
- [ ] Секрет вебхуков хранится так же и отдельно от ключа API. Тот же секрет подписывает вебхуки и редиректы со страницы оплаты.
- [ ] Продукт обращается к API только по `https://`: по `http://` ключ ушёл бы открытым текстом ещё до перенаправления — см. [аутентификацию](https://stons.io/ru/docs/authentication.md#сеть).
- [ ] Адрес вебхуков в настройках проекта начинается с `https://`: кабинет принимает и `http://`, но тогда события идут открытым текстом.
## Вебхуки
- [ ] Подпись проверяется по сырому телу запроса, до разбора JSON — см. [подпись](https://stons.io/ru/docs/webhooks.md#подпись).
- [ ] Подписи сравниваются за постоянное время.
- [ ] Вебхуки с `X-Timestamp`, отличающимся от текущего времени больше чем на 5 минут, отклоняются.
- [ ] Решение отдать товар принимается по ответу API — `GET /v1/invoices/{id}`, — а не по телу вебхука.
- [ ] Пополнение `per_user` зачисляется пользователю только после того, как `GET /v1/deposits/{tx_hash}` показал его перевод со `status` = `confirmed` и `credited` = `true`, — см. [проверку перевода](https://stons.io/ru/docs/addresses.md#проверить-перевод).
- [ ] Дубли отбрасываются по `id` события, и повторная доставка того же события не приводит к повторной выдаче товара.
- [ ] Обработчик не зависит от порядка событий: `invoice.confirmed` может прийти раньше `invoice.seen`.
- [ ] Обработчик отвечает `2xx` за 10 секунд, в том числе на типы событий, которые не обрабатывает, а тяжёлую работу выполняет асинхронно — см. [доставку](https://stons.io/ru/docs/webhooks.md#доставка).
## Платежи
- [ ] Товар отдаётся, только когда статус инвойса `confirmed` или `settled`, — не при `seen` и не при первом появлении транзакции, см. [статусы](https://stons.io/ru/docs/invoices.md#статусы).
- [ ] Продукт определил, что делает при `underpaid`, при `paid_late` и при переплате — `confirmed` с `amount_received` больше `amount_expected`.
- [ ] При `partially_paid` товар не отдаётся. Если продукт показывает оплату сам, покупатель видит `amount_remaining` и тот же адрес, см. [частичную оплату](https://stons.io/ru/docs/invoices.md#частичная-оплата).
- [ ] Суммы обрабатываются как строки или целые числа в минимальных единицах, никогда как числа с плавающей точкой.
- [ ] Число знаков после запятой берётся для конкретного актива и не зашивается в код: у USDT в BNB Chain их 18, а не 6. Сумма в ответе API содержит после точки ровно столько цифр, сколько знаков у её актива.
- [ ] Продукт сверяет свою цену с `amount_requested`, а у покупателя просит `amount_expected` — см. [комиссии](https://stons.io/ru/docs/invoices.md#комиссии).
- [ ] Каждый заказ создаёт инвойс со своим `order_id`, а повтор запроса после таймаута использует тот же `order_id` с теми же полями.
## Отображение
- [ ] Адрес показывается пользователю целиком, без сокращений, вместе с сетью и активом — см. [показ адреса](https://stons.io/ru/docs/addresses.md#показ-адреса).
- [ ] Пользователь предупреждён, что перевод в другой сети или другого токена не зачисляется автоматически.
- [ ] Интеграция принимает несколько переводов в одной транзакции и отрицательный `log_index`, отличный от `-1`: платёж нативной монетой EVM-сети, который отправил смарт-контракт, например ETH или BNB со смарт-кошелька, мультиподписи или с вывода биржи, приходит с `-2`, `-3` и далее, — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес).
- [ ] Инвойс, созданный без `asset`, оплачивается только через `checkout_url`: пока покупатель не выбрал токен, адреса у него нет.
## Страница оплаты
- [ ] Если используется страница оплаты, адреса возврата и отмены заданы в настройках проекта: без них страница не вернёт покупателя — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата).
- [ ] Подпись редиректа и окно `ts` в 5 минут проверяются, а товар по редиректу не отдаётся — см. [подпись](https://stons.io/ru/docs/checkout.md#подпись).
- [ ] Переход на адрес отмены со `status=created`, `status=seen` или `status=partially_paid` не отменяет заказ: инвойс ещё можно оплатить.
## Выплаты
- [ ] Адреса для выплат заранее внесены в вайтлист: их вносит оператор, и новый адрес становится доступен только после задержки — по умолчанию через 24 часа.
- [ ] Адрес для выплаты берётся из вайтлиста и никогда — из истории транзакций.
- [ ] Каждая выплата создаётся со своим `reference`, а повтор использует тот же `reference`.
- [ ] `pending_review` обрабатывается как принятая выплата, которая ждёт человека, а не как ошибка, а исход берётся из `GET /v1/payouts/{id}`.
## Проверка
- [ ] Весь сценарий — создание инвойса, оплата, вебхук, проверка статуса через API и выдача товара — пройден на небольшой сумме до приёма реальных платежей.
---
# Журнал изменений API
Все изменения публичного API: новые маршруты, поля, события вебхуков и коды ошибок. Новые записи добавляются сверху, даты указаны по UTC. Изменение, ломающее совместимость, отмечается словом **несовместимо** и объясняет, что должен сделать интегратор.
## 2026-10-14, сумма в стейблкоине не пересчитывается, прежняя сумма засчитывается 10 минут
- **Токен, привязанный к доллару США** (`USDT_*`, `USDC_*`): сумма инвойса с ценой в долларах, зафиксированная при выборе, действует до дедлайна инвойса и после окна котировки больше не пересчитывается, остаток после частичной оплаты — тоже. `quote.expires_at` у такого инвойса равен `expires_at` инвойса, в том числе у инвойса, выбранного раньше. Раньше сумма сдвигалась на центы каждые 30 минут, и покупатель, заплативший ровно показанное, получал `partially_paid`.
- **Остальные токены** пересчитываются после окна, как раньше, но перевод в блоке не позже 10 минут после конца окна прежней котировки оценивается и по её сумме. Если он её покрыл, инвойс становится `confirmed`, а `amount_requested`, `amount_expected` и `quote` (с её `number`) — снова те, что у прежней котировки. См. [окно котировки](https://stons.io/ru/docs/invoices.md#окно-котировки).
- Поля и коды ошибок не менялись.
## 2026-10-14, остаток меньше минимального депозита оплачивает инвойс
- Перевод меньше минимального депозита актива больше не теряется, если это остаток инвойса `per_invoice`: когда по инвойсу уже пришёл платёж, а `amount_expected` ещё не набран, перевод в активе инвойса на его адрес — платёж (`kind: payment`) любой суммы больше нуля. Раньше он становился пылью, и инвойс, которому не хватало центов, оставался `underpaid`. См. [частичная оплата](https://stons.io/ru/docs/invoices.md#частичная-оплата).
- Первый перевод по инвойсу, перевод после того, как сумма набрана, нулевой перевод и перевод на адрес `per_user` меньше минимального депозита — по-прежнему пыль.
- Поля и коды ошибок не менялись.
## 2026-10-14, нативная монета из смарт-контракта засчитывается
- **Нативная монета EVM-сети, которую отправил смарт-контракт, теперь засчитывается** — со смарт-кошелька, мультиподписи, вывода с биржи: она оплачивает инвойс, попадает в ответ `GET /v1/deposits/{tx_hash}` и даёт вебхуки `deposit.*`, как обычный перевод с кошелька. Сервис находит такой перевод трейсом блока и засчитывает, только если трейс объясняет весь рост баланса адреса; перевод, который объяснить не удалось, по-прежнему разбирает оператор, — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес).
- У такого перевода `log_index` = `-2`, `-3` и далее, как уже у перевода, сделанного контрактом в TRON. В одной транзакции их может быть несколько, и рядом — её собственная сумма с `-1`. **Проверьте интеграцию**, если она считает, что `log_index` нативной монеты всегда `-1` или что транзакция платит адресу один раз.
- Маршруты, поля, тела вебхуков и коды ошибок не менялись. Решение — ADR-0055.
## 2026-10-14, страница оплаты оценивает ожидание подтверждений
- **Новое поле `confirmation_estimate` в `GET /checkout/v1/invoices/{id}`** и в ответе `POST /checkout/v1/invoices/{id}/select`: пока перевод ждёт подтверждений — сколько блоков ему ещё нужно, сколько это секунд по времени блока сети и ждёт ли сеть ещё и окончательного блока ([страница оплаты](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы)). Страница показывает это как «Осталось около N минут», а не обещает около минуты в любой сети.
## 2026-10-14, у нативной монеты нет стандарта токенов
- **`token_standard` — `null` у нативной монеты** в `GET /checkout/v1/invoices/{id}` и в ответе `POST /checkout/v1/invoices/{id}/select`, и у инвойса, и у каждого элемента `choices`: BNB — не токен BEP-20, SOL — не токен SPL ([страница оплаты](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы)). Раньше поле называло стандарт сети для любого актива.
## 2026-10-06, цена в долларах в выбранной монете
- **Новое поле `price_currency` в `POST /v1/invoices`.** Единственное значение — `USD`, и только вместе с `asset`. С ним `amount` — цена в долларах, а не количество монет: сервис сам пересчитывает её в названный актив по своему курсу (ADR-0070, [цена в долларах в выбранной монете](https://stons.io/ru/docs/invoices.md#цена-в-долларах-в-выбранной-монете)).
- **Ответ на создание сразу готов к оплате:** `address`, `amount_expected`, `price`, `quote.rate` и `expires_at` приходят в ответе на `POST`, без страницы оплаты и без выбора покупателя.
- **Поле необязательное, прежний смысл сохранён:** без `price_currency` `amount` по-прежнему сумма в единицах актива. Существующие интеграции менять не нужно.
- `price_currency` без `asset` отклоняется (`400 validation_failed`): там `amount` и так цена в долларах.
- **Курс недоступен — инвойс не создаётся:** `503 rate_unavailable`, и в базе ничего не остаётся.
- **Новый маршрут `GET /v1/rates`:** по каждому активу проекта курс, по которому будет посчитан инвойс (рыночный за вычетом наценки сервиса), сам рыночный курс, процент наценки, время наблюдения и признак свежести. Чтобы «≈ 0,45 SOL» рядом с ценой совпадало с суммой инвойса.
## 2026-10-05, интеграция настраивает проект своим ключом
- **Новые маршруты** `GET` и `PATCH /v1/project`, `GET` и `PUT /v1/project/assets`. Интеграция — или ассистент, которому дали один ключ API — сама указывает адрес вебхука, адреса возврата и принимаемые токены. В кабинет для этого заходить не нужно (ADR-0067, [аутентификация](https://stons.io/ru/docs/authentication.md#настройки-проекта)).
- **Секрет подписи вебхуков эти маршруты не возвращают.** Он показывается один раз, при создании проекта; ключ API — не доступ к секрету.
- **Адрес вебхука можно задать и заменить, но не удалить:** `null` — `400`. События, уже поставленные в очередь, должны куда-то уходить.
- Режим адресов и режим хранения средств этими маршрутами не меняются: их разрешает оператор.
## 2026-10-05, режим адресов разрешается по проектам
- **Новый код ошибки `422 mode_not_allowed`** на `POST /v1/addresses`, `POST /v1/invoices` и `POST /checkout/v1/invoices/{id}/select`: режим адресов этого проекта ему не разрешён. Оператор разрешает ограниченный режим по проектам, а не всей платформе сразу (ADR-0066). Прежний `422 address_mode_disabled` остался и значит другое: платформа приостановила режим для всех.
- **Режим адресов проекта стало можно менять.** Раньше он выбирался один раз при создании. Теперь оператор разрешает режим, и владелец проекта переключается на него в кабинете. Смена решает, где будет собран следующий платёж, и никогда — где собран прежний: уже выданные адреса остаются при своём, зачисляются и переносятся как раньше.
- **Создать проект сразу в `per_user` из кабинета нельзя:** `422 mode_not_allowed`. Создавайте в `per_invoice` и переключайтесь после того, как оператор разрешит режим.
## 2026-10-05, Solana
Solana — четвёртое семейство сетей, с которым работает сервис. Ни один маршрут, ни одно поле и ни одно событие вебхука не изменили форму; изменился набор сетей и активов, которые они несут.
- **Приём платежей работает в десяти сетях:** к `tron`, `ethereum`, `bsc`, `polygon`, `base`, `arbitrum`, `optimism`, `avalanche` и `ton` добавилась `solana`. Её активы — `USDT_SOLANA`, `USDC_SOLANA` и нативная монета `SOL_SOLANA`, каждому нужно 1 подтверждение; mint'ы и число знаков — в [активах Solana](https://stons.io/ru/docs/invoices.md#активы-solana).
- **Выплат в Solana нет**, как нет их в EVM-сетях и TON: `422 asset_not_payable`.
- **`tx_hash` перевода в Solana — не шестнадцатеричный хэш.** Это первая подпись транзакции: 86–88 знаков base58, у которых регистр несёт смысл и должен сохраняться. В колонку, рассчитанную на 64 шестнадцатеричных знака, она не поместится, а приведение к нижнему регистру уничтожает значение. Это касается `GET /v1/deposits/{tx_hash}`, поля `deposits` инвойса и вебхуков `deposit.*` — см. [адреса в Solana](https://stons.io/ru/docs/addresses.md#адреса-в-solana).
- **Адрес Solana — тоже base58 из 32–44 знаков и тоже регистрозависим.** Храните, сравнивайте и показывайте его ровно таким, каким его вернул API; регистр не нормализуйте никогда.
- `wallet_url` на странице оплаты в Solana остаётся `null`: ссылку-диплинк кошелька сервис собирает только в TON — см. [прочитать инвойс для страницы](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы).
- Токен в Solana лежит в associated token account, а не на самом адресе; этот аккаунт сервис создаёт и наблюдает сам. Кошелёк покупателя платит на адрес как обычно, а код, который проверяет платёж чтением цепи, должен знать: `getSignaturesForAddress` на адрес перевода SPL не покажет.
## 2026-10-05, какие сети и токены работают на самом деле
Запись исправляет документацию, а не поведение API: ни один маршрут и ни одно поле не менялись. Прежние записи журнала говорили, что EVM-сети в разработке, а TON планируется; это перестало быть правдой раньше, чем документация это отразила.
- **Приём платежей работает в девяти сетях:** `tron`, `ethereum`, `bsc`, `polygon`, `base`, `arbitrum`, `optimism`, `avalanche`, `ton`. Их активы перечисляет `GET /v1/assets`, контракты и число знаков — в [активах](https://stons.io/ru/docs/invoices.md#активы).
- **Нативные монеты принимаются** наравне с токенами, по курсу: `TRX_TRON`, `GRAM_TON`, `ETH_ETHEREUM`, `BNB_BSC`, `POL_POLYGON`, `ETH_BASE`, `ETH_ARBITRUM`, `ETH_OPTIMISM`, `AVAX_AVALANCHE`. Засчитывается только обычный перевод с кошелька.
- **USDT в Polygon, Arbitrum и OP Mainnet — это USDT0**, отдельный актив с собственным контрактом: `USDT0_POLYGON`, `USDT0_ARBITRUM`, `USDT0_OPTIMISM`.
- **`USDC_TRON` выключен оператором целиком**, а не только для новых счетов: выплаты в нём тоже выключены и отклоняются с `422 asset_payouts_disabled`. Документация до этой записи обещала, что выплаты в нём работают, — это было неверно.
- **Выплаты по-прежнему только в сети TRON**, и только в `USDT_TRON`. В EVM-сетях и TON выплат нет: `422 asset_not_payable`.
- Список сетей и токенов больше не описывается статусами «работает / в разработке / планируется»: [сети и токены](https://stons.io/ru/docs/networks.md) говорят, что сервис делает в каждой сети, а что доступно проекту в данный момент — ответ `GET /v1/assets`. Актив, выключенный оператором или настройками проекта, в этом ответе не появляется.
## 2026-10-05, кошелёк сборов виден в API
- API кабинета: у объекта проекта появилось `custody_mode` — `custodial` (деньги хранит сервис) или `pass_through` (деньги идут на кошелёк самого проекта). Меняет его только оператор.
- API кабинета: `GET /cabinet/v1/merchants/{id}/settlement-wallets` отдаёт кошельки проекта, на которые пересылаются принятые средства, по одному на актив: адрес целиком, с какого момента он годен и годен ли уже. У кастодиального проекта список пуст. См. куда уходят деньги.
- Административный API: `GET /admin/v1/merchants/{id}/settlement-wallets` отдаёт то же плюс кто внёс запись и отпечаток ключа, которым она подписана.
- API мерчантов и тела вебхуков не изменились. Запись кошелька по-прежнему делается командой с подписью оператора, а не через API.
## 2026-10-05, некастодиальный режим в административном API
- Административный API: у объекта мерчанта появилось `custody_mode` — `custodial` (наше хранилище) или `pass_through` (кошелёк сборов самого мерчанта). `PATCH /admin/v1/merchants/{id}` его принимает; перевод в `pass_through` без действующего кошелька сборов отвечает `422 settlement_wallet_missing`.
- Административный API: `GET /admin/v1/custody-modes` и `PATCH /admin/v1/custody-modes/{mode}` — тумблер сквозного режима на платформе. Кастодиальный режим выключить нельзя: `422 custody_mode_last_enabled`.
- API мерчантов, API кабинета и тела вебхуков не изменились. Кошелёк, на который пересылаются деньги мерчанта, вносит оператор подписью вне сервиса, а не через API.
- Для интеграций описано: что значит `settled` у проекта с прямыми сборами, почему его баланс остаётся нулевым и что мелкие платежи копятся на адресе приёма, пока перевод не начнёт стоить своих затрат, — см. [куда уходят деньги](https://stons.io/ru/docs/invoices.md#куда-уходят-деньги).
## 2026-10-04, баланс проекта по API
- API мерчантов: `GET /v1/balance` отдаёт баланс по реестру каждого актива, в котором у проекта что-то есть, рядом — что ещё не подтверждено, и те же деньги в долларах по принятым сервисом курсам. Актив без свежего курса попадает в список и в `total.unpriced_assets`, а не оценивается по устаревшему. В ответе есть `ETag`. См. [баланс](https://stons.io/ru/docs/balance.md).
- Ни один существующий маршрут или поле не изменились.
## 2026-10-04, QR-код инвойса и условные чтения
- API мерчантов: `POST /v1/invoices` и `GET /v1/invoices/{id}` принимают параметр строки запроса `include=qr` и тогда отдают `qr.address` — QR-код адреса оплаты в виде SVG-картинки в `data:`-ссылке, около 11 КБ. Это та же картинка, которую рисует страница оплаты, поэтому продукту, который рисует свою страницу оплаты, библиотека QR не нужна. Без параметра поля нет; лента изменений этот параметр не принимает. См. [QR-код](https://stons.io/ru/docs/invoices.md#qr-код).
- API мерчантов: `GET /v1/assets`, `GET /v1/invoices`, `GET /v1/invoices/{id}` и `GET /v1/deposits/{tx_hash}` отвечают заголовком `ETag`. Запрос, который вернул его в `If-None-Match`, получает `304 Not Modified` без тела, пока ответ не изменился; `*` совпадает всегда. Ошибки метки не получают. См. [условные чтения](https://stons.io/ru/docs/authentication.md#условные-чтения).
- **Ломающее** для клиента, который присылал свои параметры строки запроса: `POST /v1/invoices` и `GET /v1/invoices/{id}` теперь принимают только `include` и отклоняют любой другой параметр с `400 validation_failed`. В документации таких параметров не было; клиенту, который добавлял к этим двум адресам свои метки, нужно перестать.
## 2026-10-04, лента изменений инвойсов
- API мерчантов: `GET /v1/invoices` отдаёт инвойсы проекта в порядке изменения состояния — догон после пропущенного вебхука. Параметры `updated_since`, `status`, `limit` (1–100, по умолчанию 50) и `cursor`; в ответе `invoices` без поля `deposits` и `next_cursor`. Страницы keyset'ом: инвойс, изменившийся во время обхода, страницу не сдвигает. См. [ленту изменений](https://stons.io/ru/docs/invoices.md#лента-изменений).
- Прежние маршруты и поля не изменились.
## 2026-10-04, шаг суммы у токена, привязанного к доллару
- API мерчантов: сумма инвойса в токене, привязанном к доллару, округляется вверх до цента этого токена, а не до его последнего знака. Цена $229.00 при курсе 0.9999 просит `229.03`, а не `229.024488`. Округление по-прежнему вверх: покупатель платит не меньше цены, разница меньше цента и в пользу мерчанта. У токенов, не привязанных ни к чему, шаг прежний — 0.00000001 единицы. Так же шагает остаток, пересчитанный после истечения окна котировки. См. [цену и курс](https://stons.io/ru/docs/invoices.md#цена-и-курс) и ADR-0053.
- Поля и коды ошибок не изменились.
## 2026-10-04, реквизиты оплаты для своей страницы
- API мерчантов: в ответе инвойса появилось поле `payment_uri` — ссылка, открывающая кошелёк с заполненным переводом. Она есть, пока запрошена точная сумма: инвойс ждёт денег, дедлайн не прошёл и котировка держится. Сейчас ссылку даёт только TON; у остальных сетей `null`, потому что схемы ссылок, которой сервис доверяет, там нет.
- API мерчантов: в `GET /v1/assets` появились `confirmations` — сколько подтверждений нужно переводу в активе, и `icon` — метка актива в каталоге. По ним мерчант рисует «2 из 20» и список монет на своей странице оплаты, не придумывая эти значения.
- Совместимость сохранена: прежние поля не изменились и не исчезли.
- Внутренние маршруты первичной настройки `/setup/*` убраны из `GET /v1/openapi.json`: документ описывает только маршруты самого мерчанта.
## 2026-09-20, отметка строки вывода выплаченной вручную
- Административный API: `POST /admin/v1/withdrawals/{id}/lines/{line_id}/force-paid` отмечает строку заявки выплаченной, не дожидаясь подтверждения из цепи, с обязательной причиной. Это единственное решение сотрудника по заявке, которое двигает деньги: проводится то же, что провела бы сверка, и теми же ключами. В объекте заявки у строк появились `forced_by`, `forced_at` и `force_reason`. См. вывод средств мерчанта и ADR-0058.
- API мерчантов, API кабинета и тела вебхуков не изменились.
## 2026-09-20, плата за вывод в долларах
- Административный API: `POST /admin/v1/withdrawal-fees` и `POST /admin/v1/merchants/{id}/withdrawal-fees` принимают `amount_usd` вместо `amount` — фиксированную часть в долларах. Сервис переводит её в монету по свежему курсу при каждой заявке и замораживает в строке; сумма в монете хранится рядом и действует, когда свежего курса нет. В ответе у действующей платы и у строк тарифа появилось поле `amount_usd`. Без свежего курса запись такой строки отвечает `503 rate_unavailable`. См. плата за вывод средств и ADR-0059.
- API мерчантов, API кабинета и тела вебхуков не изменились. Совместимость административного API сохранена: `amount` по-прежнему принимается.
## 2026-09-20, своя плата за вывод
- Административный API: у платы за вывод средств мерчанта свой тариф — `GET /admin/v1/withdrawal-fees`, `POST /admin/v1/withdrawal-fees`, `GET /admin/v1/merchants/{id}/withdrawal-fees`, `POST /admin/v1/merchants/{id}/withdrawal-fees` — и оверрайд по отдельному проекту. Новые коды ошибок `withdrawal_fee_confirmation_required` и `withdrawal_fee_valid_from_in_past` — см. плата за вывод средств. Решение — ADR-0059.
- **Несовместимо** для клиентов административного API, которые задавали плату за вывод через `POST /admin/v1/payout-fees`: теперь это только тариф выплат третьим лицам, а плату за вывод задают маршруты выше. История прежнего тарифа перенесена в новый, поэтому действующие числа не изменились.
- API мерчантов, API кабинета и тела вебхуков не изменились.
## 2026-09-19, вывод средств мерчанта
- API кабинета: заявки на вывод средств проекта на кошельки владельца — `GET /cabinet/v1/withdrawals/assets`, `GET /cabinet/v1/withdrawals`, `POST /cabinet/v1/withdrawals/preview`, `POST /cabinet/v1/withdrawals`, `GET /cabinet/v1/withdrawals/{id}`, `POST /cabinet/v1/withdrawals/{id}/cancel`. В заявке несколько токенов и кошельков, суммы с платами откладываются сразу, создание требует подтверждённого кабинетом кода второго фактора — см. вывод средств. Решение — ADR-0058.
- Административный API: `GET /admin/v1/withdrawals/summary`, `GET /admin/v1/withdrawals`, `GET /admin/v1/withdrawals/{id}`, внесение хэша ручного перевода `POST /admin/v1/withdrawals/{id}/lines/{line_id}/tx` и отказы `POST /admin/v1/withdrawals/{id}/lines/{line_id}/reject`, `POST /admin/v1/withdrawals/{id}/reject` — см. вывод средств мерчанта. Тариф выплат `POST /admin/v1/payout-fees` задаётся любому активу каталога, а не только тому, где есть выплаты: он же — плата за вывод; `GET /admin/v1/payout-fees` перечисляет все действующие активы.
- Новые коды ошибок этих маршрутов: `withdrawal_not_found`, `withdrawal_insufficient_balance`, `withdrawal_not_cancelable`, `withdrawal_line_not_found`, `withdrawal_line_not_open`, `withdrawal_tx_in_use`, `withdrawal_code_required`.
- API мерчантов и тела вебхуков не изменились.
## 2026-09-19, средства в долларах в кабинете
- API кабинета: `GET /cabinet/v1/balances` возвращает рядом с `balances` новые поля `assets`, `projects` и `total`. Это балансы по активам всех проектов, по проектам и в сумме, с оценкой в долларах США по рыночному курсу. Актив без свежего курса в суммы не входит и перечислен в `unpriced_assets` — см. API кабинета.
- API мерчантов, тела вебхуков и коды ошибок не изменились.
## 2026-09-18, TON
- Новая сеть `ton` — боевая сеть TON с активами `USDT_TON` (6 знаков) и `GRAM_TON` — Gram, нативная монета TON (9 знаков). `USDT_TON` оплачивает инвойсы; `GRAM_TON` — после того как платформа включит приём в нём, как `TRX_TRON`. Выплат в TON нет. `GET /v1/assets` перечисляет активы TON, когда оператор включит сеть, — см. [сети и токены](https://stons.io/ru/docs/networks.md) и [активы](https://stons.io/ru/docs/invoices.md#активы).
- **Адрес в TON возвращается в user-friendly форме**: 48 символов base64url, без флага bounce — `UQ…`, в тестовой сети `0Q…`. В этой форме `address` приходит в `POST /v1/addresses`, в инвойсах, в `GET /v1/deposits/{tx_hash}` и в вебхуках `deposit.*`, и так его показывают покупателю, ничего не меняя, — см. [адреса в TON](https://stons.io/ru/docs/addresses.md#адреса-в-ton). `contract` токена TON в `GET /v1/assets` — мастер-контракт жетона в raw-форме: `0:` и 64 шестнадцатеричные цифры. **Проверьте интеграцию**, если она сверяет адрес с формами TRON и EVM-сетей: форму TON тоже нужно принимать.
- Хэш транзакции TON — 64 шестнадцатеричных символа без `0x`, как в TRON; `GET /v1/deposits/{tx_hash}` его принимает. В TON `log_index` у перевода токена `0`, у перевода GRAM `-1` — см. [проверить перевод](https://stons.io/ru/docs/addresses.md#проверить-перевод).
- Маршруты страницы оплаты отдают новое поле `wallet_url`: в TON — ссылку `ton://transfer`, которая открывает приложение кошелька с адресом, оставшейся суммой и токеном; в других сетях `null`. Страница показывает её кнопкой «Открыть в кошельке» — см. [прочитать инвойс для страницы](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы). Мерчантам эти маршруты не нужны.
- Административный API: семейство `ton`, у которого `chain_id` — глобальный идентификатор сети; `chain_symbol` в предпросмотре токена бывает `null`, если жетон хранит символ вне цепи; мастер-контракт жетона TON принимается в raw-форме и в user-friendly; порог газового кошелька и `gas_wallet` есть и у TON — см. административный API.
- Коды ошибок и тела вебхуков `invoice.*` не изменились.
## 2026-09-16, платежи в нативных монетах EVM-сетей
- Платформа может принимать к оплате нативные монеты EVM-сетей — `ETH_ETHEREUM`, `BNB_BSC`, `POL_POLYGON`, `ETH_BASE`, `ETH_ARBITRUM`, `ETH_OPTIMISM`, `AVAX_AVALANCHE`, — когда включит их сеть и приём в них. Они ведут себя как `TRX_TRON`: цена по курсу, `GET /v1/assets` перечисляет их, пока в них можно выставить инвойс, 18 знаков, выплат нет — см. [сети и токены](https://stons.io/ru/docs/networks.md) и [активы](https://stons.io/ru/docs/invoices.md#активы). EVM-сети по-прежнему в разработке.
- **Засчитывается только обычный перевод с кошелька.** Монета, отправленная смарт-контрактом (смарт-кошельком, некоторыми биржами), автоматически не засчитывается, не попадает в ответ `GET /v1/deposits/{tx_hash}`, и вебхука нет — см. [что приходит на адрес](https://stons.io/ru/docs/addresses.md#что-приходит-на-адрес).
- У перевода такой монеты `log_index` = `-1` в `GET /v1/deposits/{tx_hash}` и в вебхуках `deposit.*`, как уже у перевода TRX. **Проверьте интеграцию**, если она считает, что `log_index` не бывает отрицательным.
- Маршруты, поля, тела вебхуков и коды ошибок не изменились. Административный API: `PATCH /admin/v1/assets/{id}` включает `accepts_payments` у нативной монеты EVM-сети вместе с её `rate_symbol`, и `payments_supported` у неё — `true`, — см. административный API. Решение — ADR-0055.
## 2026-09-16, платежи в TRX
- Платформа может принимать к оплате `TRX_TRON` — нативную монету TRON, когда платформа включает приём в нём. `GET /v1/assets` перечисляет `TRX_TRON`, пока в нём можно выставить инвойс: список берите оттуда — см. [список активов](https://stons.io/ru/docs/invoices.md#список-активов) и [активы](https://stons.io/ru/docs/invoices.md#активы).
- Такие инвойсы и выбор работают так же, как у любого актива, цена в который переводится по курсу. Инвойс, созданный без `asset` и оплачиваемый в `TRX_TRON`, переводится по курсу, пониженному на наценку платформы, а остаток долга после окна котировки пересчитывается — см. [цену и курс](https://stons.io/ru/docs/invoices.md#цена-и-курс). У инвойса, созданного с `asset` = `TRX_TRON`, сумма в TRX, её задаёт мерчант.
- Выплаты в `TRX_TRON` невозможны: `POST /v1/payouts` отвечает `422 asset_not_payable`.
- **Проверьте интеграцию**, если она сверяет `asset` инвойса, перевода или вебхука с фиксированным перечнем: примите `TRX_TRON`. Число знаков берите из `GET /v1/assets`, а не из кода.
- Маршруты, поля, тела вебхуков и коды ошибок не изменились. Административный API: `PATCH /admin/v1/assets/{id}` включает `accepts_payments` у `TRX_TRON`, а для нативной монеты EVM-сети и для снятия `rate_symbol` у актива, принимаемого без привязки к доллару США, отвечает `409 asset_flag_locked` — см. административный API. Решение — ADR-0054.
## 2026-09-16, цены в долларах США по курсу
- Инвойс, созданный без `asset`, переводится в выбранный токен по курсу, который собирает сервис, в том числе стейблкоин: 1 USDT больше не считается равным 1 USD. Сумма округляется вверх до последнего знака токена, а у токена, у которого знаков больше 8, — до 8 знаков, поэтому покупатель платит не меньше цены. Токен, не привязанный к доллару США, переводится по курсу, пониженному на наценку платформы. См. [цену и курс](https://stons.io/ru/docs/invoices.md#цена-и-курс); решение — ADR-0053.
- **Несовместимо** для кода, который сравнивает свою цену с `amount_requested` инвойса, созданного без `asset`: цена, переведённая по курсу, не равна `price.amount`. Сравнивайте с `price.amount`.
- **Несовместимо** для кода, который читает `amount_expected` или `amount_remaining` такого инвойса один раз. После окна котировки, пока инвойс в статусе `created` или `partially_paid` и дедлайн не прошёл, остаток долга пересчитывается по текущему курсу, и эти суммы и `amount_requested` меняются. Вебхука об этом нет: читайте суммы из `GET /v1/invoices/{id}`. См. [окно котировки](https://stons.io/ru/docs/invoices.md#окно-котировки).
- Цена, которая не записывается в токене точно, больше не отклоняется с `400 validation_failed`: она округляется вверх.
- Новое поле `quote` в ответах `POST /v1/invoices` и `GET /v1/invoices/{id}`: `rate`, `market_rate`, `markdown_percent`, `currency`, `number` и `expires_at`. Оно `null` у инвойса, созданного с `asset`, до выбора и у инвойса, токен которого выбран до этого изменения. См. [котировку](https://stons.io/ru/docs/invoices.md#котировка).
- **Проверьте интеграцию** на новый код ошибки: `POST /v1/invoices` без `asset` отвечает `503 rate_unavailable`, когда ни у одного разрешённого токена нет свежего курса, и инвойс не создаётся. Повторите позже с тем же `order_id`. `422 asset_not_offered` теперь значит, что у токена из `currencies` нет курса, а `422 no_payable_assets` — что мерчант не принимает ни одного токена с курсом. См. [коды ошибок](https://stons.io/ru/docs/errors.md#перечень).
- Маршруты страницы оплаты: `GET /checkout/v1/invoices/{id}` отдаёт `rate`, `rate_id` и `rate_expires_at` в каждом элементе `choices`, а у инвойса — `rates_unavailable` и `quote` с `rate`, `currency` и `expires_at`. `POST /checkout/v1/invoices/{id}/select` принимает необязательный `rate_id` и может ответить `503 rate_unavailable`. См. [сохранение выбора токена](https://stons.io/ru/docs/checkout.md#сохранить-выбор-токена). Мерчантам эти маршруты не нужны.
- API кабинета: `quote` у инвойсов; `POST /cabinet/v1/invoices` может ответить `503 rate_unavailable`. Административный API: `quote` у инвойсов, новые маршруты `GET /admin/v1/rates` и `PATCH /admin/v1/rates/settings` и поле актива `rate_symbol` — см. административный API.
- Тела вебхуков не изменились. Инвойсы, созданные с `asset`, работают как раньше.
## 2026-09-14, газовый кошелёк в реестре
- Административный API: новый вид счёта реестра `gas_wallet` в `GET /admin/v1/balances` и в проводках `GET /admin/v1/ledger/transactions` — газовый кошелёк EVM-сети в её нативной монете. Проводка газа переноса в хранилище — `reason` = `fee`, ключ `sweep_gas:{network}:{tx_hash}`: дебет `network_fees`, кредит `gas_wallet`. См. административный API.
- Публичный API, вебхуки и коды ошибок не изменились.
## 2026-09-14, управление сетями и токенами
- Административный API управляет сетями и токенами — административный API. Новые маршруты: `POST /admin/v1/networks`, `PATCH /admin/v1/networks/{id}`, `GET /admin/v1/networks/{id}/tokens/{contract}`, `POST /admin/v1/assets` и `DELETE /admin/v1/assets/{id}`. `PATCH /admin/v1/assets/{id}` меняет ещё минимумы, подтверждения, порог свипа, флаги приёма и выплат, архив и знак, а `GET /admin/v1/assets` принимает параметр `network`.
- Новые поля в ответах административного API: у сети — `icon_slug` и `rpc_configured`, у актива — `pegged_to`, `sweep_min_amount`, `icon_slug` и `payouts_supported`. Новые коды ошибок: `asset_exists`, `asset_in_use`, `asset_referenced`, `asset_confirmations_below_finality`, `asset_symbol_required`, `contract_invalid`, `contract_not_token`, `network_not_found`, `network_exists`, `network_without_assets`, `network_family_not_supported`, `network_rpc_not_configured`, `network_chain_id_mismatch`, `network_rpc_unavailable`.
- Маршруты страницы оплаты `GET /checkout/v1/invoices/{id}` и `POST /checkout/v1/invoices/{id}/select` отдают `network_icon_slug` — slug знака сети — у инвойса и в каждом элементе `choices`, см. [прочитать инвойс для страницы](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы). API мерчантов не изменился. Новые EVM-сети появляются в реестре выключенными, и их активы мерчантам не предлагаются.
## 2026-09-14, список активов, хэши EVM-сетей, ссылки на обозреватель
- Новые поля с названием сети и ссылками на её обозреватель. Инвойсы (`POST /v1/invoices`, `GET /v1/invoices/{id}`): `network_name`, `address_explorer_url` и `explorer_url` в каждом элементе `deposits`. `GET /v1/deposits/{tx_hash}`: `explorer_url`. Выплаты: `network_name` и `explorer_url`. Ссылка собирается из шаблона обозревателя сети и равна `null`, если шаблона у сети нет — см. [создать инвойс](https://stons.io/ru/docs/invoices.md#создать-инвойс).
- Маршрут страницы оплаты `GET /checkout/v1/invoices/{id}` отдаёт `address_explorer_url` и `explorer_url` в `deposits`, а `address` — в том виде, в каком его видит покупатель: в EVM-сетях с контрольной суммой EIP-55 — см. [прочитать инвойс для страницы](https://stons.io/ru/docs/checkout.md#прочитать-инвойс-для-страницы). API мерчанта отдаёт EVM-адреса в нижнем регистре.
- Новый маршрут `GET /v1/assets`: активы, в которых мерчант может выставить инвойс сейчас, с `network`, `network_name`, `token_standard`, `contract`, `decimals` и `min_amount`. По нему решают, какие токены предложить, вместо списка в коде — см. [список активов](https://stons.io/ru/docs/invoices.md#список-активов).
- `GET /v1/deposits/{tx_hash}` принимает и хэш в форме EVM-сетей: `0x` и 64 шестнадцатеричных символа. Хэш ищется ровно в той форме, в которой передан, в любом регистре; хэш TRON по-прежнему пишется без `0x` — см. [проверить перевод](https://stons.io/ru/docs/addresses.md#проверить-перевод).
- EVM-сети по-прежнему в разработке: пока оператор не включил ни одной, `GET /v1/assets` перечисляет только активы TRON, и для интеграции с TRON ничего не меняется. Коды ошибок не изменились.
## 2026-09-14, сеть в событиях вебхуков
- Новое поле `network` в `data` событий `invoice.*` и `deposit.*`: идентификатор сети актива, например `tron`, — то же значение, что `network` инвойса и перевода в API. В `invoice.expired` инвойса, покупатель которого не выбрал токен, оно `null`. См. [вебхуки](https://stons.io/ru/docs/webhooks.md#тело).
- Поле добавлено, ничего не удалено и не переименовано. **Проверьте интеграцию**, если она отклоняет неизвестные поля в теле вебхука: примите `network`.
- Сервис готовится принимать платежи в EVM-сетях, где один хэш транзакции может быть в нескольких сетях. Перевод определяют `network`, `tx_hash` и `log_index` вместе.
- Маршруты и коды ошибок не изменились.
## 2026-09-14, USDC в TRON только для выплат
- **Проверьте интеграцию**, если она создаёт инвойсы в `USDC_TRON` или передаёт его в `currencies`. Circle прекратила выпуск USDC в сети TRON, и оператор выключил приём в `USDC_TRON`: `POST /v1/invoices` в этом активе и с ним в `currencies` отвечает `422 asset_payments_disabled`. Уберите актив из запросов или обрабатывайте этот код. См. [активы](https://stons.io/ru/docs/invoices.md#активы).
- Страница оплаты не предлагает `USDC_TRON` покупателю, а `POST /v1/addresses` не включает его в `assets`.
- Выплаты в `USDC_TRON` работают. Уже открытые инвойсы в `USDC_TRON` оплачиваются, а перевод USDC на постоянный адрес `per_user` зачисляется, как раньше.
- Маршруты, поля и коды ошибок не изменились. Обоснование — ADR-0026, поправка от 2026-09-14.
## 2026-09-14, частичная оплата
- Новый статус инвойса `partially_paid`: до дедлайна подтверждённые переводы покрыли часть `amount_expected`, и ни один перевод не ждёт подтверждений. Покупатель может дослать остаток на тот же адрес до дедлайна. Допуска недоплаты нет: `confirmed` по-прежнему означает всю сумму, а после дедлайна без остатка приходит `underpaid`. См. [частичную оплату](https://stons.io/ru/docs/invoices.md#частичная-оплата).
- Новое событие вебхука `invoice.partially_paid` — при каждом переходе в этот статус. См. [события](https://stons.io/ru/docs/webhooks.md#события).
- Новое поле `amount_remaining` в ответах `POST /v1/invoices` и `GET /v1/invoices/{id}`: `amount_expected` минус `amount_received`, не меньше нуля; `null`, пока покупатель не выбрал токен. То же поле — в ответе маршрута страницы оплаты `GET /checkout/v1/invoices/{id}`.
- **Несовместимо** для кода, который разбирает `status` или тип события строгим перечнем. Раньше частично оплаченный инвойс до дедлайна оставался `seen`; теперь он `partially_paid`, а `seen` означает, что перевод ждёт подтверждений. Добавьте `partially_paid` в перечень и не отдавайте при нём товар. На `invoice.partially_paid`, если обработчик его не обрабатывает, отвечайте `2xx`.
- **Проверьте интеграцию.** Кнопка «Вернуться в магазин» ведёт на `cancel_url` и со `status=partially_paid`: инвойс открыт и ещё может быть оплачен — см. [переход до исхода](https://stons.io/ru/docs/checkout.md#переход-до-исхода).
- Страница оплаты при `partially_paid` просит дослать `amount_remaining` на тот же адрес, а при `seen` больше не пишет, что ничего отправлять не нужно. См. [страницу оплаты](https://stons.io/ru/docs/checkout.md#страница-для-покупателя).
- API кабинета и административный API: `partially_paid` среди статусов инвойса и в фильтре `status`. Коды ошибок не изменились.
## 2026-09-14, проверка перевода и пауза режима адресов
- Новый маршрут `GET /v1/deposits/{tx_hash}`: переводы транзакции на адреса мерчанта с `kind`, `status`, `confirmations` и `credited`. Пополнение `per_user` без инвойса теперь проверяется через API, как требует главное правило вебхуков, — см. [проверку перевода](https://stons.io/ru/docs/addresses.md#проверить-перевод). Новый код `404 deposit_not_found`.
- **Проверьте интеграцию.** Платформа может приостановить режим адресов. Пока режим на паузе, его проекты получают `422 address_mode_disabled` от `POST /v1/addresses`, `POST /v1/invoices` и `POST /checkout/v1/invoices/{id}/select`, а новый проект не может выбрать этот режим, — см. [режим на паузе](https://stons.io/ru/docs/addresses.md#режим-на-паузе). Переводы на уже выданные адреса зачисляются как обычно. Обрабатывайте код как любой другой `422`: сообщите покупателю, что оплата сейчас недоступна.
- API кабинета: `GET /cabinet/v1/address-modes`; `POST /cabinet/v1/merchants` и `POST /cabinet/v1/invoices` могут ответить `422 address_mode_disabled`. Административный API: `GET /admin/v1/address-modes` и `PATCH /admin/v1/address-modes/{mode}`, код `422 address_mode_last_enabled` — административный API.
## 2026-09-13, событие о неудавшейся выплате
- Вебхук `payout.failed` приходит при каждом переходе выплаты в `failed`. Раньше — только когда сканер находил её транзакцию неуспешной. Теперь ещё: когда сервер подписи отказался её подписывать; когда попытки исчерпаны; когда неуспешную транзакцию прежней попытки нашёл сам сервер подписи. Событие одно на выплату. См. [события](https://stons.io/ru/docs/webhooks.md#события).
- **Несовместимо** для кода, который разбирает `payout.failed` строго: `tx_hash` в нём бывает `null` — у выплаты, для которой ничего не подписывалось. Поле `reason` — текст для человека.
- Выплата больше не становится `failed`, если узел ответил на отправку её транзакции ошибкой или не ответил вовсе: деньги могли уйти. Она остаётся в `queued` с тем же `tx_hash`, пока транзакция не найдётся в цепи или не истечёт, не попав в блок. См. [статусы выплат](https://stons.io/ru/docs/payouts.md#статусы).
- Маршруты и коды ошибок не изменились.
## 2026-09-13, документация на английском
- Документация публикуется на двух языках. Английская — основная, по прежним адресам: `/docs`, `/docs/<раздел>.md`, `/llms.txt` и `/llms-full.txt`. Русская — под `/ru`: `/ru/docs`, `/ru/docs/<раздел>.md`, `/ru/llms.txt` и `/ru/llms-full.txt`. Интеграциям, которые читают русский Markdown по прежним адресам, нужно перейти на адреса с `/ru`.
- Страница оплаты открывается на языке покупателя: параметр `lang` в `checkout_url`, затем выбор покупателя на странице, затем язык браузера, иначе английский. См. [страницу оплаты](https://stons.io/ru/docs/checkout.md#страница-для-покупателя).
- API, коды ошибок и события вебхуков не изменились.
## 2026-09-13, токен выбирает покупатель
- `asset` в `POST /v1/invoices` стал необязательным. Без него токен и сеть выбирает покупатель на странице оплаты, а `amount` — цена в долларах США. Новое необязательное поле `currencies` — из каких активов выбирать. См. [токен выбирает покупатель](https://stons.io/ru/docs/invoices.md#токен-выбирает-покупатель), решение — ADR-0042.
- У инвойса новые поля `price`, `currencies` и `asset_selected_at`.
- **Несовместимо** для кода, который разбирает инвойсы строго. У инвойса без выбранного токена равны `null`:
- `asset`, `network`, `address`;
- `amount_requested`, `amount_expected`, `amount_received`, `amount_credited`;
- `fees.service.fixed` и `fees.service.amount`.
Интеграции, которые всегда передают `asset`, таких инвойсов не создают, и менять им ничего не нужно. Те же `null` — в событии `invoice.expired` инвойса, срок которого вышел без выбора, и в ответах API кабинета и консоли.
- Повтор `POST /v1/invoices` без `asset` сравнивает цену и набор `currencies`: другой набор — `409 order_id_conflict`.
- Новые коды: `422 asset_required`, `422 no_payable_assets`, `422 asset_not_offered`; у маршрута страницы оплаты ещё `409 asset_already_selected` и `409 invoice_expired`.
- Маршрут страницы оплаты `GET /checkout/v1/invoices/{id}`: поля `price` и `choices`, поля актива и сумм допускают `null`. Новый маршрут `POST /checkout/v1/invoices/{id}/select`. Мерчантам они не нужны.
- `POST /cabinet/v1/invoices`: `asset` необязателен, новое поле `currencies`.
- Административный API: в инвойсе поля `price`, `currencies` и `asset_selected_at`; актив, адрес и суммы допускают `null`.
## 2026-09-13, срок инвойса и адреса возврата
- `expires_in` в `POST /v1/invoices` стал необязательным: без него инвойс ждёт платежа 3600 секунд (1 час).
- **Несовместимо** для запросов со сроком меньше 5 минут или больше 72 часов. Границы `expires_in` теперь от 300 до 259200 секунд — в `POST /v1/invoices` и в `POST /cabinet/v1/invoices`. Раньше были от 60 до 604800 (7 дней). Запрос вне границ получает `400 validation_failed`, в том числе повтор запроса, принятого раньше: форма проверяется до поиска инвойса. Если интеграция ставит срок больше 72 часов, уменьшите его. Уже созданные инвойсы сохраняют свой срок.
- Переопределения `return_url` и `cancel_url` в `POST /v1/invoices` допускаются и в пределах сайта проекта из заявки (`site_url`): origin должен совпасть с адресом проекта того же вида или с сайтом проекта. На `t.me` и `telegram.me` совпасть должно и имя бота или канала — первый сегмент пути, без учёта регистра; это же правило теперь действует и для адреса проекта того же вида. Раньше без адреса того же вида в настройках проекта переопределение отклонялось с `422 redirect_url_not_allowed`. Требования к форме адреса не изменились — см. [адреса возврата](https://stons.io/ru/docs/checkout.md#адреса-возврата).
- **Проверьте интеграцию.** Пока инвойс можно оплатить, страница оплаты показывает кнопку «Вернуться в магазин», если у инвойса или проекта есть адрес отмены. Кнопка ведёт на `cancel_url` с подписью и `status=created` или `status=seen`: инвойс при этом открыт и ещё может быть оплачен. Если обработчик адреса отмены отменяет заказ, пусть сначала проверит статус через `GET /v1/invoices/{id}` — см. [переход до исхода](https://stons.io/ru/docs/checkout.md#переход-до-исхода). Сама страница, пока инвойс открыт, никуда не переводит.
- `POST /cabinet/v1/invoices`: необязательные `return_url` и `cancel_url` по тем же правилам.
- Маршрут страницы оплаты `GET /checkout/v1/invoices/{id}`: новое поле `back_url` — подписанный адрес для кнопки «Вернуться в магазин».
- Соответствие параметров Heleket — в [странице оплаты](https://stons.io/ru/docs/checkout.md#если-вы-переходите-с-heleket).
## 2026-09-13, процент и фиксированная сумма комиссий
- У комиссии сервиса инвойса появилась фиксированная сумма по активу. Новое поле `fees.service.fixed` — сумма, записанная в инвойс при создании; `fees.service.amount` — по-прежнему всё удержанное, теперь это процент и фиксированная сумма вместе. Поля только добавлены. То же — в данных событий `invoice.*`, в ответах API кабинета и страницы оплаты. См. [комиссии](https://stons.io/ru/docs/invoices.md#комиссии).
- **Проверьте интеграцию.** Если комиссию сервиса платит покупатель, `amount_expected` = цена + процент + фиксированная сумма. Сравнивайте свою цену с `amount_requested`, как и раньше. Пока оператор фиксированную сумму не задал, она равна нулю, и ничего не меняется.
- Фиксированная сумма удерживается один раз на инвойс и не больше полученного: платёж меньше неё удерживается целиком. Пополнение `per_user` без инвойса — отдельная операция со своей фиксированной суммой.
- У комиссии выплаты появился процент от суммы: новые поля `fees.network.percent` и `fees.network.fixed`, а `fees.network.amount` — вся комиссия, фиксированная сумма плюс процент от `amount`, округлённый вниз. То же — в данных событий `payout.confirmed` и `payout.failed`. См. [комиссию за выплату](https://stons.io/ru/docs/payouts.md#комиссия-за-выплату).
- Административный API: маршруты `GET /admin/v1/invoice-fees` и `POST /admin/v1/invoice-fees`, коды `invoice_fee_confirmation_required` и `invoice_fee_valid_from_in_past` — административный API. **Несовместимо** для клиентов административного API: `POST /admin/v1/payout-fees` требует поле `percent` и принимает `confirm_high_rate`, а в ответе у действующей комиссии и у строк тарифа появилось поле `percent`. Консоль обновлена в том же изменении.
## 2026-09-13, комиссия сети
- **Несовместимо** для кода, который проверяет `fees.network.status` инвойса по перечню `quoted`, `pending`, `settled`. У инвойса нет отдельной комиссии сети: затраты сети на приём входят в комиссию сервиса, и `fees.network.status` теперь всегда `included`. Прежние значения больше не приходят. Ключи сохранены: `quote`, `quote_rate` и `actual` — всегда `null`, `payer` — всегда `merchant`. Если интеграция проверяет перечень строго, добавьте `included`. См. [комиссии](https://stons.io/ru/docs/invoices.md#комиссии). То же — в событиях `invoice.*` и в ответах API кабинета и страницы оплаты.
- Коды `422 network_fee_payer_not_supported` и `503 network_fee_quote_unavailable` остаются: покупатель комиссию сети не несёт. Изменились только их сообщения.
- У выплаты новое поле `fees.network`: `payer` — всегда `merchant`, `amount` — фиксированная комиссия сети, записанная в выплату при создании, `charged` — сколько списано с баланса проекта. То же — в данных событий `payout.confirmed` и `payout.failed`. См. [комиссию за выплату](https://stons.io/ru/docs/payouts.md#комиссия-за-выплату).
- Комиссия сети списывается с баланса проекта при подтверждении выплаты, отдельной проводкой реестра; выплата, провалившаяся в цепи, её не несёт. Получатель по-прежнему получает ровно `amount`.
- Административный API: тариф комиссии сети за выплату — `GET /admin/v1/payout-fees` и `POST /admin/v1/payout-fees`, коды `payout_fee_confirmation_required` и `payout_fee_valid_from_in_past`; новый вид счёта реестра `network_cost_recovery` в `GET /admin/v1/balances` и в проводках — административный API.
## 2026-09-13, комиссия сервиса
- У инвойса новые поля: `amount_requested` — цена мерчанта, `amount_credited` — полученное за вычетом удержанных комиссий, `fees` — комиссия сервиса и комиссия сети. Поля только добавлены, прежние не меняют смысла: `amount_expected` — по-прежнему сумма, которую должен заплатить покупатель. См. [комиссии](https://stons.io/ru/docs/invoices.md#комиссии).
- **Проверьте интеграцию.** Если в проекте комиссию сервиса платит покупатель, `amount_expected` больше цены на эту комиссию. Код, который сравнивает `amount_expected` со своей ценой заказа, должен сравнивать `amount_requested`. В проектах, где комиссию платит мерчант (так по умолчанию), `amount_expected` равен цене, как раньше.
- Идемпотентность `POST /v1/invoices` по `order_id` сравнивает `amount` запроса с ценой `amount_requested`: повтор присылает цену, а не сумму с комиссией.
- Комиссия сервиса удерживается с каждого подтверждённого платежа отдельной проводкой реестра и уменьшает баланс мерчанта. Ставка записывается в инвойс при создании: смена тарифа не меняет уже выставленные инвойсы.
- Те же поля — в данных событий `invoice.*` ([вебхуки](https://stons.io/ru/docs/webhooks.md#тело)) и в ответе маршрута страницы оплаты; страница оплаты показывает разбивку «Цена · Комиссия сервиса · Итого», когда комиссию платит покупатель ([страница оплаты](https://stons.io/ru/docs/checkout.md)).
- Новый код `503 network_fee_quote_unavailable`: проект настроен так, что комиссию сети платит покупатель, а посчитать её сейчас нельзя. Такую настройку API кабинета больше не принимает — `422 network_fee_payer_not_supported`.
- Административный API: тарифы комиссии сервиса — `GET /admin/v1/service-fees`, `POST /admin/v1/service-fees/platform`, `GET /admin/v1/merchants/{id}/service-fees`, `POST /admin/v1/merchants/{id}/service-fees`, коды `service_fee_confirmation_required` и `service_fee_valid_from_in_past` — административный API. `GET /admin/v1/fees` остался отчётом о комиссиях сети.
## 2026-09-13, проверка проектов и настройки проекта
- Проект, который бизнес заводит сам, принимает платежи только после проверки. Пока он не одобрен или если отклонён, `POST /v1/invoices` и `POST /v1/addresses` отвечают новым кодом `403 merchant_not_approved`. Ключи API при этом работают, чтение инвойсов тоже. Все мерчанты, созданные раньше, одобрены: для существующих интеграций ничего не меняется.
- Владелец может выключить приём в активе для своего проекта. Тогда `POST /v1/invoices` в этом активе отвечает новым кодом `422 asset_disabled_by_merchant`, а `POST /v1/addresses` не включает актив в список `assets`. Уже открытые инвойсы оплачиваются и закрываются, повтор принятого запроса возвращает инвойс.
- `403 merchant_disabled` теперь означает выключение и оператором, и владельцем проекта.
- Проект без адреса вебхуков событий не получает: они не ставятся в очередь и после появления адреса не досылаются ([вебхуки](https://stons.io/ru/docs/webhooks.md#доставка)).
- Административный API: у мерчанта новые поля — `review_status`, `review_reason`, `reviewed_at`, `reviewed_by`, `site_url`, `description`, `network_fee_payer`, `service_fee_payer`, `disabled_by`. Появились фильтр `GET /admin/v1/merchants?review_status=`, маршруты `POST /admin/v1/merchants/{id}/approve` и `POST /admin/v1/merchants/{id}/reject` — административный API. **Несовместимо** для клиентов административного API: `webhook_url` мерчанта может быть `null`. Консоль обновлена в том же изменении.
- Маршруты `/cabinet/v1` для приложения кабинета: служебные, снаружи недоступны и в `GET /v1/openapi.json` не входят.
## 2026-09-13, переключатели приёма и выплат
- Оператор может временно выключить приём или выплаты в активе. Пока приём выключен, `POST /v1/invoices` в этом активе отвечает новым кодом `422 asset_payments_disabled`, а `POST /v1/addresses` не включает актив в список `assets`. Пока выключены выплаты, `POST /v1/payouts` отвечает новым кодом `422 asset_payouts_disabled`.
- Повторы запросов, принятых раньше, отвечают как прежде: инвойс по тому же `order_id` и выплата по тому же `reference` возвращаются.
- Уже открытые инвойсы оплачиваются и закрываются, уже созданные выплаты исполняются: переключатель касается только нового.
- Административный API: `PATCH /admin/v1/assets/{id}`, поля `payments_enabled` и `payouts_enabled` в объекте актива, новые коды `asset_not_found` и `asset_flag_locked` — административный API.
## 2026-09-13, административный API консоли
- Маршруты `/admin/v1` для консоли оператора: мерчанты и их ключи, инвойсы, остатки счетов реестра, движение средств, комиссии сети, каталог активов, доставки вебхуков и журнал аудита — административный API. Доступ по токену консоли `ADMIN_API_TOKEN` и заголовку оператора `X-Operator`; без токена маршрутов нет (ADR-0033).
- Адреса возврата мерчанта `return_url` и `cancel_url` видны в объекте мерчанта и меняются `PATCH /admin/v1/merchants/{id}` по правилам [страницы оплаты](https://stons.io/ru/docs/checkout.md#адреса-возврата).
- Новые коды ошибок, только у этих маршрутов: `merchant_not_found`, `api_key_not_found`, `delivery_not_found`, `delivery_not_failed`.
- Маршруты `/v1` и описание `GET /v1/openapi.json` не изменились: административные маршруты в него не входят.
## 2026-09-13, страница оплаты и адреса возврата
- У инвойса новое поле `checkout_url` — ссылка на [страницу оплаты](https://stons.io/ru/docs/checkout.md), если оператор её настроил, иначе `null`.
- Необязательные поля `return_url` и `cancel_url` в `POST /v1/invoices`: куда страница оплаты вернёт покупателя. Допускаются только в пределах origin адресов, заданных у мерчанта оператором; иначе новый код `422 redirect_url_not_allowed`. Те же поля появились в ответе инвойса.
- Повтор `POST /v1/invoices` с другими `return_url` или `cancel_url` — `409 order_id_conflict`, как повтор с другой суммой. Запросы без этих полей ведут себя как раньше.
- Возврат покупателя подписан: параметры `invoice_id`, `order_id`, `status`, `ts`, `signature`, подпись HMAC-SHA256 секретом вебхуков. Формат и код проверки — в [странице оплаты](https://stons.io/ru/docs/checkout.md#подпись). Решение — ADR-0032.
- `GET /checkout/v1/invoices/{id}` — служебный маршрут сервера страницы оплаты, без ключа API, в приватной сети. Мерчантам он не нужен.
## 2026-09-12, USDC в сети TRON
- Новый актив `USDC_TRON`: инвойсы и выплаты в USDC принимаются наравне с USDT, 6 знаков после запятой, 20 подтверждений. Контракт `TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8` — прочитан в сети, а не взят из статьи: `symbol() = USDC`, `name() = USD Coin`, `decimals() = 6`. Обоснование и граница решения — в ADR-0026.
- USDC на адресе приёма больше **не** считается неожиданным активом сам по себе. `unexpected_asset` — актив каталога, который адрес не принимает: на адресе инвойса — актив, отличный от актива инвойса, на постоянном адресе — актив, не принимаемый к оплате (TRX). Токен, которого нет в каталоге, не записывается вовсе. Первая редакция этого пункта называла `unexpected_asset` токеном вне каталога; это было неверно и исправлено 2026-09-14.
- TRX платёжным активом пока **не** стал: в режиме сжигания энергии (`SWEEP_ENERGY_MODE=burn`) комиссию за свип платит сам адрес приёма, поэтому TRX на него кладёт оператор — и с включённым флагом эти операционные деньги были бы зачислены как чей-то платёж. Сначала правило, отличающее переводы с наших адресов, и тест; потом флаг. Выплаты в TRX не появятся и после: суточные лимиты складывают суммы в одной единице, что верно только для привязанных к доллару активов.
- Активы тестовых сетей не изменились: проверяемого контракта USDC в Nile и Shasta нет.
## 2026-09-12, домен в установщике
- `POST /setup/domain` — панель находит у провайдера DNS зону, которой принадлежит указанное имя, создаёт или исправляет запись A на адрес этого сервера и возвращает то, что видно в публичном DNS прямо сейчас. Проверяется **не своя запись, а ответ сети**: записанное и видимое — разные утверждения, и работает только второе.
- Если зоны у провайдера нет, панель **создаёт её сама** и показывает NS-серверы, которые нужно прописать у регистратора домена: без делегирования зону не видит никто, какими бы правильными ни были записи в ней. Зона создаётся под регистрируемым именем — последние две метки.
- Ответ маршрута говорит, видит ли провайдер делегирование (`delegated`), создавалась ли зона (`zone_created`) и какие NS-серверы нужны (`nameservers`).
- Шаг «Домен» стоит первым и необязателен: без домена сервис доступен по адресу сервера. Переменные `SETUP_DOMAIN` и `BUNNY_API_KEY` пишет сама панель; ключ провайдера используется только для записи и проверки и обратно не показывается.
- Панель первого запуска теперь открывается в браузере по адресу сервера — порт API публикуется на время установки одной переменной и возвращается на loopback после неё (ADR-0025). Маршруты `/v1` не изменились.
## 2026-09-12, установщик записывает конфигурацию сам
- Страница первого запуска больше не печатает блок переменных для вставки руками: она **записывает** файл окружения на сервере — `POST /setup/env`. Значения проверяются теми же разборщиками, которыми сервис читает конфигурацию при старте, поэтому записанный файл — это файл, с которым сервис запускается. Маршруты `/v1` не изменились.
- `POST /setup/signer-values` и `GET /setup/signer-env` — выбор владельца для сервера подписи и **готовый файл окружения** для него вместе с командами по порядку. Пароль роли базы и два значения сида в него не попадают: первый уже есть на этом сервере, вторые создаются на том.
- Шаги «Сеть и узлы» и «Файл окружения» — новые и обязательные. Второй закрывается только когда значения не просто записаны, но и применены: переменные читаются один раз, при старте.
- Секрет, принятый страницей, обратно не отдаётся: в ответе `GET /setup/state` у него только признак «задано». Пустое поле означает «не менять».
- Поле, в которое вставили 12 или 24 коротких слова, отвергается: мнемоника не вводится на этой странице ни в одно поле. Обоснование и границы — в ADR-0024.
## 2026-09-12, страница первого запуска
- Пока настройка сервиса не завершена, API отдаёт страницу `GET /setup`, которая ведёт по шагам и проверяет каждый. Когда все обязательные шаги закрыты, её маршруты отвечают `404`. Мерчантов это не касается: маршруты `/v1` не изменились.
- Служебные маршруты страницы: `GET /setup/state`, `POST /setup/verify`, `POST /setup/steps/{key}`, `POST /setup/merchant`. Доступны только из приватной сети и только с одноразовым кодом из лога сервиса. См. первый запуск.
- Страница не принимает и не создаёт мнемонику, парольную фразу и приватную половину операторского ключа — обоснование в ADR-0024.
## 2026-09-12, отправка выплат
- События `payout.confirmed` и `payout.failed`: выплата подтверждена сетью или её транзакция попала в блок неуспешной. Поля — в [вебхуках](https://stons.io/ru/docs/webhooks.md#события). Как и раньше, тело вебхука — сигнал «сходи проверь»: состояние выплаты берите из `GET /v1/payouts/{id}`.
- Выплата в `failed` не повторяется автоматически: новая попытка требует нового `reference`.
- Остальное в этом обновлении внутреннее: подпись выплат с горячего кошелька, вторая независимая проверка вайтлиста вместе с подписью оператора, предохранитель, останавливающий выплаты сам. Публичные маршруты не изменились.
## 2026-09-12, выплаты
- `POST /v1/payouts` — создание выплаты, идемпотентное по `reference`. Ответ `202` у созданной выплаты, `200` у повтора. См. [выплаты](https://stons.io/ru/docs/payouts.md).
- `GET /v1/payouts/{id}` — текущее состояние выплаты с её статусом и причинами ожидания.
- Выплата возможна только на адрес из вайтлиста мерчанта и только после задержки перед первым использованием. Переход лимита выплату не отклоняет: она принимается со статусом `pending_review` и ждёт подтверждения человека, а `review_reasons` называет перейденные пределы. См. [лимиты](https://stons.io/ru/docs/payouts.md#лимиты).
- Новые коды ошибок: `payout_not_found`, `reference_conflict`, `asset_not_payable`, `address_not_whitelisted`, `payouts_halted`. См. [коды ошибок](https://stons.io/ru/docs/errors.md).
- Отправка выплат в сеть и события вебхуков `payout.confirmed` и `payout.failed` появятся в следующем обновлении: пока выплата доходит до статуса `queued` и ждёт исполнения.
## 2026-09-12, свипы
- Инвойс доходит до статуса `settled`: средства по нему перенесены в холодное хранилище. Для мерчанта это то же, что `confirmed`, вебхука у перехода нет — товар отдаётся на `confirmed`, как и раньше. См. [статусы](https://stons.io/ru/docs/invoices.md#статусы).
- Остальное в этом обновлении внутреннее: подпись и отправка свипов, подтверждение их в цепи и проводки в реестре. Публичные маршруты и события вебхуков не изменились.
## 2026-09-11, операционные маршруты
- `GET /admin/health` — состояние сервиса для оператора и мониторинга: отставание сканера в блоках и глубина очереди вебхуков. См. операционное состояние.
- `GET /metrics` — те же показатели в текстовом формате Prometheus.
- Оба маршрута служебные: аутентификации не требуют, данных мерчантов не содержат и доступны только в приватной сети.
## 2026-09-11, доставка вебхуков
- События из очереди уходят на URL мерчанта с подписью `X-Signature` и заголовком `X-Timestamp`, с повторами по расписанию из [описания вебхуков](https://stons.io/ru/docs/webhooks.md#доставка). До этого события только записывались.
- Заголовок `User-Agent` исходящих вебхуков содержит название продукта.
- Ответ `3xx` не считается успешной доставкой и не выполняется.
## 2026-09-11, сканер платежей
- Инвойсы меняют статус по переводам в сети: `seen`, `confirmed`, `underpaid`, `expired`, `paid_late`. См. [статусы](https://stons.io/ru/docs/invoices.md#статусы).
- В ответе `GET /v1/invoices/{id}` поле `deposits` заполняется платежами по инвойсу.
- Сервис формирует события вебхуков `invoice.seen`, `invoice.confirmed`, `invoice.overpaid`, `invoice.underpaid`, `invoice.expired`, `invoice.paid_late`, `deposit.confirmed` и `deposit.orphaned`. См. [события](https://stons.io/ru/docs/webhooks.md#события). Отправка событий на URL мерчанта включится в следующем обновлении.
## 2026-09-11, адреса и инвойсы
- Аутентификация по заголовку `X-Api-Key` и лимиты запросов по ключу и по IP-адресу — см. [аутентификацию](https://stons.io/ru/docs/authentication.md).
- `POST /v1/addresses` — постоянный адрес пользователя для мерчантов в режиме `per_user`. См. [адреса](https://stons.io/ru/docs/addresses.md).
- `POST /v1/invoices` — создание инвойса, идемпотентное по `order_id`. См. [инвойсы](https://stons.io/ru/docs/invoices.md).
- `GET /v1/invoices/{id}` — текущее состояние инвойса с его платежами.
- `GET /v1/openapi.json` — машиночитаемое описание API в формате OpenAPI 3.1.
- Новые коды ошибок: `validation_failed`, `unauthorized`, `merchant_disabled`, `invoice_not_found`, `address_mode_mismatch`, `order_id_conflict`, `asset_not_supported`, `amount_below_minimum`, `rate_limited`, `addresses_exhausted`. См. [коды ошибок](https://stons.io/ru/docs/errors.md).
## 2026-09-11, служебные маршруты
- Добавлены служебные маршруты `GET /healthz` и `GET /readyz` — см. мониторинг.
- Введён единый формат ошибок `{ "error": { "code", "message" } }` и коды `bad_request`, `route_not_found`, `internal_error`, `service_unavailable` — см. [коды ошибок](https://stons.io/ru/docs/errors.md).
- Каждый ответ содержит заголовок `X-Request-Id` для поиска запроса в логах.