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

Выплаты

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

Откуда платятся выплаты

Только с отдельного горячего кошелька. Не с адресов приёма и не из холодного хранилища. Баланс горячего кошелька ограничен сверху и пополняется вручную из холодного хранилища: это потолок потерь, если сервер подписи будет скомпрометирован.

Создать выплату

POST /v1/payouts

Запрос

Поле Тип Обязательно Описание
reference string да Идентификатор выплаты на стороне мерчанта, уникальный в пределах мерчанта: 1–200 видимых ASCII-символов без пробелов. Ключ идемпотентности
asset string да Актив, например USDT_TRON. Сервис платит не во всех активах, в которых принимает: см. столбец «Выплаты» в активах
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.

Статусы

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, см. события. Событие пишется в той же транзакции базы, что и смена статуса, и приходит одно на выплату, какой бы сервис её ни перевёл. Сеть сервис оплачивает с горячего кошелька, и из суммы выплаты ничего не вычитается: получатель получает ровно amount (ADR-0023). С баланса проекта при подтверждении списывается комиссия за выплату.

Комиссия за выплату

За каждую подтверждённую выплату STONS списывает с баланса проекта комиссию — процент от суммы выплаты и фиксированную сумму, известные заранее и свои у каждого актива, как принято на рынке. Сколько на самом деле сожгла сеть, на комиссию не влияет: разницу несёт платформа. Поля комиссии по-прежнему лежат в fees.network: это имя осталось с тех пор, когда она была фиксированной комиссией сети (журнал изменений).

  • Комиссия = фиксированная сумма + ⌊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 Выплаты остановлены

Пример

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"}'
{
  "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:

{
  "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"
}