# Выплаты

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

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

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

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

`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"`. Её получает получатель целиком |
| `fees` | object | Комиссия сети: `fees.network.payer`, `fees.network.amount`, `fees.network.charged` — см. [комиссию сети](#комиссия-сети) |
| `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`.

## Статусы

```text
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` — см. [события](https://stons.io/docs/webhooks.md#события). Сеть сервис оплачивает с горячего кошелька, и из суммы выплаты ничего не вычитается: получатель получает ровно `amount` (ADR-0023). С баланса проекта при подтверждении списывается [комиссия сети](#комиссия-сети).

## Комиссия сети

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

- **Сумма записывается в выплату при её создании** — `fees.network.amount`. Смена тарифа не меняет уже созданные выплаты.
- **Списывается при подтверждении** выплаты, в той же операции, что сама выплата. До подтверждения `fees.network.charged` равен нулю.
- **Выплата, провалившаяся в цепи** (`failed`), комиссию не несёт: получатель ничего не получил. Отклонённая выплата — тоже.
- **Платит всегда проект.** Получатель получает ровно `amount`: вариант «комиссию платит получатель» не поддерживается.
- **Сумму назначает оператор**. Пока он её не задал, комиссия равна нулю.
- **Баланс при создании выплаты не проверяется**, как и раньше: выплату ограничивают вайтлист и лимиты. Комиссия, как и сама выплата, списывается при подтверждении и может увести баланс проекта ниже нуля. Лимиты сравнивают только `amount`: комиссия получателю не уходит.

| Поле | Описание |
| --- | --- |
| `fees.network.payer` | Всегда `merchant` |
| `fees.network.amount` | Комиссия сети, записанная в выплату при создании, со всеми знаками актива |
| `fees.network.charged` | Сколько списано с баланса проекта за эту выплату: `amount` после подтверждения, иначе ноль |

Пример: выплата 100.5 USDT при комиссии 1.70 USDT. Получатель получает 100.5 USDT, баланс проекта после подтверждения уменьшается на 102.2 USDT.

## Куда можно платить

Только на адреса из вайтлиста мерчанта. Вайтлистом управляет оператор командами, см. команды оператора.

- Адрес, добавленный в вайтлист, можно использовать только после задержки: `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). Процедура целиком, с параметрами команд, — в командах оператора. Коротко:

1. Оператор смотрит выплаты в `pending_review`: `payout:list`. Вывод показывает адрес целиком, сумму в минимальных единицах актива и действующие лимиты.
2. Он сверяет адрес получателя символ за символом с тем, что ожидает мерчант, и проверяет причину ожидания.
3. `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` | Выплаты остановлены |

## Пример

```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",
  "to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "amount": "100.500000",
  "fees": { "network": { "payer": "merchant", "amount": "1.700000", "charged": "0.000000" } },
  "review_reasons": [],
  "tx_hash": 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",
  "to_address": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
  "amount": "900.000000",
  "fees": { "network": { "payer": "merchant", "amount": "1.700000", "charged": "0.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"
}
```
