Выплаты
Выплата — перевод средств мерчанта на адрес, который заранее внесён в его вайтлист. Приём ошибается в пользу сервиса, вывод — в чужую, поэтому контроль на выводе строже: адрес только из вайтлиста и только после задержки, три лимита, порог автоподтверждения и ручное подтверждение другим человеком.
Откуда платятся выплаты
Только с отдельного горячего кошелька. Не с адресов приёма и не из холодного хранилища. Баланс горячего кошелька ограничен сверху и пополняется вручную из холодного хранилища: это потолок потерь, если сервер подписи будет скомпрометирован.
Создать выплату
POST /v1/payouts
Запрос
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
reference |
string | да | Идентификатор выплаты на стороне мерчанта, уникальный в пределах мерчанта: 1–200 видимых ASCII-символов без пробелов. Ключ идемпотентности |
asset |
string | да | Актив, например USDT_TRON. Сервис платит не во всех активах, в которых принимает |
to_address |
string | да | Адрес получателя целиком, 20–100 букв и цифр. Должен быть в вайтлисте мерчанта по этому активу |
amount |
string | да | Сумма в единицах актива: "100.5". Не больше знаков после запятой, чем у актива, без знака, экспоненты и пробелов. Ноль не принимается |
Ответ
202 — выплата создана, 200 — выплата с этим reference уже была создана раньше. Тело в обоих случаях одинаковое:
| Поле | Тип | Описание |
|---|---|---|
payout_id |
string (UUID) | Идентификатор выплаты |
reference |
string | Идентификатор выплаты на стороне мерчанта |
status |
string | Статус, см. ниже |
asset |
string | Актив |
network |
string | Сеть актива: tron |
to_address |
string | Адрес получателя целиком |
amount |
string | Сумма со всеми знаками актива: "100.500000" |
review_reasons |
array | Какие правила выплата перешла и потому ждёт человека. Пусто у выплаты, прошедшей по правилам |
tx_hash |
string или 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.
Статусы
pending_review → approved → queued → signing → broadcast → confirmed
↓ ↓
rejected failed
| Статус | Смысл |
|---|---|
pending_review |
Выплата перешла лимит или порог и ждёт подтверждения человека |
approved |
Человек подтвердил выплату. Ставится только ручным подтверждением: выплата в пределах правил через этот статус не проходит |
queued |
Выплата принята к исполнению и ждёт подписи |
signing |
Сервер подписи взял выплату в работу |
broadcast |
Транзакция отправлена в сеть и ждёт подтверждений |
confirmed |
Транзакция подтверждена сетью. Окончательный статус |
rejected |
Человек отклонил выплату. Окончательный статус: повтор требует нового reference |
failed |
Отправка не удалась. Окончательный статус: автоматических повторов нет |
Выплата в failed не повторяется автоматически — сначала проверяется, что транзакция действительно не попала в сеть, иначе повтор означал бы двойную выплату.
Переход в confirmed и в failed сопровождается вебхуком payout.confirmed или payout.failed — см. события. Комиссию сети платит горячий кошелёк сервиса, и из суммы выплаты она не вычитается: получатель получает ровно amount (ADR-0023).
Куда можно платить
Только на адреса из вайтлиста мерчанта. Вайтлистом управляет оператор командами, см. команды оператора.
- Адрес, добавленный в вайтлист, можно использовать только после задержки:
WHITELIST_COOLDOWN_HOURS, по умолчанию 24 часа. Момент «использовать с» считается при добавлении и хранится в строке, поэтому смена настройки не ускоряет уже добавленные адреса. До этого момента выплата отклоняется кодомaddress_not_whitelisted, и сообщение называет момент, с которого адрес станет годен. - Добавление подписывается операторским ключом офлайн. Подпись покрывает мерчанта, актив, адрес целиком и момент добавления, поэтому её нельзя переставить на другой адрес или другого мерчанта. Строку без верной подписи не примет ни команда, ни сервер подписи — обоснование в ADR-0021.
- Добавление адреса, у которого первые и последние символы совпадают с уже внесённым, блокируется до явного подтверждения с показом обоих адресов целиком. Так отсекается подмена адреса похожим (address poisoning).
- Адрес для выплаты никогда не берётся из истории транзакций.
- Адрес сверяется с вайтлистом дважды: при создании выплаты в API и ещё раз, независимо, сервером подписи перед подписью — и Signer проверяет не только наличие строки, но и подпись оператора под ней. Запись вайтлиста, разрешившая выплату, хранится на самой выплате, поэтому проверять нечего заново выводить.
- Отзыв адреса подписи не требует: он только убирает разрешение. Отозванный адрес можно добавить заново, и задержка начнётся сначала.
Лимиты
Действуют четыре значения. Все они заданы в одной учётной единице — в долларах, — потому что суточный лимит сервиса складывает выплаты по всем активам, а складывать минимальные единицы активов с разными decimals нельзя. Отсюда правило: сервис платит только в активах, привязанных к доллару (ADR-0022).
| Переменная | Что ограничивает |
|---|---|
PAYOUT_MAX_AMOUNT |
Одну выплату |
PAYOUT_AUTO_APPROVE_LIMIT |
Порог, ниже которого подтверждение человека не нужно |
PAYOUT_DAILY_LIMIT_MERCHANT |
Сумму выплат одного мерчанта за сутки |
PAYOUT_DAILY_LIMIT_TOTAL |
Сумму выплат всего сервиса за сутки |
Суточные лимиты считаются по скользящему окну в 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 |
Вместе с этой выплатой сервис превысит суточный лимит целиком |
Ручное подтверждение
Подтверждает и отклоняет выплату человек, командой на сервере — маршрута для этого нет, пока нет административных ролей и второго фактора (ADR-0019). Процедура целиком, с параметрами команд, — в командах оператора. Коротко:
- Оператор смотрит выплаты в
pending_review:payout:list. Вывод показывает адрес целиком, сумму в минимальных единицах актива и действующие лимиты. - Он сверяет адрес получателя символ за символом с тем, что ожидает мерчант, и проверяет причину ожидания.
payout:approve --actor <кто> --id <выплата>переводит выплату вapproved.payout:reject --actor <кто> --id <выплата> --note <почему>— вrejected, окончательно.
Разделение полномочий. Тот, кто добавил адрес в вайтлист, не может подтвердить выплату на этот адрес: команда откажет и назовёт, кто добавлял. Одна учётная запись не должна уметь и выбрать адрес, и отпустить на него деньги. Отклонение такого ограничения не имеет: остановить выплату может кто угодно.
Каждое подтверждение, отклонение и добавление в вайтлист попадает в неизменяемый журнал аудита вместе с адресом целиком.
Идемпотентность
Выплата создаётся с идентификатором мерчанта reference, уникальным в пределах мерчанта. Повторный запрос с тем же reference, активом, адресом и суммой не создаёт вторую выплату и не возвращает ошибку: ответом будет уже созданная выплата с кодом 200. Поэтому запрос можно безопасно повторять после таймаута, и повтор работает даже когда выплаты остановлены. Одновременные запросы с одним reference тоже создают одну выплату.
Запрос с тем же reference, но другой суммой, активом или адресом отклоняется кодом 409 reference_conflict: это другая выплата, а не повтор.
Остановка выплат
Выплаты останавливаются автоматически при расхождении реестра с сетью и аварийной командой оператора. Пока остановка действует, новые выплаты не принимаются — код 503 payouts_halted; повтор с тем же reference после снятия остановки создаст выплату. Причина остановки наружу не раскрывается: она описывает внутренний инцидент. Снять остановку можно только вручную, с записью в журнал аудита — см. остановку выплат.
Ошибки
| HTTP | error.code |
Когда |
|---|---|---|
| 400 | validation_failed |
Поле отсутствует, лишнее или неверного формата; у суммы слишком много знаков после запятой; сумма не больше нуля |
| 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",
"to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"amount": "100.500000",
"review_reasons": [],
"tx_hash": 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",
"to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
"amount": "900.000000",
"review_reasons": ["above_auto_approve_limit"],
"tx_hash": null,
"created_at": "2026-09-12T12:05:00.000Z",
"updated_at": "2026-09-12T12:05:00.000Z"
}