# Адреса для приёма платежей

Этот документ описывает адреса для приёма платежей: два режима адресов, как `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).
