# Документация API AETERNA

Этот документ является зеркалом встроенной страницы документации API (`AeternaWebApp/Components/Pages/Docs.razor`). Он должен поддерживаться в синхронном состоянии с этой страницей и с английской версией (`API_Documentation_EN.md`).

## Оглавление

### Часть 1. Интеграция для мерчантов

1. [Введение и аутентификация](#введение-и-аутентификация)
2. [PayIn-ордеры](#payin-ордеры)
3. [PayOut-ордеры](#payout-ордеры)
4. [Трансграничный PayIn (третья страна)](#трансграничный-payin-третья-страна)
5. [Статусы ордеров](#статусы-ордеров)
6. [Криптовыводы](#криптовыводы)
7. [Получение баланса](#получение-баланса)
8. [Чат и диспуты](#чат-и-диспуты)
9. [Справочник: методы оплаты](#справочник-методы-оплаты)
10. [Справочник: курсы обмена](#справочник-курсы-обмена)
11. [Колбэки](#колбэки)

### Часть 2. Payment Orchestrator (PayIn)

1. [Payment Orchestrator — введение / RSA](#payment-orchestrator--введение--rsa)
2. [Интеграция с Orchestrator](#интеграция-с-orchestrator)
3. [CreateOrder (PayIn)](#createorder-payin)
4. [ApprovePayInOrder](#approvepayinorder)
5. [GetOrderStatus](#getorderstatus)
6. [CancelOrder (PayIn)](#cancelorder-payin)
7. [CreateDispute](#createdispute)
8. [Вебхуки и ошибки (PayIn)](#вебхуки-и-ошибки-payin)

### Часть 3. Payment Orchestrator (PayOut)

1. [PayOut-аукцион](#payout-аукцион)
2. [CreatePayoutOrder](#createpayoutorder)
3. [CancelPayoutOrder](#cancelpayoutorder)
4. [ApprovePayoutOrder](#approvepayoutorder)
5. [PayOut-колбэки](#payout-колбэки)

---

# Часть 1. Интеграция для мерчантов

## Введение и аутентификация

Добро пожаловать в документацию API для интеграции мерчантов. Наш API позволяет автоматизировать создание заявок на ввод (PayIn) и вывод (PayOut) средств, получать статистику, а также управлять балансом и диспутами.

### Аутентификация и общие требования

Все эндпоинты API доступны по базовому URL нашего сервиса (например, `https://{host}/`). Каждый POST-запрос к API (кроме публичных методов) должен соответствовать следующим требованиям:

- **Метод:** `POST`
- **Content-Type:** `application/json`
- **Accept:** `application/json, text/plain`

Дополнительно каждый запрос должен содержать следующие обязательные заголовки аутентификации:

- `X-App-Token` — публичный токен мерчанта (PublicToken).
- `X-App-Access-Ts` — текущий Unix Timestamp (в секундах). Время жизни таймстампа ограничено.
- `X-App-Access-Sig` — цифровая подпись запроса в формате HMAC-SHA256 (результат в нижнем регистре).

### Создание подписи (X-App-Access-Sig)

Для создания цифровой подписи используйте свой секретный ключ (`SecretToken`) и email, привязанный к учётной записи мерчанта. *Примечание: строки таймстампа и email должны конкатенироваться напрямую, без пробелов и разделителей.*

```plaintext
signStr = "{X-App-Access-Ts}{Email}"
X-App-Access-Sig = hex(HMAC_SHA256(signStr, SecretToken))
```

**Пример на C#:**

```csharp
using System.Security.Cryptography;
using System.Text;

string time = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
string signStr = $"{time}{email}";

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretToken));
byte[] hashValue = hmac.ComputeHash(Encoding.UTF8.GetBytes(signStr));
string signature = BitConverter.ToString(hashValue).Replace("-", "").ToLower();
```

**Пример на Python:**

```python
import hmac
import hashlib
import time

unix_time = str(int(time.time()))
sign_str = f"{unix_time}{email}"

signature = hmac.new(
    secret_token.encode('utf-8'),
    sign_str.encode('utf-8'),
    hashlib.sha256
).hexdigest()
```

**Пример на Java:**

```java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;

String time = String.valueOf(Instant.now().getEpochSecond());
String signStr = time + email;

Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(secretToken.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] hashValue = sha256_HMAC.doFinal(signStr.getBytes("UTF-8"));

StringBuilder signature = new StringBuilder();
for (byte b : hashValue) {
    signature.append(String.format("%02x", b));
}
String result = signature.toString();
```

### Белый список IP

Если у учётной записи включён фильтр IP внешнего API, запрос с адреса вне списка отклоняется до выполнения операции. HTTP-статус — `403`. Тело — обычный формат ошибки, текст ровно `IP Not Allowed`. Адрес, разрешённый для входа в кабинет, API не открывает, и наоборот. Публичные справочники `GET /api/GetRates` и `GET /api/PaymentMethods` мерчанта не узнают и этим фильтром не закрываются. Список адресов вы ведёте и фильтр включаете и выключаете сами — на странице «Доступ по IP» кабинета мерчанта. Включить фильтр можно, только когда в списке есть хотя бы один адрес. Действия в кабинете — не вызовы ваших систем, этот список их не ограничивает.

### Аутентификация вебхуков

Чтобы вы могли проверить подлинность входящих колбэков на ваш сервер, мы отправляем подпись в заголовке `X-App-Access-Sig`.

> **Важно — другой токен!** Подписи колбэков вычисляются с использованием **отдельного** токена — `CallBackToken` — а не `SecretToken`, которым вы подписываете свои исходящие запросы к нашему API. Это два независимых поля профиля мерчанта, и их значения обычно различаются. Попытка проверить колбэк с помощью `SecretToken` всегда завершится неудачей.

**Алгоритм проверки:**

```plaintext
signStr = raw UTF-8 bytes of the HTTP request body (compact JSON, no whitespace)
X-App-Access-Sig = hex(HMAC_SHA256(signStr, CallBackToken))   // lowercase hex
```

*Предупреждение:* вычисляйте HMAC по **сырым байтам тела запроса** в том виде, в котором они получены, **до** какой-либо десериализации JSON. Повторная сериализация разобранного объекта изменит пробелы, порядок ключей или регистр экранирования, и подпись разойдётся.

**Нет заголовка с подписью?** Если в профиле мерчанта поле `CallBackToken` пустое, система отправляет колбэк **без** заголовка `X-App-Access-Sig` вообще (заголовок опускается, а не остаётся пустым). Либо принимайте неподписанные колбэки (если это согласовано с нами), либо отклоняйте их; во втором случае запросите установку `CallBackToken` для вашей учётной записи.

**Сравнение:** используйте сравнение с постоянным временем (например, `crypto.timingSafeEqual` в Node.js, `hmac.compare_digest` в Python, `CryptographicOperations.FixedTimeEquals` в .NET) и сравнивайте в нижнем регистре.

---

## PayIn-ордеры

Эндпоинты для обработки входящих платежей (депозитов) от пользователей. Система проверяет лимиты и наличие доступных трейдеров и реквизитов. При успехе назначается трейдер, и ордер ожидает оплаты пользователем.

**Жизненный цикл:** после того как пользователь совершил оплату, трейдер проверяет поступление средств и завершает ордер вручную (либо система делает это автоматически в большинстве случаев). Если ордер не завершён в течение времени жизни, он отменяется по таймауту. В случае задержек вы можете связаться с трейдером через эндпоинты чата или открыть диспут.

### POST /api/v3/PayInOrder

Создаёт новый PayIn-ордер. Система проверяет лимиты, предоставляет подходящие платёжные реквизиты (карта, номер телефона и т. д.) и возвращает ссылку на оплату или реквизиты для клиента.

**Опциональный заголовок:** `X-Request-Timeout-Ms` (int) — максимальное время в миллисекундах для ожидания создания ордера (по умолчанию: 5000).

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `amount` (Req.) | Number | Сумма депозита в фиатной валюте. |
| `currency` (Req.) | String | Трёхбуквенный код валюты (например, "KZT", "RUB", "AZN"). |
| `payment_method` (Req.) | String | Строковый код метода оплаты (например, "card_number"). |
| `bank` (Opt.) | String | Необязательный фильтр «предпочитаемый банк» для подбора реквизитов. Имя должно совпадать с банком в реестре **для запрошенной валюты**. Неизвестное имя → отказ создания с `Bank not found`. Если поле не передать — система сама выбирает доступный реквизит и возвращает **его** банк в `requisites`. Это не банк из ответа — см. [PayIn requisites.bank_name](#payin-requisitesbank_name--банк-в-ответе-создания-депозита). |
| `payer_id` (Opt.) | String | Уникальный идентификатор плательщика в вашей системе. |
| `payer_name` (Opt.) | String | Имя плательщика в произвольной форме (обязательно в некоторых гео). |
| `payer_ip` (Opt.) | String | IP-адрес плательщика (обязательно в некоторых гео). |
| `incoming_id` (Opt.) | String | Ваш внутренний идентификатор транзакции для предотвращения повторных списаний. |
| `ttl` (Opt.) | Integer | Время жизни ордера в минутах (переопределяет настройки мерчанта). |
| `back_link` (Opt.) | String | URL для редиректа плательщика после завершения платежа (успех / отказ). |
| `callback_url` (Opt.) | String | URL для получения вебхуков об изменении статуса именно этого ордера. |
| `comment` (Opt.) | String | Необязательная свободная заметка, которая сохраняется на ордере. Это **не** выданные платёжные реквизиты. Реквизиты, по которым плательщик должен платить, приходят в `requisites`. |
| `iterative_sum_search_enabled` (Opt.) | Boolean | Включает поиск уникальной суммы путём небольшого изменения для избежания дублирующих активных ордеров. |
| `extra` (Opt.) | Object | Дополнительные данные. Может содержать `card_details` и `client_details`, если данные карты вы собираете у себя, либо `customer_details`, если данные плательщика у вас уже лежат в этом блоке (см. ниже). |

#### Данные карты, собранные на вашей стороне

Если вы собираете данные карты сами, передайте их в `extra.card_details` вместе с `extra.client_details` и используйте `payment_method = "c2becom"`. Списание происходит внутри запроса, поэтому плательщик не уходит на платёжную страницу. Оба блока необязательные: без `card_details` ордер обрабатывается как обычно и вы получаете `payment_link`. Сбор данных карты на своей стороне требует соответствия PCI DSS.

| `card_details` | Тип | Описание |
|----------------|-----|----------|
| `card_number` (Req.) | String | Номер карты, 16–19 цифр. Пробелы и дефисы игнорируются. |
| `cardholder` (Opt.) | String | Имя держателя карты, как напечатано на карте. |
| `expmo` (Req.) | String | Месяц окончания срока действия, 2 цифры (например, "01"). |
| `expyr` (Req.) | String | Год окончания срока действия, 4 цифры (например, "2030"). |
| `cvv` (Req.) | String | Код проверки подлинности карты (CVV2 / CVC2), 3–4 цифры. |

Передавайте блок целиком: если номер карты, срок действия или код проверки не заполнены, ордер отклоняется — на платёжную страницу он молча не переключается.

| `client_details` | Тип | Описание |
|------------------|-----|----------|
| `ip` (Req.) | String | IP-адрес плательщика (v4 или v6). Обязателен, когда переданы данные карты. Если не указан, берётся `payer_ip` из корня запроса. |
| `ip_country` (Opt.) | String | Страна IP-адреса, ISO 3166-1 alpha-2. Часть провайдеров требует страну при списании с карты и без неё отклоняет платёж, поэтому передавайте поле всегда, когда значение известно. Если не указано, используется `country`. |
| `email`, `phone` (Opt.) | String | Контакты плательщика. При отсутствии берутся `extra.user_email` и `extra.user_phone`. |
| `first_name`, `last_name` (Opt.) | String | Имя и фамилия плательщика. При отсутствии берутся из `extra.user_name`. |
| `country` (Opt.) | String | Страна плательщика, ISO 3166-1 alpha-2. |
| `date_of_birth` (Opt.) | String | Дата рождения плательщика, `ГГГГ-ММ-ДД`. Принимаются также `ДД.ММ.ГГГГ` и `ДД/ММ/ГГГГ`; дату, которую не удалось прочитать, мы дальше не передаём. |
| `billing_address` (Opt.) | Object | Платёжный адрес держателя карты. |
| `browser` (Req.) | Object | Параметры браузера плательщика, используемые 3D Secure. На практике обязателен: без него провайдер отклоняет списание (см. ниже). |

| `client_details.billing_address` | Тип | Описание |
|----------------------------------|-----|----------|
| `country` | String | Страна платёжного адреса, ISO 3166-1 alpha-2. Ключевое поле блока: часть провайдеров отклоняет списание, если в запросе нет страны платёжного адреса. Если поле не указано, используется `country`, затем `ip_country` плательщика. |
| `address_line1` | String | Улица и номер дома. |
| `address_line2` | String | Квартира, офис или дополнительная строка адреса. |
| `city` | String | Город. |
| `postal_code` | String | Почтовый индекс. |

Передавайте блок по частям: поля, которые вы не собираете, можно опустить. Без страны блок не передаётся провайдеру вовсе — остальные поля без неё бесполезны.

| `client_details.browser` | Тип | Источник значения на вашей странице |
|--------------------------|-----|--------------------------------------|
| `user_agent` | String | `navigator.userAgent` |
| `language` | String | `navigator.language` |
| `timezone_offset` | Integer | `new Date().getTimezoneOffset()`, минуты |
| `window_width`, `window_height` | Integer | `document.body.clientWidth` / `clientHeight` |
| `screen_width`, `screen_height` | Integer | `screen.width` / `screen.height` |
| `screen_color_depth` | Integer | `screen.colorDepth` |
| `java_enabled` | Boolean | `navigator.javaEnabled()` |

**Передавайте блок целиком.** Формально эти значения необязательны, но списание без них отклоняется: провайдеру не с чем проводить 3-D Secure, и он отвечает общей ошибкой, похожей на некорректный запрос. Частично заполненный блок тоже не спасает — мы проверили, что запрос с одним `user_agent` и запрос без `window_width` / `window_height` отклоняются оба, а тот же запрос со всеми значениями из таблицы принимается. Пропустить можно только `java_enabled`. Значения должны сниматься с реального браузера плательщика, а не быть захардкожены.

После того как списание принято, в ответе приходит `requisites.card_input_link` — покажите эту ссылку плательщику (редиректом или в iframe); финальный статус ордера придёт колбэком.

```json
{
  "amount": 5000,
  "currency": "KZT",
  "payment_method": "c2becom",
  "payer_id": "client-42",
  "payer_ip": "185.192.23.117",
  "incoming_id": "merchant-order-123",
  "callback_url": "https://merchant.example/api/callback",
  "extra": {
    "card_details": {
      "card_number": "4111111111111111",
      "cardholder": "IVAN IVANOV",
      "expmo": "01",
      "expyr": "2030",
      "cvv": "123"
    },
    "client_details": {
      "ip": "185.192.23.117",
      "ip_country": "KZ",
      "email": "payer@example.com",
      "phone": "+77001234567",
      "first_name": "Ivan",
      "last_name": "Ivanov",
      "country": "KZ",
      "date_of_birth": "1990-02-01",
      "billing_address": {
        "country": "KZ",
        "address_line1": "Abay Avenue 10",
        "address_line2": "Apt 5",
        "city": "Almaty",
        "postal_code": "050000"
      },
      "browser": {
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        "language": "en-GB",
        "timezone_offset": -120,
        "window_width": 1642,
        "window_height": 883,
        "screen_width": 1710,
        "screen_height": 1107,
        "screen_color_depth": 30,
        "java_enabled": false
      }
    }
  }
}
```

Данные карты не сохраняются: номер карты в журнале запросов маскируется до первых шести и последних четырёх цифр, код проверки подлинности не сохраняется вовсе.

Если в вашей интеграции данные плательщика уже лежат в `extra.customer_details`, переносить их не нужно: этот блок мы тоже читаем и используем там, где в `client_details` осталось пусто. Поле, заполненное в `client_details`, всегда важнее. То же внутри `billing_address`: если объект есть, а `address_line1` или `postal_code` пустые, их заполняют `card_street` / `card_post_code`.

| `customer_details` | Что заполняет |
|--------------------|---------------|
| `date_of_birth` | `client_details.date_of_birth` |
| `country_of_residence` | `client_details.country` |
| `card_country_code` | `billing_address.country` |
| `card_street` | `billing_address.address_line1` |
| `card_city` | `billing_address.city` |
| `card_post_code` | `billing_address.postal_code` |
| `email`, `phone`, `first_name`, `last_name` | Контакты и имя плательщика |

Отпечатка браузера в этом блоке нет, поэтому `client_details.browser` всё равно нужно передавать — это единственная часть данных плательщика, у которой нет второго источника.

#### Особенности карточного канала

Карточный канал работает иначе, чем схема с трейдером: решение о списании принимает провайдер, и его вердикт зависит от того, насколько полны данные плательщика. Разберите эти правила до того, как включите канал в продакшене.

- **Отсутствие блока `browser` — самостоятельная причина отказа.** Из всего, что несёт блок плательщика, этот решает исход: с отпечатком списание уходит на 3-D Secure, без него провайдер отвечает общей ошибкой. Мы установили это, меняя по одному условию за раз — сумма, страна плательщика, IP, срок действия карты, имя держателя, платёжный адрес и дата рождения исход не меняли, перевернул его только отпечаток. Если списания по картам перестали проходить, проверяйте этот блок первым.
- **Остальной блок плательщика тоже передавайте целиком.** Помимо `card_details` провайдер проверяет, кто платит: `date_of_birth`, весь `billing_address`, `ip` вместе с `ip_country`, `email`, `phone`, `first_name`, `last_name`. Если чего-то не хватает, отказ приходит общим кодом без читаемой причины, и повтор того же запроса не поможет — неполны данные, а не неверен формат.
- **Следите за согласованностью стран.** Страна IP-адреса, страна платёжного адреса и страна выпуска карты сверяются между собой, и расхождение само по себе становится причиной отказа, даже когда все поля заполнены.
- **Страна плательщика должна входить в канал.** Набор допустимых стран зависит от валюты расчёта: канал с расчётами в EUR или USD обычно настроен на карты, выпущенные в Европе. Для страны вне канала провайдер отвечает, что терминал не найден, — это маршрутизация на его стороне, и правка тела запроса ничего не изменит. Уточняйте у своего менеджера, какие страны и платёжные системы карт покрывает ваш канал.
- **Тестовые карты работают только на тестовом контуре.** На боевых ключах тестовая карта отклоняется как ошибка валидации запроса; формулировка обманчива — запрос корректен, не принимается сама карта.
- **Отправляйте плательщика по `card_input_link` сразу, как только он пришёл.** Если карта требует 3-D Secure, провайдер держит платёж до этого перехода и отменяет его по таймауту, когда по ссылке никто не пошёл. Заявка закроется отменой, хотя запрос был принят, поэтому переход по ссылке — обязательный шаг, а не подстраховка.
- **Списание может быть принято асинхронно.** Провайдер вправе взять платёж в обработку и сообщить исход позже, поэтому отсутствие мгновенного успеха не означает отказ. Считайте финальным статусом тот, что придёт колбэком.
- **Телефон передавайте в международном формате**, с кодом страны. Мы приводим его к формату провайдера, но номер без кода страны может быть отклонён.

#### Проверка карточного канала

Списания по картам проверяются только на тестовом контуре. Его адрес и отдельный токен приложения запросите у своего менеджера: боевые ключи там не работают, а тестовый номер карты, отправленный в продакшен, отклоняется как ошибка валидации запроса.

Собирайте тестовый запрос так же, как боевой: тот же полный блок плательщика, страна плательщика из числа поддерживаемых каналом, небольшая сумма. Срок действия подойдёт любой будущий, код проверки подлинности — любой из трёх цифр. Ниже — опубликованные провайдером тестовые карты; в колонке результата указано, чем они заканчиваются на канале в действительности.

| Номер карты | Что происходит |
|-------------|----------------|
| `4000 0000 0000 0408` | Списание проходит. Этим номером проверяйте весь путь вплоть до колбэка. |
| `4000 0000 0000 0002` | Списание отклоняет эквайер, даже после успешной аутентификации. Подходит, чтобы проверить обработку отказа на вашей стороне. |
| `4242 4242 4242 4242` | Опубликована как неуспешная аутентификация. |
| `4000 0000 0000 0416` | Опубликована как отказ в авторизации. |

Успешный тестовый прогон выглядит так:

1. Создайте заявку с `card_details` и полным блоком `client_details`, включая `browser`.
2. Возьмите из ответа `requisites.card_input_link` и откройте его так, как это сделал бы плательщик. Ссылка приходит по всем тестовым картам, включая те, что опубликованы как не требующие 3-D Secure.
3. Откроется тестовая страница аутентификации банка. Выберите успешный результат и подтвердите.
4. За пару минут придёт колбэк с финальным статусом, заявка закроется успехом.

Если по ссылке никто не перешёл, платёж отменяется по таймауту примерно через двадцать минут и заявка закрывается отменой — ровно так же, как в продакшене. Отказ сразу после создания заявки, без всякого перехода, означает либо что блок `browser` отсутствует или заполнен частично, либо что не принят сам номер карты.

#### Выход в продакшен на карточном канале

1. Уточните у своего менеджера, какие валюты, страны плательщиков и платёжные системы карт покрывает ваш канал, и получите для него боевые ключи.
2. Переходите на реальные карты: тестовые номера на боевых ключах отклоняются, причём в тексте отказа упоминается валидация запроса, хотя сам запрос корректен.
3. Продолжайте передавать блок плательщика целиком, вместе с отпечатком браузера, — те же поля, что и на тестовом контуре.
4. Показывайте `card_input_link` плательщику сразу. Заявка, оставленная без этого перехода, закрывается отменой по таймауту, а не отказом.
5. Финальным статусом считайте колбэк, а перед запуском трафика проведите одно небольшое реальное списание.

#### Пример успешного ответа

```json
{
  "result": true,
  "order_id": 123456,
  "initial_amount": 5000,
  "usdt_amount": 52.3,
  "rate": 95.6,
  "amount": 5000,
  "life_time": "2026-04-09T12:00:00Z",
  "requisites": {
    "bank_id": 12,
    "bank_name": "Sberbank",
    "name": "Ivan Ivanov",
    "payment_link": "https://payment.example.com/pay/...",
    "card_input_link": "https://payment.example.com/card-input/...",
    "card": "4276********1234",
    "holder": "IVAN IVANOV",
    "phone_number": null,
    "iban": null,
    "account_number": null,
    "transfer_info": "V2345445",
    "data_for_qr": null
  }
}
```

Карточный пример выше — для перевода на карту. Не используйте его как форму ответа для `account_number` (перевод на счёт) — см. следующий подраздел.

#### PayIn `requisites.bank_name` — банк в ответе создания депозита

Поисковые слова: `bank`, `requisites.bank_name`, `requisites.bank_id`, банк в ответе PayIn.

На PayIn есть **два разных** `bank`. Не объясняйте одно другим.

- **Поле запроса `bank`** — необязательный фильтр «предпочитаемый банк» для *подбора* реквизитов. Имя должно быть в реестре банков *для этой валюты*. Несовпадение → HTTP 400 и `Bank not found`. Если поле не передали — система выбирает доступный реквизит и возвращает *его* банк в `requisites`.
- **Поля ответа `requisites.bank_name` и `requisites.bank_id`** — банк **выданных** реквизитов. Он уже привязан к выбранному реквизиту. По номеру счёта или карты **не** определяется.

Когда `payment_method` равен `account_number`, в успешном блоке `requisites` заполнены `account_number` и `bank_name`; карточные поля — `null` (не наоборот):

```json
{
  "result": true,
  "order_id": 123456,
  "initial_amount": 150000,
  "usdt_amount": 110.29,
  "rate": 1360.0,
  "amount": 150000,
  "life_time": "2026-04-09T12:00:00Z",
  "requisites": {
    "bank_id": 44,
    "bank_name": "IBK",
    "name": "HONG GILDONG",
    "payment_link": null,
    "card_input_link": null,
    "card": null,
    "holder": null,
    "phone_number": null,
    "iban": null,
    "account_number": "012345678901",
    "transfer_info": null,
    "data_for_qr": null
  }
}
```

#### Типичные ошибки

- **The order does not meet the limits** — сумма вне допустимых границ.
- **The payer is blocked** — `payer_id` или `payer_name` совпадает со значением из списка заблокированных плательщиков: общего списка платформы или записей вашего аккаунта, например после диспута с решением «Фейковый чек». Чтобы снять блокировку, обратитесь к менеджеру.
- **Bank not found** / **Currency not found** — `bank` в запросе нет в реестре для этой валюты, либо неизвестен код валюты.
- **The system could not find payment details, please try again later** — нет свободных платёжных реквизитов.
- **It is not possible to assign details due to the large number of duplicate amounts** — не удалось найти уникальную сумму (если итеративный поиск суммы был отключён или не сработал).

### POST /api/v3/CancelPayInOrder

Отменяет существующий PayIn-ордер. Ордер может быть отменён только если он ожидает оплаты (Status = 1).

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `order_id` (Opt.) | Number | Внутренний идентификатор ордера в Aeterna. |
| `incoming_id` (Opt.) | String | Ваш внешний идентификатор транзакции (если order_id не передан). |

#### Пример ответа

```json
{
  "result": true,
  "message": "Order canceled"
}
```

### POST /api/GetOrderStatus

Возвращает текущий статус PayIn-ордера. Нужно передать хотя бы одно из полей `order_id` / `incoming_id`. Если переданы оба, поиск идёт по `order_id`.

HTTP-статус — `200` и для успешного, и для неуспешного ответа. Аутентификация — та же legacy-схема HMAC, что и у остальных методов `/api/Get*` (`X-App-Token`, `X-App-Access-Ts`, `X-App-Access-Sig`).

В этом ответе нет `status_id_num` — это поле есть только в колбеке. Строка статуса — имя из словаря (например, `completed`), не `paid` и не `success`.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `order_id` (Opt.) | Number (Long) | Внутренний ID ордера. Нужно передать хотя бы одно из полей `order_id` / `incoming_id`. |
| `incoming_id` (Opt.) | String | Ваш внешний ID ордера (используется, если `order_id` не передан). |

#### Пример успешного ответа

```json
{
  "result": true,
  "order_id": 33,
  "incoming_id": "PIN-1",

  "amount_in": 100,
  "currency_in": "BDT",
  "amount_out": 0.8,
  "currency_out": "USDT",
  "rate": 125.54,
  "comission": 9,
  "status_id": 6,
  "status": "completed",

  "direction": "PayIn",
  "fiat_amount": 100,
  "fiat_currency": "BDT",
  "usdt_amount": 0.8,
  "usdt_currency": "USDT",
  "commission": 9
}
```

**Совместимость:** устаревшие (legacy) поля (`amount_in`, `currency_in`, `amount_out`, `currency_out`, `comission`) сохранены без изменений. **Для новых интеграций** предпочтительнее канонический блок (`fiat_amount`, `fiat_currency`, `usdt_amount`, `usdt_currency`, `direction`, `commission`).

#### Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `result` | Boolean | `true`, если ордер найден. |
| `order_id` | Number (Long) | Внутренний ID ордера. |
| `incoming_id` | String | Ваш внешний идентификатор транзакции. |
| `amount_in` | Number (Double) | Legacy. Сумма ордера в фиате. |
| `currency_in` | String | Legacy. ISO-код фиатной валюты, в которой платит плательщик. |
| `amount_out` | Number (Double) | Legacy. Сумма ордера в USDT. |
| `currency_out` | String | Legacy. ISO-код исходящей валюты (берётся из ордера). |
| `rate` | Number (Double) | Курс ордера, округлённый до 6 знаков после запятой. |
| `comission` | Number (Double) | Legacy. Комиссия мерчанта в процентах (историческое написание с одной `m`). |
| `status_id` | Number (Integer) | Числовой ID статуса. См. [Статусы ордеров](#статусы-ордеров). |
| `status` | String | Системное имя статуса для этого ID (первое значение словаря статусов), например `"completed"`, `"trader_assingned"`. |
| `direction` | String | Canonical. На этом эндпоинте всегда `"PayIn"`. |
| `fiat_amount` | Number (Double) | Canonical. То же значение, что `amount_in`. |
| `fiat_currency` | String | Canonical. То же значение, что `currency_in`. |
| `usdt_amount` | Number (Double) | Canonical. То же значение, что `amount_out`. |
| `usdt_currency` | String | Canonical. Всегда `"USDT"`. |
| `commission` | Number (Double) | Canonical. То же значение, что `comission`. |

#### Ответ с ошибкой

HTTP `200`. Тело — JSON-объект с `result: false`.

```json
{
  "result": false,
  "message": "Order not found"
}
```

#### Сообщения об ошибках

Поле `message` содержит одну из следующих строк либо текст исключения при непредвиденном сбое:

- `Request parameters cannot be empty!` — тело запроса пустое.
- `Merchandiser not registered!` — нет мерчанта для `X-App-Token`.
- `Merchandiser are blocked!` — мерчант заблокирован.
- `The signature is not valid!` — подпись HMAC не совпадает.
- `User not found!` — нет учётной записи пользователя мерчанта.
- `Order number can not be null` — не переданы ни `order_id`, ни `incoming_id`.
- `Order not found` — у этого мерчанта нет PayIn-ордера с таким идентификатором.
- `Headers not a valid!` — заголовки запроса отсутствуют.

### POST /api/GetPayInOrders

Возвращает список PayIn-ордеров с фильтрацией по датам и статусу. Возвращает данные в формате JSON или CSV (с табуляцией в качестве разделителя).

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `date_from` (Req.) | String | Начальная дата выборки (формат: `"dd.MM.yyyy"`). |
| `date_to` (Req.) | String | Конечная дата выборки (формат: `"dd.MM.yyyy"`). |
| `status_id` (Opt.) | Integer/String | ID статуса для фильтрации. Передайте `0`, чтобы получить все ордеры независимо от статуса. |
| `format` (Req.) | String | Формат ответа: `"json"` или `"csv"`. |

HTTP-статус — `200` и для успешного, и для неуспешного ответа. Успешный ответ при `format=json` — **JSON-массив** (без обёртки `result`), `Content-Type: application/json`. Успешный ответ при `format=csv` — обычный текст с колонками через табуляцию, `Content-Type: text/plain`. Ошибка — JSON-объект `{ "result": false, "message": "..." }`.

#### Пример успешного ответа (format=json)

```json
[
  {
    "id": 1234567,
    "incoming_id": "merchant-order-123",
    "created_at": "01.04.2026 12:34:56",
    "status_id": 6,
    "status": "completed",
    "currency_code": "RUB",
    "amount": 1500,
    "amount_usdt": 16.45,
    "rate": 91.32,
    "comission": 1.5,
    "comission_amount": 0.25
  }
]
```

Если ордеров по фильтру нет, тело ответа — пустой массив: `[]`.

#### Пример успешного ответа (format=csv)

Обычный текст, колонки разделены табуляцией. Первая строка — заголовок. Колонка процента комиссии в заголовке называется `comission_rate`; то же значение в JSON — поле `comission`.

```
id	incoming_id	created_at	status_id	status	currency_code	amount	amount_usdt	rate	comission_rate	comission_amount
1234567	merchant-order-123	01.04.2026 12:34:56	6	completed	RUB	1500	16.45	91.32	1.5	0.25
```

Если ордеров по фильтру нет, тело ответа — только строка заголовка.

#### Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `id` | Number (Long) | Внутренний ID ордера. |
| `incoming_id` | String | Ваш внешний идентификатор транзакции. Пустая строка, если не задан. |
| `created_at` | String | Время создания ордера в формате `dd.MM.yyyy HH:mm:ss`. |
| `status_id` | Number (Integer) | Числовой ID статуса. См. [Статусы ордеров](#статусы-ордеров). |
| `status` | String | Системное имя статуса для этого ID (первое значение словаря статусов), например `"completed"`, `"trader_assingned"`. |
| `currency_code` | String | ISO-код фиатной валюты, в которой платит плательщик. |
| `amount` | Number (Double) | Сумма ордера в этой фиатной валюте. |
| `amount_usdt` | Number (Double) | Сумма ордера в USDT. |
| `rate` | Number (Double) | Курс ордера, округлённый до 6 знаков после запятой. |
| `comission` | Number (Double) | Комиссия мерчанта в процентах. В CSV заголовок этой колонки — `comission_rate`. |
| `comission_amount` | Number (Double) | Комиссия в USDT: `amount_usdt / 100 * comission`, округление до 2 знаков. |

#### Ответ с ошибкой

HTTP `200`. Тело — JSON-объект, не массив.

```json
{
  "result": false,
  "message": "The signature is not valid!"
}
```

#### Сообщения об ошибках

Поле `message` содержит одну из следующих строк либо текст исключения при непредвиденном сбое:

- `Merchandiser not registered!` — нет мерчанта для `X-App-Token`.
- `Merchandiser are blocked!` — мерчант заблокирован.
- `The signature is not valid!` — подпись HMAC не совпадает.
- `User not found!` — нет учётной записи пользователя с email мерчанта.
- `The order number does not exist in the system, please indicate the correct status id.` — `status_id` не равен `0` и не является известным статусом.
- ``date_from or date_to can`t be empty`` — дата отсутствует или не разбирается.
- `Format not response` — `format` не равен `json` и не равен `csv` (сравнение без учёта регистра).
- `Headers not a valid` — заголовки запроса отсутствуют.
- `Request parameters cannot be empty!` — тело запроса пустое.

---

## PayOut-ордеры

Ордеры на вывод (исходящие платежи клиентам мерчанта). Мерчант расходует USDT/крипто со своего баланса, а система выполняет фиатную выплату по реквизитам, указанным клиентом.

**Жизненный цикл:** при создании PayOut-ордера он ожидает принятия трейдером. Как только трейдер берёт его в работу, он отправляет фиатные средства по указанным вами реквизитам. После того как трейдер отметит перевод выполненным, система финализирует ордер автоматически. При возникновении проблемы вы можете открыть диспут.

### POST /api/v3/PayoutOrder

Создаёт новую заявку на выплату. Система проверяет достаточность баланса с учётом комиссий и наценок к курсу.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `amount` (Req.) | Number | Сумма в основных единицах фиатной валюты (не в центах / не в минимальной единице). |
| `currency` (Req.) | String | Код фиатной валюты (например, "RUB", "KZT", "KRW"). |
| `payment_method` (Req.) | String | Код метода оплаты (см. список ниже). Какие коды включены для валюты — настройка мерчанта, а не полный каталог `GET /api/PaymentMethods`. |
| `incoming_id` (Req.) | String | Ваш внешний уникальный идентификатор транзакции (для идемпотентности). Если поле не передать или оставить пустым, ордер всё равно создаётся. Если передать — значение должно быть уникальным у этого мерчанта. |
| `comment` (Req., nullable) | String | Платёжные реквизиты в произвольной форме, сохраняются как есть. Локальный трейдер видит этот текст как строку реквизитов. Как поле читает внешний партнёр — зависит от пути исполнения, см. [KRW payout](#krw-payout--куда-класть-реквизиты-получателя). Поле обязательно в запросе, но значение может быть `null`. |
| `bank` (Opt.) | String | Необязательное имя банка для **подбора курса**, не реквизиты получателя. Нераспознанное значение **не** отклоняет создание (в отличие от PayIn). Банк получателя **не** выводится из номера счёта. См. [KRW payout](#krw-payout--куда-класть-реквизиты-получателя). |
| `payer_id` (Opt.) | String | Стабильный идентификатор конечного пользователя (получателя) на стороне мерчанта. Используется для антифрода и привязки повторных выплат к одному клиенту. Это не номер счёта и не номер карты. |
| `payer_name` (Opt.) | String | Имя получателя в произвольной форме, сохраняется на ордере. Локальный трейдер **не** видит это поле в строке реквизитов — если имя должен прочитать трейдер, положите его в `comment`. |
| `back_link` (Opt.) | String | Поле принимается в запросе, но для обычных выплат `account_number` / `card_number` не используется. PayOut не открывает hosted-страницу оплаты. |
| `callback_url` (Opt.) | String | URL вебхука именно для этого ордера. |
| `ttl` (Opt.) | Integer | Время жизни ордера в минутах до автоотмены. |
| `iterative_sum_search_enabled` (Opt.) | Boolean | Включает итеративный поиск суммы для подбора доступных реквизитов (используется в специфических сценариях). По умолчанию: `false`. |
| `extra` (Opt.) | Object | Дополнительные поля, зависящие от гео/метода (например, `user_phone`, `user_email`, `user_name`, `pok_user_cnic` для Пакистана, `bdt_user_account_no` для Бангладеш). Для KRW-партнёра каскада `extra.user_name` — имя получателя, которое партнёр требует; локальный трейдер не видит его в строке реквизитов. |

**Поддерживаемые методы оплаты (`PaymentMethod`):**

- `card_number` — перевод по номеру банковской карты (Visa, MasterCard и т. д.).
- `phone_number` — перевод по номеру телефона (мобильные переводы, SBP).
- `account_number` — перевод на расчётный счёт.
- `iban` — перевод по IBAN.
- `sepa_transfer` — SEPA-перевод (Европа).
- `sbp` — Система быстрых платежей (СБП) для России.
- `fawry` — Fawry (Египет).
- `upi` — UPI (Индия).
- `imps` — IMPS (Индия).
- `pago_movil` — PagoMovil (Венесуэла).
- `qr_code` — перевод по QR-коду.
- `jazz_cash` — JazzCash (Пакистан).
- `easypaisa` — Easypaisa (Пакистан).
- `quasi_ecom` — оплата по номеру карты с подтверждением 3D-Secure.
- `bkash` — Bkash (Бангладеш).
- `nagad` — Nagad (Бангладеш).
- `c2becom` — банковский эквайринг.
- `paypal` — электронный кошелёк PayPal.

#### Пример успешного ответа

```json
{
  "result": true,
  "order_id": 987654,
  "current_rate": 95.5
}
```

В ответе создания только эти три поля. Для обычного PayOut нет `payment_link`, `card_input_link` и редиректа.

#### KRW payout — куда класть реквизиты получателя

Поисковые слова: `KRW`, South Korea, KRW payout, `comment`, recipient bank, `account_number`.

KRW PayOut **не** открывает hosted-страницу оплаты. Редиректа нет. `payment_link`, `card_input_link` и `back_link` относятся к другим методам / к PayIn — не к обычной KRW-выплате `account_number`.

`amount` — в основных единицах валюты (не в центах). `ttl` — минуты, не секунды.

API не вычисляет банк получателя по номеру счёта. Одного номера счёта недостаточно, чтобы гарантировать исполнение.

Как читают реквизиты — зависит от того, кто исполняет ордер. Не отправляйте одну раскладку полей на все пути.

- **Локальный трейдер.** В кабинете трейдера строка реквизитов — весь `comment` (плюс метод оплаты и, если `bank` из запроса совпал со справочником, это имя банка). `payer_name` и `extra.user_name` пишутся в ордер, но **не** показываются в этой строке. Для локального KRW-перевода оператор опирается на текст, который видит: в `comment` одной строкой **банк + номер счёта + имя получателя** (свободная форма, пробелы допустимы), например `"IBK 012345678901 HONG GILDONG"`.
- **Внешний партнёр выплаты (KRW-каскад).** Партнёр берёт номер счёта из *первого токена* `comment` (цифры в токене до первого пробела), имя получателя из `extra.user_name` (обязательно — без имени партнёр отклоняет), банк из `bank` запроса (должен быть в справочнике, иначе партнёр отклоняет). Строка, которая начинается с кода банка, например `"IBK 012345678901 HONG GILDONG"`, здесь ломается: в первом токене нет цифр. Нужно: `comment` = номер счёта, `extra.user_name` = держатель, `bank` = имя банка.
- **Внешний аукцион.** Разбор `comment` best-effort: счёт / карта / телефон / IBAN ищется в любой позиции; оставшиеся токены становятся кандидатами названия банка. Личное имя в той же строке может быть принято за банк, а не за имя получателя. Нераспознанные части создание не блокируют.

**Контракт API (все пути):** `comment` сохраняется как есть; `bank` в запросе — роутинг курса, неизвестное значение создание не отклоняет; `payer_name` пишется в ордер; `extra.user_name` пишется в JSON `extra` и обязателен, когда ордер уходит KRW-партнёру каскада. Не описывайте `bank` запроса как «банк получателя» и не кладите имя только в `payer_name` / `extra.user_name`, если его должен увидеть локальный трейдер.

### POST /api/GetPayOutOrderStatus

Возвращает текущий статус конкретного PayOut-ордера по его ID. Нужно передать хотя бы одно из полей `order_id` / `incoming_id`. Если переданы оба, поиск идёт по `order_id`.

HTTP-статус — `200` и для успешного, и для неуспешного ответа. В этом ответе нет `status_id_num` — это поле есть только в колбеке. Для `status_id` 6 строка — `completed`, не `success` и не `paid`.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `order_id` (Opt.) | Number (Long) | Внутренний идентификатор ордера в Aeterna. Должен быть передан хотя бы один из `order_id` / `incoming_id`. |
| `incoming_id` (Opt.) | String | Ваш внешний идентификатор ордера (используется, если `order_id` не передан). |

#### Пример ответа

```json
{
  "result": true,
  "order_id": 987654,
  "incoming_id": "ext-payout-123",

  // Legacy fields (preserved as-is for backward compatibility; for PayOut, in/out roles are mirrored)
  "amount_in": 104.71,
  "currency_in": "USDT",
  "amount_out": 10000.0,
  "currency_out": "RUB",
  "rate": 95.5,
  "comission": 1.5,
  "status_id": 6,
  "status": "completed",

  // Canonical fields (recommended for new integrations)
  "direction": "PayOut",
  "fiat_amount": 10000.0,
  "fiat_currency": "RUB",
  "usdt_amount": 104.71,
  "usdt_currency": "USDT",
  "commission": 1.5
}
```

**Совместимость:** устаревшие (legacy) поля (`amount_in`, `currency_in`, `amount_out`, `currency_out`, `comission`) сохранены без изменений. **Для новых интеграций** предпочтительнее канонический блок (`fiat_amount`, `fiat_currency`, `usdt_amount`, `usdt_currency`, `direction`, `commission`) — он симметричен для PayIn / PayOut и исключает историческую путаницу `in`/`out`.

### POST /api/GetPayOutOrders

Возвращает историю выплат. Возвращает список выплат в формате JSON или CSV (с табуляцией) за указанный период.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `date_from` (Req.) | String | Начальная дата выборки (формат: `"dd.MM.yyyy"`). |
| `date_to` (Req.) | String | Конечная дата выборки (формат: `"dd.MM.yyyy"`). |
| `status_id` (Opt.) | Integer/String | ID статуса для фильтрации. Передайте `0`, чтобы получить все ордеры независимо от статуса. |
| `format` (Req.) | String | Формат ответа: `"json"` или `"csv"`. |

HTTP-статус — `200` и для успешного, и для неуспешного ответа. Успешный ответ при `format=json` — **JSON-массив** (без обёртки `result`), `Content-Type: application/json`. Успешный ответ при `format=csv` — обычный текст с колонками через табуляцию, `Content-Type: text/plain`. Ошибка — JSON-объект `{ "result": false, "message": "..." }`.

#### Пример успешного ответа (format=json)

```json
[
  {
    "id": 987654,
    "incoming_id": "ext-payout-123",
    "created_at": "01.04.2026 12:34:56",
    "status_id": 6,
    "status": "completed",
    "currency_code": "RUB",
    "amount": 10000,
    "amount_usdt": 104.71,
    "rate": 95.5,
    "comission": 1.5,
    "comission_amount": 1.57
  }
]
```

Если ордеров по фильтру нет, тело ответа — пустой массив: `[]`.

#### Пример успешного ответа (format=csv)

Обычный текст, колонки разделены табуляцией. Первая строка — заголовок. Колонка процента комиссии в заголовке называется `comission_rate`; то же значение в JSON — поле `comission`.

```
id	incoming_id	created_at	status_id	status	currency_code	amount	amount_usdt	rate	comission_rate	comission_amount
987654	ext-payout-123	01.04.2026 12:34:56	6	completed	RUB	10000	104.71	95.5	1.5	1.57
```

Если ордеров по фильтру нет, тело ответа — только строка заголовка.

#### Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `id` | Number (Long) | Внутренний ID ордера. |
| `incoming_id` | String | Ваш внешний идентификатор транзакции. Пустая строка, если не задан. |
| `created_at` | String | Время создания ордера в формате `dd.MM.yyyy HH:mm:ss`. |
| `status_id` | Number (Integer) | Числовой ID статуса. См. [Статусы ордеров](#статусы-ордеров). |
| `status` | String | Системное имя статуса для этого ID (первое значение словаря статусов), например `"completed"`, `"trader_assingned"`. |
| `currency_code` | String | ISO-код фиатной валюты выплаты. |
| `amount` | Number (Double) | Сумма выплаты в этой фиатной валюте. |
| `amount_usdt` | Number (Double) | Сумма ордера в USDT. |
| `rate` | Number (Double) | Курс ордера, округлённый до 6 знаков после запятой. |
| `comission` | Number (Double) | Комиссия мерчанта в процентах. В CSV заголовок этой колонки — `comission_rate`. |
| `comission_amount` | Number (Double) | Комиссия в USDT: `amount_usdt / 100 * comission`, округление до 2 знаков. |

#### Ответ с ошибкой

HTTP `200`. Тело — JSON-объект, не массив.

```json
{
  "result": false,
  "message": "The signature is not valid!"
}
```

#### Сообщения об ошибках

Поле `message` содержит одну из следующих строк либо текст исключения при непредвиденном сбое:

- `Merchandiser not registered!` — нет мерчанта для `X-App-Token`.
- `Merchandiser are blocked!` — мерчант заблокирован.
- `The signature is not valid!` — подпись HMAC не совпадает.
- `User not found!` — нет учётной записи пользователя с email мерчанта.
- `The order number does not exist in the system, please indicate the correct status id.` — `status_id` не равен `0` и не является известным статусом.
- ``date_from or date_to can`t be empty`` — дата отсутствует или не разбирается.
- `Format not response` — `format` не равен `json` и не равен `csv` (сравнение без учёта регистра).
- `Headers not a valid` — заголовки запроса отсутствуют.
- `Request parameters cannot be empty!` — тело запроса пустое.

---

## Трансграничный PayIn (третья страна)

Специализированный PayIn-поток для трансграничных депозитов. Вы задаёте сумму в своей **расчётной валюте** (`currency`); плательщик платит в **той же валюте**, а средства зачисляются получателю в **валюте третьей страны** (`third_country_currency`) — например, вы создаёте ордер в KZT, плательщик отправляет KZT через систему международных переводов своего банка, и KZT конвертируется в UZS и зачисляется получателю. Ордер распределяется **только среди внутренних трейдеров**, у которых совпадает валюта третьей страны; **внешний каскад или аукцион не используются**.

**Жизненный цикл:** идентичен обычному PayIn — назначаются трейдер и платёжные реквизиты, и ордер ожидает плательщика. Ожидаемая сумма зачисления получателю в валюте третьей страны рассчитывается и сохраняется в ордере, а затем сверяется с фактическим входящим платежом системой сопоставления.

Аутентификация, выбор трейдера/реквизитов и форма ответа **унифицированы с** `/api/v3/PayInOrder`. Единственные дополнения — поле `third_country_currency` и расчёт суммы третьего плеча.

### POST /api/NewOrderThirdCountry

Создаёт трансграничный PayIn-ордер. Система проверяет лимиты, определяет курсы обмена для обоих плеч, выбирает трейдера и платёжные реквизиты по валюте третьей страны и возвращает реквизиты / ссылку на оплату для плательщика.

**Опциональный заголовок:** `X-Request-Timeout-Ms` (int) — максимальное время в миллисекундах для ожидания создания ордера.

#### Модель валют

- `currency` — ваша расчётная валюта; `amount` выражается в ней, и плательщик платит в ней (например, "KZT").
- `USDT` — то, что вы получаете после конвертации (возвращается как `usdt_amount`).
- `third_country_currency` — валюта, в которой средства зачисляются получателю после международной конвертации банком отправителя (например, "UZS"); трейдер выбирается по совпадению валюты третьей страны.

Суммы: `usdt_amount = amount / rate` (rate = currency → USDT). Сумма зачисления получателю = `amount × third_rate` (third_rate = third_country_currency → currency); оба курса используют окно актуальности 24 часа.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `currency` (Req.) | String | Код вашей расчётной валюты, в которой задаётся `amount` и платит плательщик (например, "KZT", "RUB"). |
| `third_country_currency` (Req.) | String | Валюта зачисления получателю после международной конвертации банком отправителя (например, "UZS"). По этой валюте подбираются трейдеры и реквизиты. |
| `amount` (Req.) | Number | Сумма ордера в расчётной валюте (`currency`). |
| `payment_method` (Req.) | String | Строковый код метода оплаты (например, "card_number"). Строка, а не число. |
| `bank` (Opt.) | String | Предпочтительный банк для подбора реквизитов. Выбирается автоматически, если не указан. |
| `payer_id` (Opt.) | String | Уникальный идентификатор плательщика в вашей системе. |
| `payer_name` (Opt.) | String | Имя плательщика в произвольной форме (обязательно в некоторых гео). |
| `incoming_id` (Opt.) | String | Ваш внутренний идентификатор транзакции для предотвращения дублирующих ордеров (идемпотентность). |
| `ttl` (Opt.) | Integer | Время жизни ордера в минутах (переопределяет настройки мерчанта). |
| `back_link` (Opt.) | String | URL для редиректа плательщика после завершения платежа. |
| `callback_url` (Opt.) | String | URL для получения вебхуков об изменении статуса этого ордера. |
| `comment` (Opt.) | String | Комментарий к ордеру в произвольной форме. |

#### Пример запроса

```json
{
  "currency": "KZT",
  "third_country_currency": "UZS",
  "amount": 50000,
  "payment_method": "card_number",
  "incoming_id": "merchant-order-555",
  "callback_url": "https://merchant.example/api/callback",
  "back_link": "https://merchant.example/payment/return"
}
```

#### Пример успешного ответа

```json
{
  "result": true,
  "order_id": 168,
  "initial_amount": 50000,
  "usdt_amount": 92.16,
  "rate": 542.56,
  "amount": 50000,
  "life_time": "2026-06-05T08:35:30Z",
  "requisites": {
    "bank_id": 171,
    "bank_name": "Humo telegram",
    "name": "Receiver Name",
    "payment_link": "https://payment.example.com/pay/...",
    "card": "7845 **** **** 1151",
    "phone_number": null,
    "account_number": null,
    "iban": null,
    "transfer_info": null
  }
}
```

**Примечание:** `amount` в ответе указан в вашей расчётной валюте (`currency`), согласованно с v3 PayIn. Ожидаемая сумма зачисления получателю в валюте третьей страны сохраняется в ордере для последующего сопоставления платежей и **не** возвращается здесь.

#### Типичные ошибки

- **third_country_currency is required** — валюта третьей страны не передана.
- **Currency not found** — неверный код `currency` / `third_country_currency`.
- **The order does not meet the limits** — сумма вне допустимых границ.
- **The exchange rate is not available** — нет актуального курса currency → USDT.
- **The exchange rate for the third currency is not available** — нет актуального курса third_country_currency → currency.
- **There are currently no available traders…** — нет трейдера, подходящего по валюте третьей страны / комиссии / балансу.
- **…payment details cannot be provided by the trader…** — нет подходящих платёжных реквизитов.
- **Order with this IncomingId already exists** — дублирующий `incoming_id`.

---

## Статусы ордеров

И PayIn-, и PayOut-ордеры используют одну и ту же систему внутренних статусов.

Успешный ордер — `status_id = 6`, системное имя `completed` (не `success` и не `paid`). Статусы `8`–`13` — отмены. Отменённый или спорный ордер не является зачислением.

Ордер в споре (`7`, `disputed`) — и, если оператор позже подтвердит, отменённый ордер — всё ещё может перейти в `6`. Тогда уходит ещё один колбек. Источник истины — **последний** колбек вместе с `POST /api/GetOrderStatus` (PayIn) или `POST /api/GetPayOutOrderStatus` (PayOut).

`status_id_num` есть только в колбеке, не в этих status-эндпоинтах. Канонические суммы в колбеке — `fiat_amount` / `fiat_currency` и `usdt_amount` / `usdt_currency`; не путайте зеркальные legacy `amountIn` / `amountOut` между PayIn и PayOut. Подпись и повторная отправка — в разделе [Колбэки](#колбэки).

| Status ID | Системное имя | Описание |
|-----------|---------------|----------|
| `1` | new_order | Новый ордер создан и ожидает оплаты или назначения. |
| `2` | trader_assingned | Трейдер назначен на ордер. |
| `3` | payment_detail_assigned | Для ордера предоставлены платёжные реквизиты. |
| `4` | payment_completed_by_merch | Пользователь отметил платёж как выполненный на странице оплаты. |
| `5` | payment_completed_by_trader | Трейдер подтвердил получение платежа. |
| `6` | completed | Ордер полностью завершён и успешен. |
| `7` | disputed | По ордеру открыт диспут. Также ставится на таймауте ордера, который всё ещё в `new_order`, если присланный вами по нему чек передан на решение трейдеру (см. [Чат и диспуты](#чат-и-диспуты)). |
| `8` | canceled_by_timeout | Ордер автоматически отменён из-за истечения времени (TTL). Для ордера, который исполнял платёжный партнёр, — также когда партнёр сам снял платёж по сроку; причина партнёра приходит в `comment` колбэка. Также — когда трейдер решает диспут не в пользу оплаты (см. [Чат и диспуты](#чат-и-диспуты)); текст решения приходит в `comment` колбэка. Также — для диспута, который никто не ведёт (см. [Чат и диспуты](#чат-и-диспуты)). |
| `9` | canceled_by_merch | Ордер вручную отменён мерчантом. |
| `10` | canceled_by_trader | Ордер отменён трейдером. |
| `11` | canceled_by_administrator | Ордер отменён администратором системы или платформой после отказа платёжного партнёра; причина партнёра приходит в `comment` колбэка. |
| `12` | canceled_by_disputed | Ордер отменён в результате разрешения диспута. |
| `13` | canceled_by_support | Ордер отменён командой поддержки. |
| `14` | no suitable external system | Не найдена подходящая внешняя система для каскада/аукциона. |
| `15` | no payment details in external systems | Ни одна внешняя система не смогла предоставить платёжные реквизиты. |
| `16` | Sent to external system | Ордер передан во внешнюю систему для исполнения. |

---

## Криптовыводы

Эндпоинты для вывода криптовалюты (USDT) с баланса мерчанта на внешние криптокошельки.

### POST /api/NewWithdrawalOrder

Создаёт заявку на вывод криптовалюты. Заявка передаётся администраторам платформы на обработку.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `amount` (Req.) | Number | Сумма к выводу в USDT (должна быть больше 0). |
| `wallet` (Req.) | String | Адрес внешнего криптокошелька (не может быть пустым). |
| `net` (Req.) | String | Сеть перевода, например, "TRC20", "ERC20" (не может быть пустой). |

#### Пример успешного ответа

```json
{
  "result": true,
  "assigned_id": 456,
  "message": "Accepted for processing"
}
```

---

## Получение баланса

### POST /api/GetBalance

Запрашивает текущий доступный баланс мерчанта (обычно в базовой валюте — USDT). Баланс кэшируется на 1 минуту.

> **Ограничение частоты:** запрашивать баланс можно **не чаще одного раза в минуту** на токен. Превышение лимита вернёт ошибку: `You can request your balance no more than once per minute`.

Если ваши средства хранит платёжный провайдер на отдельном счёте, открытом для вас, в ответе есть и `provider_balances` — баланс у провайдера, доступный к выплате, по валютам. Платформа этот баланс не рассчитывает: он обновляется у провайдера раз в несколько минут и уменьшается, когда провайдер выплачивает его на ваш кошелёк.

#### Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `result` | Boolean | `true`, если баланс получен. |
| `balance` | String | Баланс в USDT, два знака после запятой. |
| `provider_balances` | Array \| null | Баланс у платёжного провайдера. Пустой массив — такого баланса нет; `null` — баланс временно недоступен, повторите запрос позже. |
| `provider_balances[].currency` | String | Код валюты, например `EUR`. |
| `provider_balances[].amount` | String | Сумма, доступная к выплате, два знака после запятой. |
| `provider_balances[].updated_at` | String | Время последнего удачного обновления у провайдера, UTC (ISO 8601). |

#### Пример успешного ответа

```json
{
  "result": true,
  "balance": "15420.50",
  "provider_balances": [
    { "currency": "EUR", "amount": "1250.40", "updated_at": "2026-09-29T10:05:00Z" }
  ]
}
```

---

## Чат и диспуты

Предоставляет функциональность API для взаимодействия с трейдерами по проблемным ордерам (например, несоответствие суммы или задержки платежей) через механизмы чата и диспутов.

Файл в чате ордера, который всё ещё в `new_order` (`1`), или открытый по нему диспут до таймаута ордера статус не меняют: ордер продолжает ждать оплату и исполняется, если оплата поступит. Если к таймауту оплата не поступила, ордер переходит в `disputed` (`7`) вместо `canceled_by_timeout` (`8`), только если присланный вами чек передан на решение трейдеру (см. «Разрешение диспута трейдером» ниже), и вам приходит колбэк со `status` = `disputed`. В остальных случаях — разрешение диспутов трейдером для вашего аккаунта не включено или файл чека не присылался — ордер на таймауте отменяется, как обычно, и по отменённому ордеру можно открыть диспут. Ордер с отметкой об оплате (`payment_completed_by_merch`, `4`) на таймауте отменяется, как обычно. Ордера, которые исполняет платёжный партнёр, получают статус от партнёра.

Диспут, который никто не ведёт, отменяется по таймауту: ордер переходит из `disputed` (`7`) в `canceled_by_timeout` (`8`), и вам приходит тот же колбэк, что и при отмене ордера по таймауту. Это происходит через 15 минут после таймаута ордера, если ордер перешёл в `disputed` на таймауте, но его диспут не передан трейдеру, и через 5 минут после выключения разрешения диспутов трейдером для вашего аккаунта, если трейдер ещё не принял решение. Диспуты, которые разбирает поддержка, второй круг и диспуты, открытые вами после отмены ордера, не затрагиваются.

### POST /api/NewChatMessage

Отправляет сообщения в чат ордера. Позволяет прикреплять чеки (подтверждение оплаты). Передача `changed_amount` вместе с `initial_amount` обновит сумму ордера и автоматически инициирует диспут.

Если для вашего аккаунта включено разрешение диспутов трейдером, сообщение с файлом чека (`.jpg`, `.jpeg`, `.png`, `.bmp` или `.pdf`) открывает диспут у трейдера — см. [Разрешение диспута трейдером](#разрешение-диспута-трейдером). Сообщение без файла остаётся обычной перепиской.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `order_id` (Req.) | Number | Внутренний идентификатор ордера. |
| `message` (Opt.) | String | Текст сообщения. Может быть опущен, если есть вложение или изменение суммы. |
| `bytes` (Opt.) | String | Строка файла в кодировке Base64 (например, чек об оплате). Макс. 100 МБ. |
| `extension` (Opt.) | String | Расширение файла (например, ".png", ".pdf", ".jpg"). |
| `initial_amount` (Opt.) | Number | Текущая (исходная) сумма ордера. Должна отправляться вместе с `changed_amount` при запросе на изменение суммы. |
| `changed_amount` (Opt.) | Number | Новая исправленная сумма ордера. Если задана и отличается от текущей суммы — сумма ордера будет обновлена. |
| `actual_for_merch` (Opt.) | Boolean | Установите `false` при отправке сообщения от имени мерчанта, чтобы избежать самоуведомлений. |
| `actual_for_trader` (Opt.) | Boolean | Установите `true`, чтобы трейдер получал уведомления о новых сообщениях. |
| `actual_for_administrator` (Opt.) | Boolean | Установите `true`, чтобы уведомлять администраторов. |
| `must_show` (Opt.) | Boolean | Установите `true` (рекомендуется), чтобы показывать специальные принудительные оповещения в кабинете трейдера. |

#### Пример ответа

```json
{
  "result": true,
  "message": "Message registered successfully"
}
```

### POST /api/OpenDispute

Принудительно открывает диспут по ордеру для разрешения конфликтных ситуаций администраторами/арбитрами.

Запрос регистрирует диспут в чате ордера и сам статус ордера не меняет: `status_id` и `status` в ответе показывают статус ордера на момент вызова. Если для вашего аккаунта включено разрешение диспутов трейдером, передайте в `path` ссылку на чек — тогда чек уходит трейдеру, см. [Разрешение диспута трейдером](#разрешение-диспута-трейдером).

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `order_id` (Req.) | Number | Внутренний идентификатор ордера. |
| `comment` (Req.) | String | Описание причины открытия диспута. |
| `path` (Opt.) | String | URL ранее загруженного файла (чек/скриншот). Для разрешения диспута трейдером: публичная ссылка `http` или `https`, по которой файл скачивается без авторизации за 20 секунд; файл JPG, PNG, BMP или PDF до 20 МБ (формат определяется по содержимому файла). |

#### Пример ответа

```json
{
  "result": true,
  "order_id": 123456,
  "status_id": 8,
  "amount": 5000,
  "currency": "RUB",
  "status": "canceled_by_timeout"
}
```

### Разрешение диспута трейдером

Доступно, если для вашего аккаунта включено разрешение диспутов трейдером, — обратитесь к менеджеру. Чек пересылается трейдеру, назначенному на ордер, и трейдер принимает решение в пределах срока, настроенного для вашего аккаунта. Ордера, которые исполняет платёжный партнёр, так не обрабатываются: по ним действует обычный порядок диспута.

#### Как начинается диспут

- Чек — это файл, приложенный через `POST /api/NewChatMessage`, или ссылка в `path` запроса `POST /api/OpenDispute`.
- По ордеру, который всё ещё в `new_order` (`1`), чек уходит трейдеру только после таймаута ордера, когда ордер переходит в `7` `disputed`; оплата, поступившая до таймаута, исполняет ордер без диспута. Если к таймауту разрешение диспутов трейдером для вашего аккаунта выключено, ордер отменяется по таймауту (`canceled_by_timeout`), а чек трейдеру не уходит. Если за это время оплату отметили как выполненную (`payment_completed_by_merch`, `4`), таймаут отменяет ордер (`canceled_by_timeout`), а затем он переходит в `7` `disputed`: вам приходят оба колбэка. Оба колбэка возможны и тогда, когда чек пришёл за несколько секунд до таймаута.
- В остальных статусах ордер сразу переходит в `7` `disputed`, и вам приходит колбэк со `status` = `disputed`.
- Чек по уже исполненному ордеру (`completed`, `6`) статус не меняет: его разбирает поддержка.
- Повторный чек по тому же ордеру во время диспута добавляется к нему; срок не сбрасывается.

#### Итог диспута

| Решение трейдера | Статус ордера | Колбэк |
|------------------|---------------|--------|
| Оплата поступила | `6` `completed` | Да |
| Оплаты не было | `8` `canceled_by_timeout` | Да |
| Фейковый чек | `8` `canceled_by_timeout` | Да |
| Неверные реквизиты | `8` `canceled_by_timeout` | Да |
| Оплачено другим ордером | Остаётся `7` `disputed`, диспут разбирает поддержка | Нет, пояснение добавляется в чат ордера |
| Нет решения до конца срока | `6`, `8` или остаётся `7` — по правилу платформы | Да, если статус меняется |
| Нет решения, а разрешение диспутов трейдером для вашего аккаунта выключено | `8` `canceled_by_timeout` через 5 минут после выключения | Да, тот же, что при отмене ордера по таймауту |

В этих колбэках, кроме последней строки, `message` содержит код статуса (`completed` или `canceled_by_timeout`), `comment.message` — текст решения, заданный платформой, а `comment.path` — пустая строка. Обрабатывайте результат по `status`, текст решения не разбирайте.

#### Второй круг

После того как диспут с трейдером завершён или передан поддержке, поддержка может запустить второй круг разбора ордера. Ордер переходит в `7` `disputed`, а в чат ордера добавляется сообщение о втором круге. Вам приходит колбэк со `status` = `disputed`: `comment.message` содержит текст сообщения, `comment.path` — пустая строка; `message` содержит код статуса `disputed`, если ордер был в другом статусе, или пустую строку, если он уже был в споре. Срока у второго круга нет: решение принимает поддержка, результат приходит обычным колбэком о смене статуса. Ордера в `completed` (`6`) на второй круг не отправляются.

#### Важные замечания

- Пояснения и файлы-подтверждения трейдера добавляются в чат ордера, который виден в кабинете мерчанта; колбэк по ним не отправляется.
- Если колбэк доставляется повторно после неудачной попытки, `comment` может быть `null`: текст решения остаётся в чате ордера.
- Если ссылку на чек не удалось скачать или файл не JPG, PNG, BMP или PDF, чек трейдеру не отправляется, статус ордера не меняется, а поддержка получает уведомление.
- Если файл не проходит обязательную проверку чека (например, это не банковский чек — логотип, фотография или скриншот переписки, даже если на нём видна сумма, — или сумму платежа на нём прочитать не удалось), он не отправляется ни трейдеру, ни в поддержку, и из-за него ордер не переходит в `7` `disputed`. Ордер, который уже перешёл в `7` по таймауту, отменяется как диспут, который никто не ведёт. Следующий чек по ордеру проверяется заново и обрабатывается как обычно.
- Если поддержка или трейдер меняют статус ордера иначе, пока диспут открыт, диспут завершается с этим статусом.
- Если трейдер отметил чек как фейковый, а платформа включила блокировку плательщиков по итогам диспутов, `payer_id`, `payer_name` плательщика или оба значения заносятся в список заблокированных плательщиков вашего аккаунта: новые ордера этого плательщика отклоняются с `The payer is blocked`. Блокировка действует только для вашего аккаунта.

---

## Справочник: методы оплаты

### GET /api/PaymentMethods

Возвращает словарь **кодов** методов оплаты, которые знает платформа. Результат кэшируется на 5 минут. Список не фильтруется по валюте, стране или мерчанту.

> **Важно:** список методов оплаты постоянно расширяется. Чтобы ваша интеграция была надёжной и актуальной, настоятельно рекомендуется регулярно запрашивать доступные методы через этот эндпоинт, а не задавать их жёстко в коде приложения.

#### Валюта и `payment_method` (KRW, South Korea)

Поисковые слова: `KRW`, South Korea, `payment_method`, `GET /api/PaymentMethods`.

`GET /api/PaymentMethods` — это **справочник кодов**, а не «что включено этому мерчанту в этой валюте». Маппинга валюта→методы в API нет. Не отвечайте на вопрос «какие методы для KRW / Южной Кореи» полным enum (`sbp`, `upi`, `card_number`, `bkash`, …).

Какие коды можно отправлять — задаётся в настройках мерчанта. Для конкретной валюты смотрите настройки / спросите ops. Для KRW ops обычно включает `account_number` и на PayIn, и на PayOut — это операционная настройка, а не жёсткое правило API. Не экстраполируйте полный каталог на страну.

#### Справочник известных методов

На момент написания этой документации поддерживаются, в частности, следующие методы:

- `card_number` — перевод по номеру карты (Visa, MasterCard и т. д.)
- `phone_number` — перевод по номеру телефона (мобильные переводы, SBP)
- `account_number` — перевод на расчётный счёт
- `iban` — перевод по IBAN
- `sepa_transfer` — SEPA-перевод (Европа)
- `sbp` — Система быстрых платежей (СБП, НСПК Россия)
- `fawry` — Fawry (Египет)
- `upi` — UPI (Индия)
- `imps` — IMPS (Индия)
- `pago_movil` — PagoMovil (Венесуэла)
- `qr_code` — перевод по QR-коду
- `jazz_cash` — JazzCash (Пакистан)
- `easypaisa` — Easypaisa (Пакистан)
- `quasi_ecom` — оплата по номеру карты с подтверждением 3D-Secure
- `bkash` — Bkash (Бангладеш)
- `nagad` — Nagad (Бангладеш)
- `c2becom` — банковский эквайринг
- `paypal` — электронный кошелёк PayPal

#### Пример ответа (массив объектов)

```json
[
  {
    "id": 1,
    "name": "phone_number",
    "description": "Transfer via phone number"
  },
  {
    "id": 2,
    "name": "card_number",
    "description": "Transfer via card number"
  },
  {
    "id": 4,
    "name": "sbp",
    "description": "Faster Payments System (SBP)"
  }
]
```

---

## Справочник: курсы обмена

### GET /api/GetRates

Возвращает список поддерживаемых пар USDT→фиат и их текущие курсы обмена из кэша в памяти. Базовая валюта всегда USDT — другие направления не поддерживаются.

> **Публичный эндпоинт:** аутентификация не требуется. Нет проверки HMAC / X-App-Token / перекрёстного токена. Безопасен для частого опроса.

Также доступен альтернативный версионированный маршрут `GET /api/v1/GetRates`, возвращающий те же данные.

#### Пример успешного ответа

```json
[
  { "currency_pair": "USDT/RUB", "exchange_rate": 92.35 },
  { "currency_pair": "USDT/INR", "exchange_rate": 85.72 },
  { "currency_pair": "USDT/AZN", "exchange_rate": 1.70 }
]
```

#### Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `currency_pair` | String | Валютная пара в формате `"USDT/{CurrencyCode}"`, где `CurrencyCode` — ISO-код фиата (например, `"RUB"`, `"INR"`, `"AZN"`, `"KZT"`). |
| `exchange_rate` | Number (Double) | Сколько единиц фиата соответствует `1 USDT`. **Это число, а не строка** — учитывайте это при парсинге. |

#### Пустой / недоступный ответ

Если кэш в памяти пуст или ни один из кэшированных ID валют не присутствует в справочнике валют, эндпоинт возвращает объект ошибки вместо пустого массива:

```json
{ "result": false, "message": "Exchange rate not available" }
```

#### Важные замечания

- Значения кэшируются в памяти приложения и обновляются фоновым заданием — фиксированного TTL нет, но устаревание измеряется секундами.
- **Не** используйте этот курс для фактических расчётов по ордеру: при создании PayIn / PayOut-ордера API фиксирует собственный курс (возвращается в поле `rate` ответа на создание). Используйте его для расчётов; `GetRates` предназначен только для предварительных оценок в UI до создания ордера.

---

## Колбэки

Колбэк (вебхук) — это HTTP POST-запрос, отправляемый на ваш сервер при изменении статуса ордера. Тело представляет собой компактный JSON (без лишних пробелов) и аутентифицируется через заголовок `X-App-Access-Sig` (HMAC-SHA256 с `CallBackToken`; см. раздел «Аутентификация»).

Ордер может получить больше одного колбека (повторы, позднее подтверждение после спора или отмены). Источник истины — **последний** колбек вместе со status-эндпоинтом. `status_id_num` есть здесь и отсутствует в `POST /api/GetOrderStatus` / `POST /api/GetPayOutOrderStatus`.

### Полезная нагрузка PayIn-колбэка

Отправляется для ордеров входящих платежей (`direction: "PayIn"`).

#### Пример полезной нагрузки JSON

```json
{
  "order_id": "123456",
  "incoming_id": "ext-123",
  "direction": "PayIn",

  // Legacy block (preserved as-is)
  "currencyOut": "RUB",
  "amountOut": "10000.0",
  "amountOut_num": 10000.0,
  "currencyIn": "USDT",
  "amountIn": "104.5",
  "amountIn_num": 104.5,
  "status_id": "6",
  "status_id_num": 6,
  "status": "completed",
  "message": "Order successfully completed",
  "comment": null,
  "rate": 95.69,
  "requisites": null,
  "requested_payment_method": "card_number",

  // Canonical block (recommended for new integrations)
  "fiat_amount": 10000.0,
  "fiat_currency": "RUB",
  "usdt_amount": 104.5,
  "usdt_currency": "USDT",
  "commission": 1.5,

  // Conditional — only present when status_id == 6 AND the order is linked
  // to a matched mobile push notification with a non-empty message.
  "trigger_push_message": "Поступление 10 000.50 RUB на карту *1111"
}
```

#### Справочник полей (PayIn)

| Поле | Тип | Описание |
|------|-----|----------|
| `order_id` | String | Внутренний идентификатор ордера (числовое значение в виде строки). |
| `incoming_id` | String | Внешний/входящий идентификатор ордера мерчанта (может быть пустым). |
| `direction` | String | Всегда `"PayIn"` для этой полезной нагрузки. |
| `currencyOut` (legacy) | String | Код валюты **со стороны плательщика** (фиат, который платит пользователь). Устаревшее именование — см. «Совместимость». |
| `amountOut` (legacy) | String | Сумма в `currencyOut`, инвариантная культура (`.` как десятичный разделитель). |
| `amountOut_num` (legacy) | Number | То же значение, что и `amountOut`, но в виде числа JSON (`double`). |
| `currencyIn` (legacy) | String | Код валюты **со стороны мерчанта** (актив, который получает мерчант — обычно `"USDT"`). |
| `amountIn` (legacy) | String | Сумма в `currencyIn`, инвариантная культура. |
| `amountIn_num` (legacy) | Number | То же значение, что и `amountIn`, но в виде числа JSON (`double`). |
| `status_id` | String | Числовой ID статуса в виде строки. См. «Статусы ордеров». |
| `status_id_num` | Number | То же значение, что и `status_id`, но в виде целого числа JSON. |
| `status` | String | Код статуса в snake_case (например, `"completed"`, `"canceled_by_timeout"`). |
| `message` | String | Человекочитаемое сообщение, описывающее событие (название статуса или комментарий трейдера/системы). |
| `comment` | Object \| null | Опциональный фрагмент чата/диспута: `{ "message": "...", "path": "..." }`, если есть связанная запись чата. Если ордер снял платёжный партнёр (`status_id` `8` или `11`), в `message` приходит причина партнёра — `"Canceled by payment partner: 3.01 Declined by Acquirer: Anti-fraud"`, — а `path` равен `null`. Иначе `null`. |
| `rate` | Number | Курс обмена ордера (`double`), округлённый до **6 знаков** после запятой. Курс хранится без округления, поэтому не сравнивайте его на точное равенство и не пересчитывайте из него `usdt_amount` — берите готовое значение `usdt_amount`. |
| `requisites` | Object \| null | Разобранный JSON платёжных реквизитов ордера (банк, карта, держатель и т. д.) или `null`. |
| `requested_payment_method` | String \| null | Метод оплаты, запрошенный при создании ордера (например, `"card_number"`). |
| `fiat_amount` (canonical) | Number | Фиатная сумма (то же значение, что и `amountOut`, но в виде числа JSON). |
| `fiat_currency` (canonical) | String | Код фиатной валюты (то же, что и `currencyOut`). |
| `usdt_amount` (canonical) | Number | Сумма в USDT (то же значение, что и `amountIn`, но в виде числа JSON). |
| `usdt_currency` (canonical) | String | Всегда `"USDT"`. |
| `commission` (canonical) | Number | Комиссия мерчанта, применённая к ордеру. |
| `trigger_push_message` (conditional) | String | **Опционально, условно.** Текст мобильного push-уведомления, сопоставленного с ордером. Присутствует **только** когда `status_id == 6` И ордер связан с сопоставленным уведомлением с непустым сообщением; иначе ключ **полностью опускается** (не `null`). |

### Полезная нагрузка PayOut-колбэка

Отправляется для ордеров исходящих выплат (`direction: "PayOut"`). По сравнению с PayIn форма отличается тремя моментами:

- **Зеркальная семантика legacy-полей**: для PayOut `currencyOut` — это сторона **USDT** (то, что списывается с мерчанта), а `currencyIn` — сторона **фиата** (то, что получает получатель). Это противоположно PayIn — используйте канонический блок (`fiat_*` / `usdt_*`), чтобы избежать путаницы.
- **Нет** полей `requisites` и **нет** `requested_payment_method`.
- Добавляется `decline_reason` (заполняется только для отменённых/отклонённых ордеров, `status_id >= 8`; иначе `null`).

#### Пример полезной нагрузки JSON (отклонённый ордер)

```json
{
  "order_id": "987654321",
  "incoming_id": "payout-ext-002",
  "direction": "PayOut",

  // Legacy block (preserved as-is; in/out roles are mirrored vs PayIn)
  "currencyOut": "USDT",
  "amountOut": "50.0",
  "amountOut_num": 50.0,
  "currencyIn": "RUB",
  "amountIn": "4750.0",
  "amountIn_num": 4750.0,
  "status_id": "9",
  "status_id_num": 9,
  "status": "canceled_by_merch",
  "message": "Canceled by merchant",
  "comment": null,
  "rate": 95.0,
  "decline_reason": "Merchant requested cancel",

  // Canonical block (recommended for new integrations)
  "fiat_amount": 4750.0,
  "fiat_currency": "RUB",
  "usdt_amount": 50.0,
  "usdt_currency": "USDT",
  "commission": 1.5
}
```

#### Справочник полей (PayOut)

| Поле | Тип | Описание |
|------|-----|----------|
| `order_id` | String | Внутренний идентификатор PayOut-ордера (числовое значение в виде строки). |
| `incoming_id` | String | Внешний/входящий идентификатор ордера мерчанта. |
| `direction` | String | Всегда `"PayOut"` для этой полезной нагрузки. |
| `currencyOut` (legacy) | String | Код валюты **стороны USDT** для PayOut (то, что списывается с мерчанта). Зеркально относительно PayIn — см. «Совместимость». |
| `amountOut` (legacy) | String | Сумма в USDT, инвариантная культура. |
| `amountOut_num` (legacy) | Number | То же значение, что и `amountOut`, но в виде числа JSON. |
| `currencyIn` (legacy) | String | Код валюты **стороны фиата** для PayOut (то, что получает получатель). |
| `amountIn` (legacy) | String | Фиатная сумма, инвариантная культура. |
| `amountIn_num` (legacy) | Number | То же значение, что и `amountIn`, но в виде числа JSON. |
| `status_id` | String | Числовой ID статуса в виде строки. |
| `status_id_num` | Number | То же значение, что и `status_id`, но в виде целого числа JSON. |
| `status` | String | Код статуса в snake_case. |
| `message` | String | Человекочитаемое сообщение, описывающее событие. |
| `comment` | Object \| null | Опциональный фрагмент чата/диспута `{ "message", "path" }` или `null`. |
| `rate` | Number | Курс обмена ордера, округлённый до **6 знаков** после запятой. Курс хранится без округления, поэтому не сравнивайте его на точное равенство и не пересчитывайте из него суммы. |
| `decline_reason` | String \| null | Заполняется только когда `status_id >= 8` (отменённые/отклонённые ордеры). Если причина не указана, подставляется `"Unspecified decline reason"`. Для всех остальных статусов — `null`. |
| `fiat_amount` (canonical) | Number | Фиатная сумма (то же значение, что и `amountIn` для PayOut). |
| `fiat_currency` (canonical) | String | Код фиатной валюты (то же, что и `currencyIn` для PayOut). |
| `usdt_amount` (canonical) | Number | Сумма в USDT (то же значение, что и `amountOut` для PayOut). |
| `usdt_currency` (canonical) | String | Всегда `"USDT"`. |
| `commission` (canonical) | Number | Комиссия мерчанта, применённая к выплате. |

### Совместимость и стиль полей

Каждый колбэк содержит **два параллельных набора полей**:

- **Legacy-блок** — исходные camelCase-поля (`currencyOut`, `currencyIn`, `amountOut`, `amountIn`) плюс их строковые/`_num`-числовые дубликаты. Роли in/out **зеркальны** между PayIn и PayOut (см. таблицы по каждой полезной нагрузке выше). Существующие интеграции продолжают работать без изменений.
- **Канонический блок** — `fiat_amount`, `fiat_currency`, `usdt_amount`, `usdt_currency` (всегда `"USDT"`), `direction` и `commission`. Рекомендуется для **новых интеграций**: семантика однозначна и не зависит от `direction`.

**Советы по стилю полей:**

- Именование ключей **смешанное по дизайну**: snake_case (`order_id`, `incoming_id`, `status_id`, `fiat_amount`, ...) и camelCase для legacy-полей (`currencyOut`, `currencyIn`, `amountOut`, `amountIn`). Рассматривайте имена ключей как непрозрачные строки.
- `order_id` и `status_id` — **строки**; `status_id_num`, `amountOut_num`, `amountIn_num`, `rate`, все `fiat_*`/`usdt_*`/`commission` — **числа**.
- `amountOut` / `amountIn` используют `InvariantCulture` (всегда `.` как десятичный разделитель).
- Поле `comment`, если присутствует, является объектом `{ "message", "path" }`, а не обычной строкой. Оно независимо от поля `message`. Если PayIn-ордер снял платёжный партнёр, `comment.message` начинается с `Canceled by payment partner: `, за которым идут код и текст партнёра, а `comment.path` равен `null`.
- `requisites` — произвольный объект, отражающий сохранённые платёжные реквизиты ордера (банк, карта, держатель, телефон и т. д.), и присутствует **только для PayIn**.

### Повторные отправки и пропущенные колбэки

Если первичная отправка не удалась (не-2xx ответ, таймаут, ошибка DNS и т. д.), система автоматически повторяет колбэк по фиксированному расписанию. Отдельный фоновый воркер также обнаруживает ордеры, статус которых изменился, но по которым **не было записано ни одного колбэка** («пропущенные колбэки»), и отправляет их тоже.

#### Расписание повторов

Воркер срабатывает каждую минуту и решает, выполнять ли попытку отправки ожидающего колбэка, исходя из времени, прошедшего с момента первого создания записи. Минимальная задержка перед *следующей* попыткой растёт геометрически (основание `5`):

| Уже выполнено попыток | Мин. минут с момента создания до следующей попытки | Δ относительно предыдущей попытки |
|-----------------------|------------------------------------------------------|-----------------------------------|
| `0` | 1 мин | — |
| `1` | 6 мин | +5 |
| `2` | 31 мин | +25 |
| `3` | 156 мин (~2 ч 36 м) | +125 |
| `4` | 781 мин (~13 ч) | +625 |

Лимит попыток по умолчанию — **5**; после этого запись финализируется, и дальнейшие повторы не выполняются. Таким образом, от неудачного колбэка отказываются примерно на отметке 13 часов с момента первого сбоя.

#### Идемпотентность

Как только ваш эндпоинт ответит 2xx-кодом (`200 OK`, `201 Created` и т. д.), колбэк помечается финальным и **не** будет повторяться. Чтобы избежать дубликатов при 2xx, возвращайте успех **только после** того, как вы сохранили новое состояние ордера.

Воркер также пропускает колбэки с пустым настроенным URL — они помечаются как `"Callback URL not configured"` и финализируются немедленно. Обновите URL колбэка в профиле мерчанта, чтобы (повторно) включить доставку.

#### Пропущенные колбэки

Если по какой-либо причине первичный колбэк так и не был сохранён (например, процесс воркера упал во время отправки), фоновое задание подхватит ордер автоматически. Чтобы не конкурировать с первичной отправкой, обнаружение пропущенных колбэков использует **5-минутное окно актуальности**: ордеры, обновлённые менее 5 минут назад, пропускаются в текущем такте.

*При повторной отправке форма JSON идентична первичной отправке.* Отличаться могут только поля, зависящие от контекста: для PayIn фрагмент чата не восстанавливается, поэтому в `comment` приходит причина отмены платёжным партнёром, если она есть, иначе `null`, а `decline_reason` неприменим; для PayOut `comment` восстанавливается из последней записи чата с вложением, а `decline_reason` берётся из сохранённой в ордере причины отклонения.

### POST /api/CallbackResender/resend — повторная отправка колбэков (вручную)

Инструмент для принудительной массовой повторной отправки вебхуков (колбэков) на ваш сервер на случай, если ваша инфраструктура испытывала простой. Передайте либо точный массив ордеров (`items`), либо укажите период времени (`from`, `to`) вместе с `resend_pay_in`/`resend_pay_out`.

#### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `items` (Opt.) | Array | Массив объектов: `{ "id": "string", "type": "pay_in" \| "payout", "callback_address": "string?" }`. Если передан — поля периода ниже игнорируются. |
| `from` (Opt.) | DateTime | Начало периода (UTC). Используется только если `items` не передан. |
| `to` (Opt.) | DateTime | Конец периода (UTC). Используется только если `items` не передан. |
| `resend_pay_in` (Opt.) | Boolean | Повторно отправить PayIn-колбэки за период. |
| `resend_pay_out` (Opt.) | Boolean | Повторно отправить PayOut-колбэки за период. |

**Примечание по `items[].type`:** значение — строка в snake_case — либо `pay_in` (входящий ордер от плательщика), либо `payout` (исходящая выплата получателю).

#### Пример ответа

```json
{
  "count": 150
}
```

Система ставит перечисленные колбэки в очередь асинхронно и выполняет повторную отправку.

---

# Часть 2. Payment Orchestrator (PayIn)

## Payment Orchestrator — введение / RSA

Payment Orchestrator — это специализированный B2B API для платёжных систем (ПС) и шлюзов, который позволяет маршрутизировать ордеры AETERNA через вашу платформу для предоставления лучших платёжных реквизитов для PayIn или для исполнения исходящих PayOut-переводов.

### Модель аукциона

Маршрутизация реализована как **аукцион**: AETERNA отправляет единый запрос на создание ордера *всем* участникам аукциона параллельно и ждёт предложения до **5 секунд**. Каждый участник возвращает предлагаемый курс обмена и комиссию (а для PayIn — и фактические платёжные реквизиты). Победитель выбирается на основе трёх факторов:

- Предлагаемая **комиссия**;
- Предлагаемый **курс обмена**;
- Внутренний **рейтинг** участника в AETERNA (доля успешных операций, доля диспутов, время отклика).

Поскольку рассылка параллельна, ваши платёжные реквизиты резервируются **всего на несколько секунд**, пока выбирается победитель — проигравшие участники получают немедленный `CancelOrder`, чтобы освободить резерв.

> **Совет:** если ваши предложения часто отклоняются, рассмотрите снижение курса обмена или комиссии и улучшите свой рейтинг, снижая количество диспутов и задержку обработки ордеров на вашей стороне.

### Аутентификация (RSA)

Взаимодействие использует асимметричные RSA-подписи 2048 бит. Вы один раз генерируете пару ключей на своей стороне и передаёте нам **только публичный ключ**; приватный ключ никогда не покидает ваши серверы.

- **Ваш приватный ключ:** формат PEM, PKCS#8 — используется вами для подписи всех исходящих запросов и колбэков.
- **Ваш публичный ключ (передаётся нам):** формат PEM, X.509 — используется AETERNA для проверки ваших колбэков и исходящих запросов к нам.

> **Храните приватный ключ в секрете.** Никогда не добавляйте его в систему контроля версий, не отправляйте через чат/почту и меняйте его, если подозреваете утечку. Компрометация приватного ключа позволяет злоумышленнику выдавать себя за вашу платформу.

#### Генерация ключей (через OpenSSL)

```bash
# Generate private key
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048

# Extract public key to provide to AETERNA
openssl rsa -in private_key.pem -pubout -out public_key.pem
```

#### Примеры формата PEM

**Приватный ключ (PKCS#8):**

```plaintext
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASC... (base64 body) ...IDAQAB
-----END PRIVATE KEY-----
```

**Публичный ключ (X.509):**

```plaintext
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A... (base64 body) ...IDAQAB
-----END PUBLIC KEY-----
```

#### Генерация подписи

Каждый запрос должен содержать два заголовка:

- `X-Timestamp` — текущий Unix timestamp в **секундах UTC**. Запросы старше 2 минут или с таймстампом в будущем отклоняются.
- `X-Signature` — RSA-SHA256-подпись канонической строки, закодированная в **Base64**.

**Формат канонической строки:** `{timestamp}|{ExternalSystemName}|{orderId}|{MethodName}`

Пример: `1712664000|MyPaySystem|ext-12345|CreateOrder`

Каждый эндпоинт интеграции использует свой суффикс `{MethodName}` — точное значение см. в соответствующем разделе.

#### Эталонная реализация на C# (BouncyCastle.NetCore)

Приведённый ниже фрагмент — это в точности тот хелпер, который AETERNA использует внутри для подписи и проверки канонических строк. Он работает как для ваших исходящих запросов, так и для входящих колбэков AETERNA.

```csharp
using System.Text;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;

public static class RsaAuthHelper
{
    private static readonly AsymmetricKeyParameter PrivateKey =
        LoadPrivateKey(File.ReadAllText("private_key.pem"));

    public static string SignCanonicalString(string canonical)
    {
        var signer = SignerUtilities.GetSigner("SHA256withRSA");
        signer.Init(true, PrivateKey);
        var bytes = Encoding.UTF8.GetBytes(canonical);
        signer.BlockUpdate(bytes, 0, bytes.Length);
        return Convert.ToBase64String(signer.GenerateSignature());
    }

    public static bool VerifySignature(string canonical, string base64Signature, string publicKeyPem)
    {
        var publicKey = LoadPublicKey(publicKeyPem);
        var signer = SignerUtilities.GetSigner("SHA256withRSA");
        signer.Init(false, publicKey);
        var bytes = Encoding.UTF8.GetBytes(canonical);
        signer.BlockUpdate(bytes, 0, bytes.Length);
        var signatureBytes = Convert.FromBase64String(base64Signature);
        return signer.VerifySignature(signatureBytes);
    }

    private static AsymmetricKeyParameter LoadPrivateKey(string pem)
    {
        using var reader = new StringReader(pem);
        var obj = new PemReader(reader).ReadObject();
        return obj switch
        {
            AsymmetricCipherKeyPair keyPair => keyPair.Private,
            AsymmetricKeyParameter key when key.IsPrivate => key,
            _ => throw new Exception("Cannot parse private key")
        };
    }

    private static AsymmetricKeyParameter LoadPublicKey(string pem)
    {
        using var reader = new StringReader(pem);
        var obj = new PemReader(reader).ReadObject();
        return obj switch
        {
            AsymmetricKeyParameter key when !key.IsPrivate => key,
            _ => throw new Exception("Cannot parse public key")
        };
    }
}
```

#### Эндпоинт самопроверки

Чтобы проверить вашу реализацию подписи относительно эталона AETERNA, вы можете вызвать публичный тестовый эндпоинт:

**POST /api/TestExternalAuction/TestSign**

Передайте любую `canonical_string`; эндпоинт вернёт Base64 RSA-SHA256-подпись, созданную с помощью общеизвестного *тестового* приватного ключа. Для проверки на своей стороне используйте соответствующий тестовый публичный ключ (возвращается `GET /api/TestExternalAuction/GetPublicKey`). Ключи **только тестовые** и не используются в продакшене.

**Тело запроса:**

```json
{ "canonical_string": "1712664000|MyPaySystem|ext-12345|CreateOrder" }
```

**Ответ:**

```json
{ "signature": "Base64EncodedSignatureValue==" }
```

---

## Интеграция с Orchestrator

Orchestrator работает как система ставок/аукциона, где внешние системы выступают участниками аукциона. Мы ожидаем ответ с наилучшими возможными условиями (курс обмена и комиссия) в строгие сроки.

### Эндпоинты, которые вы должны реализовать

Чтобы выступать участником аукциона, ваша платформа должна предоставить следующие HTTP POST-эндпоинты под единым `{RootUrl}`. AETERNA будет вызывать их с RSA-подписанным телом:

| Метод | Путь | Направление | Описание |
|-------|------|-------------|----------|
| POST | `{RootUrl}/CreateOrder` | PayIn | Создать ордер входящего платежа и вернуть ваши платёжные реквизиты. |
| POST | `{RootUrl}/ApprovePayInOrder` | PayIn | Получить уведомление о выигрыше PayIn-аукциона — ордер закреплён за вами. |
| POST | `{RootUrl}/GetOrderStatus` | PayIn / PayOut | Вернуть текущий статус ордера. |
| POST | `{RootUrl}/CancelOrder` | PayIn | Отменить PayIn-ордер и освободить зарезервированные реквизиты. |
| POST | `{RootUrl}/CreateDispute` | PayIn / PayOut | Передать сообщение чата, запрос на изменение суммы или открыть диспут. |
| POST | `{RootUrl}/CreatePayoutOrder` | PayOut | Сделать ставку на исходящую выплату (курс + комиссия, без реквизитов). |
| POST | `{RootUrl}/CancelPayoutOrder` | PayOut | Отменить PayOut-ставку (например, когда выиграл другой участник). |
| POST | `{RootUrl}/ApprovePayoutOrder` | PayOut | Получить реквизиты выигравшего PayOut и выполнить перевод. |

### Правила времени отклика

- Мы ожидаем ответа на `CreateOrder` / `CreatePayoutOrder` максимум в течение **5 секунд**.
- Если вы ответите *до* `stop_auction_time_unix`, ваша ставка принимается в аукцион.
- Если вы ответите *после* `stop_auction_time_unix`, но в пределах 5 секунд, мы отправим вам запрос `CancelOrder` / `CancelPayoutOrder` с причиной `too_long_response`.
- Если вы ответите *после* 5 секунд, результат полностью игнорируется, и запрос на отмену не отправляется.
- Если ваш ответ не проходит валидацию на стороне AETERNA (отсутствующие поля, несоответствие `payment_method`, несоответствие суммы при `iterative_sum_search_enabled = false` и т. д.), мы отправляем `CancelOrder` с причиной `not_valid_response`.
- Если ваша ставка **выигрывает** PayIn-аукцион, AETERNA отправляет вам уведомление `ApprovePayInOrder`, подтверждающее, что ордер закреплён за вами — отдельный accept не требуется, просто продолжайте обработку. (Победители PayOut вместо этого получают `ApprovePayoutOrder` с реквизитами.)

### Общие требования

- Все запросы и ответы должны быть в формате **JSON** с `Content-Type: application/json`.
- Поля должны именоваться в `snake_case`.
- Вы должны предоставить RSA **публичный ключ** (PEM, X.509), чтобы AETERNA могла проверять *ваши* исходящие запросы и колбэки.
- Вы должны **подписывать каждый исходящий запрос и колбэк** к AETERNA своим RSA приватным ключом (PEM, PKCS#8) — см. «Введение / RSA».
- Вы должны **проверять каждый входящий запрос от AETERNA**: верифицировать `X-Signature` относительно канонической строки нашим публичным ключом и отклонять любой запрос, чей `X-Timestamp` старше 2 минут или установлен в будущем.
- Каждый метод имеет собственный суффикс канонической строки (например, `...|CreateOrder`, `...|CreatePayoutOrder`) — см. соответствующий раздел.

> **Уведомления-колбэки обязательны.** Как только ордер назначен вам, ваша платформа **обязана** отправлять вебхуки об изменении статуса на `callback_url`, переданный нами в запросе на создание. Без колбэков интеграция не будет работать — AETERNA не сможет произвести расчёт по ордеру с мерчантом.

---

## CreateOrder (PayIn)

**POST {RootUrl}/CreateOrder**

AETERNA вызывает этот эндпоинт на **вашей** платформе, чтобы сделать ставку на новый PayIn-ордер. Вы должны ответить платёжными реквизитами, удовлетворяющими ограничениям `max_exchange_rate` и `max_commission`, либо вернуть ошибку.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CreateOrder
```

### Тело запроса

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID). |
| `currency` (Req.) | String | Код фиатной валюты (например, "RUB", "VES"). |
| `max_exchange_rate` (Req.) | Double | Максимально допустимый курс обмена к USDT, округлённый до 6 знаков после запятой. |
| `max_commission` (Req.) | Double | Максимально допустимая комиссия за исполнение ордера (в процентах). |
| `amount` (Req.) | Double | Сумма ордера в фиате. |
| `cancel_order_time_unix` (Req.) | Long | Unix timestamp (сек), когда ордер становится неактуальным/истекает. |
| `stop_auction_time_unix` (Req.) | Long | Unix timestamp (сек) дедлайна аукциона. |
| `callback_url` (Req.) | String | URL, на который AETERNA ожидает получать ваши вебхуки об изменении статуса для этого ордера. |
| `allowed_payment_method` (Req.) | String | Разрешённый метод оплаты, snake_case. Поддерживаемые в аукционе: `card_number`, `phone_number`, `account_number`, `iban`, `sbp`, `quasi_ecom`. Полный реестр доступен по `GET https://api.skyphoenix.net/api/PaymentMethods`. |
| `iterative_sum_search_enabled` (Req.) | Boolean | Если `true`, вам разрешено вернуть реквизиты для слегка изменённой суммы, когда точная запрошенная сумма недоступна. Если `false` и сумма в ответе отличается от запрошенной — ответ отклоняется. |
| `allowed_bank_name` (Opt.) | String | Конкретный разрешённый банк (опционально). |

### Пример успешного ответа

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string",
  "amount": 1000.0,
  "exchange_rate": 75.25,
  "commission": 1.5,
  "payment_details": {
     "type": "card_number",
     "name": "Ivan Ivanov",
     "bank_name": "Sberbank",
     "card": "4276........"
  }
}
```

Структура объекта `payment_details` зависит от поля `type` — см. таблицы по каждому методу ниже.

### payment_details по методам оплаты

#### `card_number` — перевод по банковской карте

| Поле | Тип | Описание |
|------|-----|----------|
| `type` (Req.) | String | Всегда `"card_number"`. |
| `name` (Req.) | String | Имя получателя. |
| `bank_name` (Req.) | String | Название банка. |
| `card` (Req.) | String | Номер карты. |

#### `phone_number` — перевод по номеру телефона

| Поле | Тип | Описание |
|------|-----|----------|
| `type` (Req.) | String | Всегда `"phone_number"`. |
| `name` (Req.) | String | Имя получателя. |
| `bank_name` (Req.) | String | Название банка. |
| `phone_number` (Req.) | String | Номер телефона получателя. |

#### `account_number` — перевод на банковский счёт

| Поле | Тип | Описание |
|------|-----|----------|
| `type` (Req.) | String | Всегда `"account_number"`. |
| `name` (Req.) | String | Имя получателя. |
| `bank_name` (Req.) | String | Название банка. |
| `account_number` (Req.) | String | Номер банковского счёта получателя. |

#### `iban` — перевод по IBAN

| Поле | Тип | Описание |
|------|-----|----------|
| `type` (Req.) | String | Всегда `"iban"`. |
| `name` (Req.) | String | Имя получателя. |
| `bank_name` (Req.) | String | Название банка. |
| `iban` (Req.) | String | IBAN получателя. |

#### `sbp` — Система быстрых платежей (Россия)

| Поле | Тип | Описание |
|------|-----|----------|
| `type` (Req.) | String | Всегда `"sbp"`. |
| `name` (Req.) | String | Имя получателя. |
| `bank_name` (Req.) | String | Название банка. |
| `phone_number` (Req.) | String | Номер телефона получателя, зарегистрированный в СБП. |

#### `quasi_ecom` — оплата картой с 3D-Secure / хостинговая страница

| Поле | Тип | Описание |
|------|-----|----------|
| `type` (Req.) | String | Всегда `"quasi_ecom"`. |
| `payment_link` (Req.) | String | URL страницы ввода карты на вашей стороне. Плательщик мерчанта перенаправляется сюда для ввода данных карты и прохождения 3D-Secure. |

### Валидация ответа

Даже если вы вернёте `is_success=true`, AETERNA внутренне проверит ответ и может отклонить его (отправив вам `CancelOrder` с причиной `not_valid_response`), когда:

- `iterative_sum_search_enabled = false` в запросе, но `amount` в ответе отличается от запрошенной суммы более чем на `0.1`.
- `exchange_rate` в ответе не больше `0`.
- `payment_details` отсутствует или `null`.
- `payment_details.type` не соответствует запрошенному `allowed_payment_method`.
- Отсутствуют обязательные для метода поля (`name`, `bank_name`, `card`/`phone_number`/`iban`/`account_number`/`payment_link`).

---

## ApprovePayInOrder

**POST {RootUrl}/ApprovePayInOrder**

После того как аукцион определён, AETERNA вызывает `ApprovePayInOrder` на стороне **победителя**, чтобы явно подтвердить, что PayIn-ордер закреплён за вами и его нужно продолжать обрабатывать. Платёжные реквизиты вы уже вернули в `CreateOrder` — `ApprovePayInOrder` лишь подтверждает выбор и итоговые согласованные условия. Отдельный accept не требуется.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|ApprovePayInOrder
```

### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID), то же значение, что и в `CreateOrder`. |
| `external_order_id` (Req.) | String | Ваш внутренний ID ордера, возвращённый в `CreateOrder`. |
| `amount` (Req.) | Double | Подтверждённая итоговая сумма к оплате (после возможного итеративного подбора суммы). |
| `exchange_rate` (Req.) | Double | Подтверждённый курс обмена к USDT. |
| `commission` (Req.) | Double | Подтверждённая комиссия (в процентах). |

### Тело ответа (JSON)

| Поле | Тип | Описание |
|------|-----|----------|
| `is_success` (Req.) | Boolean | Верните `true`, чтобы подтвердить, что ордер принят на вашей стороне — это останавливает повторы AETERNA. `false` (а также неразбираемое тело или статус не-2xx) считается неуспешной попыткой. |
| `error_code` (Opt.) | String | Машиночитаемый код ошибки (snake_case) при `is_success=false`; `null` при успехе. |
| `error_message` (Opt.) | String | Человекочитаемое пояснение ошибки (опционально). |
| `external_order_id` (Opt.) | String | Необязательное эхо вашего внутреннего ID ордера — только информативно, AETERNA его не проверяет. |

> Отвечайте **HTTP 200** и `is_success=true` для подтверждения. Любой статус не-2xx, неразбираемое тело или `is_success=false` считается неуспешной попыткой и запускает следующий повтор.

### Пример успешного ответа

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string"
}
```

### Механизм повторов (best-effort)

AETERNA повторяет `ApprovePayInOrder` до **3 раз** с фиксированной задержкой в **10 секунд** между попытками. Повторы прекращаются, как только любая попытка вернёт `is_success=true`.

В отличие от `ApprovePayoutOrder`, если все попытки неудачны, ордер **не** отменяется: платёжные реквизиты уже переданы плательщику и оплата может идти. Фолбэком остаётся `GetOrderStatus`, которым AETERNA сверяет актуальный статус ордера. Каждая попытка — успешная или нет — логируется на стороне AETERNA.

> **Совет по реализации:** обрабатывайте `ApprovePayInOrder` как идемпотентное подтверждение — отвечайте `is_success=true`, даже если получили его по одному и тому же ордеру несколько раз.

---

## GetOrderStatus

**POST {RootUrl}/GetOrderStatus**

AETERNA вызывает этот эндпоинт на вашей платформе, чтобы получить текущий статус ордера (PayIn или PayOut).

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|GetOrderStatus
```

### Тело запроса

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID). |
| `external_id` (Req.) | String | Ваш внутренний ID запроса. |

### Пример ответа

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "status": 6
}
```

**Статусы:**

- `1`: Создан
- `2`: Трейдер назначен
- `3`: Платёжные реквизиты назначены
- `4`: Мерчант подтвердил платёж
- `5`: Трейдер подтвердил платёж
- `6`: Завершён (успех)
- `7`: Открыт диспут
- `8`: Отменён по таймауту
- `9`: Отменён мерчантом
- `10`: Отменён трейдером
- `11`: Отменён администратором
- `12`: Отменён супервайзером
- `13`: Отменён в результате диспута

---

## CancelOrder (PayIn)

**POST {RootUrl}/CancelOrder**

AETERNA вызывает этот эндпоинт на вашей платформе, чтобы отменить PayIn-ордер и попросить вас освободить зарезервированные платёжные реквизиты. Срабатывает, например, когда другой участник выигрывает аукцион или ваш ответ был слишком поздним либо невалидным.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CancelOrder
```

### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID). |
| `external_id` (Req.) | String | Ваш внутренний ID запроса. |
| `reason` (Req.) | String | Код причины отмены (snake_case). |
| `reason_message` (Opt.) | String | Текстовое пояснение. |

**Причины отмены (код `reason`):**

- `too_long_response`: ответ занял слишком много времени.
- `not_valid_response`: неверный формат ответа или непрохождение валидации (например, отсутствие реквизитов, несоответствие типа, несоответствие суммы).
- `system_selected_another_performer`: аукцион выиграл другой участник.
- `auction_timeout_after_finish`: таймаут произошёл после завершения аукциона.
- `server_error`: ошибка сервера на нашей стороне.
- `other`: иная причина — см. `reason_message` для деталей.

---

## CreateDispute

**POST {RootUrl}/CreateDispute**

AETERNA вызывает этот эндпоинт на вашей платформе, чтобы передать сообщение чата, запрос на изменение суммы или формально открыть диспут. Применяется как к PayIn-, так и к PayOut-ордерам.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CreateDispute
```

### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID). |
| `external_order_id` (Req.) | String | Ваш внутренний ID запроса. |
| `comment` (Req.) | String | Текст сообщения/диспута. |
| `attachment_path` (Opt.) | String | Ссылка на скриншот/изображение (ваша платформа может его скачать). |
| `type` (Req.) | String | Тип сообщения, snake_case: `message` (обычное сообщение чата), `change_amount` (запрос на изменение суммы ордера) или `dispute` (формально открыть диспут). |
| `new_amount` (Opt.) | Double | Новая сумма, обязательна при `type = "change_amount"`. |

---

## Вебхуки и ошибки (PayIn)

### Уведомления-колбэки

Вы отправляете POST-запросы на `callback_url`, переданный нами в `CreateOrder`, всякий раз, когда статус или сумма PayIn-ордера меняется на вашей стороне.

#### Полезная нагрузка (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | ID ордера (GUID), то же значение, что и в `CreateOrder`. |
| `status_id` (Opt.) | Integer | Новый ID статуса (1-13), если изменился. |
| `amount` (Opt.) | Double | Новая сумма, если она была скорректирована. |

Должен присутствовать хотя бы один из `status_id` / `amount`, иначе AETERNA ответит `validation_error`.

#### Заголовки

- `Content-Type` — должен быть `application/json`.
- `X-Timestamp` — Unix-время в секундах UTC. AETERNA принимает только таймстампы в пределах последних 2 минут; значения в будущем отклоняются с `timestamp_invalid_future`, значения старше 2 минут — с `timestamp_expired`.
- `X-Signature` — ваша Base64 RSA-SHA256-подпись канонической строки ниже.

**Каноническая строка:**

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|AuctionCallback
```

#### Ответ AETERNA — успех

При успехе AETERNA отвечает HTTP 200 и:

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null
}
```

#### Ответ AETERNA — ошибка

При любой ошибке валидации/аутентификации AETERNA отвечает HTTP 200 и объектом ошибки — ваша платформа должна трактовать `is_success=false` как неудачу доставки и повторить колбэк позже:

```json
{
  "is_success": false,
  "error_code": "timestamp_expired",
  "error_message": "Timestamp is expired."
}
```

### Общие коды ошибок (`external_auction_error_code`)

- `signature_missing`: подпись отсутствует.
- `signature_invalid`: подпись недействительна.
- `timestamp_invalid`: неверный формат таймстампа.
- `timestamp_invalid_future`: таймстамп в будущем относительно UTC-часов AETERNA.
- `timestamp_expired`: таймстамп истёк (старше 2 минут).
- `validation_error`: валидация запроса не пройдена.
- `request_parameters_is_invalid`: неверные параметры запроса.
- `order_not_found`: ордер не найден.
- `no_available_traders`: нет доступных трейдеров для исполнения ордера.
- `unable_process_required_payment_system`: платёжная система не поддерживается.
- `unable_process_required_payment_method`: метод оплаты не поддерживается.
- `all_payments_details_busy`: все платёжные реквизиты сейчас заняты.
- `too_low_commission`: предложенная комиссия слишком низкая.
- `too_high_exchange_rate`: предложенный курс обмена слишком высокий.
- `exists_same_amount_order`: активный ордер с точно такой же суммой уже существует.
- `other`: иная общая ошибка.

---

# Часть 3. Payment Orchestrator (PayOut)

## PayOut-аукцион

PayOut-аукцион — это отдельный аукционный трек для **исходящих выплат** (мерчант выплачивает фиат конечному пользователю). Он работает аналогично PayIn-аукциону, но с двумя ключевыми отличиями в протоколе:

- **Реквизиты отправляет AETERNA, а не участник.** PayOut-ставка содержит только ваш предлагаемый курс обмена и комиссию — фактические реквизиты выплаты (IBAN / карта / телефон / счёт) приходят *от* AETERNA *победителю* в отдельном вызове `ApprovePayoutOrder`.
- **Сумма фиксируется AETERNA** и не может быть изменена участниками — соответственно, в ответе на ставку нет поля `amount`.

### Сквозной поток

1. AETERNA рассылает `CreatePayoutOrder` всем настроенным участникам PayOut-аукциона параллельно.
2. Каждый участник отвечает `exchange_rate` и `commission` (без реквизитов) в течение 5 секунд.
3. AETERNA выбирает лучшую ставку на основе комиссии, курса и рейтинга участника.
4. Проигравшие участники получают `CancelPayoutOrder` с причиной, описывающей, почему они были исключены.
5. Победитель получает `ApprovePayoutOrder`, содержащий структурированный объект `requisites` для фактического перевода.
6. `ApprovePayoutOrder` повторяется до **3 раз** с интервалом в **10 секунд**, если победитель возвращает `is_success=false`.
7. Если все 3 попытки неудачны, AETERNA отменяет ордер автоматически: статус ордера становится `8` (отменён по таймауту), победителю отправляется `CancelPayoutOrder` с причиной `other` и `reason_message="ApprovePayoutOrderFailed"`, а мерчант получает колбэк об отмене.
8. Победитель отправляет уведомления `PayoutAuctionCallback` на `callback_url` AETERNA по мере продвижения ордера (назначен, оплачен, завершён и т. д.).

### Эндпоинты, которые нужно реализовать на вашей стороне

| Метод | Путь | Описание |
|-------|------|----------|
| POST | `{RootUrl}/CreatePayoutOrder` | Сделать ставку с курсом + комиссией для PayOut. |
| POST | `{RootUrl}/CancelPayoutOrder` | Отменить PayOut-ставку / одобренный ордер. |
| POST | `{RootUrl}/ApprovePayoutOrder` | Получить структурированные реквизиты выигравшего PayOut и выполнить перевод. |

`GetOrderStatus` и `CreateDispute` общие с PayIn — см. соответствующие разделы.

---

## CreatePayoutOrder

**POST {RootUrl}/CreatePayoutOrder**

AETERNA рассылает этот запрос всем участникам PayOut-аукциона параллельно. Вы должны ответить своим предлагаемым `exchange_rate` и `commission` в течение 5 секунд. **Не** включайте реквизиты выплаты — они придут позже в `ApprovePayoutOrder`, если вы выиграете.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CreatePayoutOrder
```

### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID). |
| `currency` (Req.) | String | Код фиатной валюты (например, "RUB", "VES"). |
| `max_commission` (Req.) | Double | Максимально допустимая комиссия (в процентах), при которой ваша ставка будет принята. |
| `amount` (Req.) | Double | Фиксированная сумма выплаты в фиате — не может быть изменена вами. |
| `cancel_order_time_unix` (Req.) | Long | Unix timestamp (сек), когда ордер становится неактуальным. |
| `stop_auction_time_unix` (Req.) | Long | Unix timestamp (сек), когда заканчивается аукцион. |
| `callback_url` (Req.) | String | URL, на который AETERNA ожидает получать ваши уведомления `PayoutAuctionCallback`. |
| `allowed_payment_method` (Req.) | String | Разрешённый метод оплаты, snake_case (поддерживаемый список см. в CreateOrder). |
| `allowed_bank_name` (Opt.) | String | Конкретный разрешённый банк (опционально). |

### Пример успешного ответа

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string",
  "exchange_rate": 75.25,
  "commission": 1.5
}
```

### Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `is_success` (Req.) | Boolean | Результат обработки. |
| `error_code` (Opt.) | String | Код ошибки (см. «Общие коды ошибок»). |
| `error_message` (Opt.) | String | Текст сообщения об ошибке. |
| `external_order_id` (Req.) | String | Ваш внутренний ID ордера — на него далее ссылается `ApprovePayoutOrder.external_order_id`. |
| `exchange_rate` (Req.) | Double | Ваш предлагаемый курс. Должен быть больше `0`. |
| `commission` (Req.) | Double | Ваша предлагаемая комиссия (в процентах). Должна быть больше или равна `0`. |

> **Отличия от PayIn `CreateOrder`:**
> - Нет `payment_details` в ответе — реквизиты доставляются победителю через `ApprovePayoutOrder`.
> - Нет поля `amount` в ответе — участник не может изменить сумму.
> - Нет `iterative_sum_search_enabled` в запросе — неактуально для PayOut.

### Валидация ответа

Даже если вы вернёте `is_success=true`, AETERNA отклонит ставку (отправив `CancelPayoutOrder` с причиной `not_valid_response`), когда:

- `exchange_rate` не больше `0`.
- `commission` отрицательна.

---

## CancelPayoutOrder

**POST {RootUrl}/CancelPayoutOrder**

AETERNA вызывает этот эндпоинт на вашей платформе, чтобы отменить PayOut-ставку или одобренный PayOut-ордер. Форма тела и список кодов `reason` идентичны PayIn CancelOrder.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|CancelPayoutOrder
```

### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID). |
| `external_id` (Req.) | String | Ваш внутренний ID ордера, возвращённый в `CreatePayoutOrder`. |
| `reason` (Req.) | String | Код причины отмены (snake_case). Полный список см. в PayIn CancelOrder. |
| `reason_message` (Opt.) | String | Текстовое пояснение в произвольной форме. В частности, заполняется как `"ApprovePayoutOrderFailed"`, когда AETERNA отменяет ордер после 3 неудачных попыток `ApprovePayoutOrder`. |

### Пример успешного ответа

```json
{ "is_success": true }
```

---

## ApprovePayoutOrder

**POST {RootUrl}/ApprovePayoutOrder**

После того как аукцион определён, AETERNA вызывает `ApprovePayoutOrder` на стороне победителя и передаёт структурированный объект `requisites`, описывающий, куда отправить фиатную выплату. Ваша платформа должна инициировать фактический перевод и ответить `is_success=true`, как только запрос принят в обработку на вашей стороне.

### Каноническая строка для подписи

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|ApprovePayoutOrder
```

### Тело запроса (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | Наш ID запроса (GUID), то же значение, что и в `CreatePayoutOrder`. |
| `external_order_id` (Req.) | String | Ваш внутренний ID ордера, возвращённый в `CreatePayoutOrder`. |
| `requisites` (Req.) | Object | Структурированные реквизиты выплаты — см. таблицу ниже. |

### Объект `requisites`

| Поле | Тип | Описание |
|------|-----|----------|
| `payment_method` (Req.) | String | Метод оплаты, snake_case: `iban`, `card_number`, `phone_number`, `account_number`, `sepa_transfer`, `sbp`, `quasi_ecom`, `c2becom`, `pago_movil`, `jazz_cash`, `easypaisa`, `bkash`, `nagad`, `upi`, `imps`, `fawry`. |
| `iban` (cond.) | String | Заполняется, когда `payment_method` — `iban` или `sepa_transfer`. |
| `card_number` (cond.) | String | Заполняется, когда `payment_method` — `card_number`, `quasi_ecom` или `c2becom`. |
| `phone_number` (cond.) | String | Заполняется, когда `payment_method` — `phone_number`, `sbp`, `pago_movil`, `jazz_cash`, `easypaisa`, `bkash` или `nagad`. |
| `account_number` (cond.) | String | Заполняется, когда `payment_method` — `account_number`, `upi`, `imps` или `fawry`. |
| `bank_name` (Opt.) | String | Название банка, если известно. |
| `payer_name` (Opt.) | String | Имя получателя (если предоставлено мерчантом). |
| `comment` (Req.) | String | Исходный комментарий мерчанта в произвольной форме, сохранён как есть. |

### Пример объекта `requisites`

```json
{
  "payment_method": "iban",
  "iban": "TR123456789012345678901234",
  "card_number": null,
  "phone_number": null,
  "account_number": null,
  "bank_name": "Ziraat Bankası",
  "payer_name": "John Doe",
  "comment": "TR123456789012345678901234 Ziraat Bankası"
}
```

### Тело ответа (JSON)

| Поле | Тип | Описание |
|------|-----|----------|
| `is_success` (Req.) | Boolean | Верните `true`, чтобы подтвердить, что выплата принята в обработку на вашей стороне — это останавливает повторы AETERNA. `false` (а также неразбираемое тело или статус не-2xx) считается неуспешной попыткой. |
| `error_code` (Opt.) | String | Машиночитаемый код ошибки (snake_case) при `is_success=false`; `null` при успехе. |
| `error_message` (Opt.) | String | Человекочитаемое пояснение ошибки (опционально). |
| `external_order_id` (Opt.) | String | Необязательное эхо вашего внутреннего ID ордера — только информативно, AETERNA его не проверяет. |

> Отвечайте **HTTP 200** и `is_success=true` для подтверждения. Любой статус не-2xx, неразбираемое тело или `is_success=false` считается неуспешной попыткой и запускает следующий повтор (после 3 неуспешных попыток ордер отменяется — см. ниже).

### Пример успешного ответа

```json
{
  "is_success": true,
  "error_code": null,
  "error_message": null,
  "external_order_id": "uuid-string"
}
```

### Механизм повторов

AETERNA повторяет `ApprovePayoutOrder` до **3 раз** с фиксированной задержкой в **10 секунд** между попытками. Повторы прекращаются, как только любая попытка вернёт `is_success=true`.

Если все 3 попытки возвращают `is_success=false` (или завершаются сетевой ошибкой):

- Статус ордера устанавливается в `8` (отменён по таймауту).
- AETERNA отправляет вам `CancelPayoutOrder` с `reason="other"` и `reason_message="ApprovePayoutOrderFailed"`.
- Мерчант получает колбэк об отмене.

Каждая попытка — успешная или нет — логируется на стороне AETERNA.

> **Совет по реализации:** отвечайте `is_success=true`, как только выплата поставлена в очередь на исполнение на вашей стороне, а не после финализации фактического фиатного перевода. Итоговые результаты доставляются позже через `PayoutAuctionCallback`.

---

## PayOut-колбэки

### Уведомления-колбэки

Поток, форма полезной нагрузки, заголовки, формат ответа об успехе/ошибке и коды ошибок для PayOut-колбэков **идентичны** PayIn — полный справочник см. в «Вебхуки и ошибки (PayIn)».

Единственное отличие, специфичное для PayOut, — **суффикс канонической строки**: PayOut-колбэки должны подписываться с `PayoutAuctionCallback` вместо `AuctionCallback`.

### Каноническая строка для подписи (PayOut)

```plaintext
{timestamp}|{external_system_name}|{system_order_id}|PayoutAuctionCallback
```

### Полезная нагрузка (JSON)

| Параметр | Тип | Описание |
|----------|-----|----------|
| `system_order_id` (Req.) | String | ID ордера (GUID), то же значение, что и в `CreatePayoutOrder`. |
| `status_id` (Opt.) | Integer | Новый ID статуса (1-13), если изменился. |
| `amount` (Opt.) | Double | Новая сумма, если она была скорректирована. |

Должен присутствовать хотя бы один из `status_id` / `amount`, иначе AETERNA ответит `validation_error`.

### Заголовки, формат ответа, коды ошибок

См. «Вебхуки и ошибки (PayIn)» — правила те же. `Content-Type: application/json`, `X-Timestamp` в пределах ±2 минут (секунды UTC, таймстампы в будущем отклоняются), `X-Signature` как Base64 RSA-SHA256 канонической строки выше.
