--- title: "Обмены" url: "https://docs.x-hub.online/exchange/" description: "Создание обмена, статусы и работа с клиентами обмена." relatedDocuments: - "https://docs.x-hub.online/exchange/kyc/" - "https://docs.x-hub.online/webhooks/" --- # Обмены (Exchange) X-Hub поддерживает два направления обменов: - **USDT → RUB** — основной поток: клиент присылает USDT, X-Hub конвертирует и отправляет RUB клиенту на карту по СБП. Создаётся **мерчантом** через `POST /api/v1/exchanges` (см. ниже). Предназначено для мерчантов-обменников. - **RUB → USDT** — обратный поток: клиент платит рубли по СБП, X-Hub отправляет USDT на крипто-кошелёк клиента. **Создаётся X-Hub для тебя по запросу** — публичного API для этого направления нет. Мерчант видит статусы через те же webhooks `exchange.status_changed`. **Доступно только для мерчантов** с настроенной поддержкой нужного направления (сообщается при подключении). > **RUB → USDT настраивается X-Hub по твоему запросу.** Если ты работаешь как обменник и тебе нужно чтобы клиенты платили RUB и получали USDT — напиши менеджеру X-Hub, мы создадим обмен от твоего имени. Статус-переходы узнаешь через тот же webhook `exchange.status_changed`. ## Жизненный цикл обмена ``` USDT → RUB (создаётся мерчантом): awaiting_usdt → usdt_received → rub_sending → completed ↓ partial (недополнение USDT — ждём доплату) ↓ expired (таймаут 15 минут) RUB → USDT (создаётся X-Hub): awaiting_rub → rub_received → usdt_sending → completed ↓ partial (клиент недоплатил RUB) ``` **Финальные статусы**: `completed`, `failed`, `expired`, `cancelled`. ### Подробные переходы | Из | В | Условие | |----|---|---------| | pending | awaiting_usdt | X-Hub получил крипто-адрес для депозита клиента (USDT → RUB) | | awaiting_usdt | usdt_received | Клиент прислал USDT на адрес | | awaiting_usdt | partial | Клиент прислал меньше запрошенной суммы | | awaiting_usdt / partial | expired | Прошло 15 минут, USDT не пришли полностью | | usdt_received | rub_sending | X-Hub инициировал SBP-выплату | | rub_sending | completed | Клиент получил RUB на карту | | pending | awaiting_rub | X-Hub создал SBP-инвойс для приёма RUB от клиента (RUB → USDT) | | awaiting_rub | rub_received | Клиент оплатил через СБП | | awaiting_rub / rub_received | partial | Клиент недоплатил | | rub_received | usdt_sending | X-Hub инициировал отправку USDT на кошелёк клиента (после hold-периода защиты от chargeback, ~5 мин) | | usdt_sending | completed | USDT подтверждены в блокчейне | | * | cancelled | Мерчант отменил обмен до получения средств | | * | failed | Техническая ошибка (bank rejected, validation) | ## Создать обмен USDT → RUB — POST /exchanges Доступно только для направления USDT → RUB. Обмены RUB → USDT создаются X-Hub по запросу — публичного POST-эндпоинта нет. ```bash curl -X POST https://api.x-hub.online/api/v1/exchanges \ -H "X-Api-Key: xh_live_..." \ -H "X-Api-Secret: ..." \ -H "Idempotency-Key: f47ac10b-58cc-4372-a567-0e02b2c3d479" \ -H "Content-Type: application/json" \ -d '{ "amount_usdt": "100.00", "client_phone": "+79991234567", "client_bank_id": "bank100000000111", "external_id": "EXCH-2026-001" }' ``` ### Headers | Header | Обяз. | | |---|---|---| | `X-Api-Key` | ✓ | | | `X-Api-Secret` | ✓ | | | `Idempotency-Key` | ✓ | UUID. Проверяется **до** чтения тела: без него — `400 MISSING_IDEMPOTENCY_KEY`, не-UUID — `400 VALIDATION_ERROR` | | `Content-Type` | ✓ | `application/json`. Без него тело не разбирается вовсе и запрос получает `400 VALIDATION_ERROR` — как будто ты не передал ни одного поля | ### Параметры тела | Поле | Обязательно | Описание | |------|-------------|----------| | `amount_usdt` | да | Сумма USDT которую клиент пришлёт. Decimal, до 8 знаков после точки, строго больше нуля. Минимум — чтобы итоговый RUB был ≥ 1000 | | `client_phone` | да | Телефон клиента, формат `+79991234567` (`+` и 10–15 цифр) | | `client_bank_id` | да | ID банка СБП, в который зачислять рубли клиенту. Формат: `bank<цифры>`. **Справочник доступных банков** запрашивайте у команды X-Hub — пока он передаётся списком, публичный endpoint будет в следующей версии API. Примеры: `bank100000000111` (Сбер), `bank100000000008` (Альфа-Банк). | | `external_id` | нет | Ваш ID обмена (до 40 символов, логируется в реквизитах СБП) | | `idempotency_key` | нет | **Legacy, не используйте.** Поле принимается ради обратной совместимости, но идемпотентность задаёт **заголовок** `Idempotency-Key` — при расхождении побеждает он, а без заголовка запрос вообще не дойдёт до тела | ### Ответ (201) ```json { "id": "exch_abc123-...", "status": "awaiting_usdt", "crypto_deposit_address": "TQr...", "network": "tron", "amount_usdt": "100.00", "amount_rub": "8245.00", "rate": "82.45", "expires_at": "2026-04-19T15:16:00Z", "external_id": "EXCH-2026-001" } ``` ### Что делать дальше 1. Покажите клиенту `crypto_deposit_address` и сумму `amount_usdt` 2. Клиент присылает USDT на адрес (сеть `tron`) 3. Банковская сеть определяет поступление → X-Hub фиксирует факт оплаты 4. X-Hub инициирует SBP-выплату клиенту на телефон+банк 5. Клиент получает `amount_rub` на карту 6. Вы получаете webhook `exchange.status_changed` со статусом `completed` + заработанные USDT на баланс мерчанта ### Ошибки | Код | HTTP | Причина | |-----|------|---------| | `MISSING_IDEMPOTENCY_KEY` | 400 | Не передан заголовок `Idempotency-Key`. Проверяется первым — до тела запроса | | `VALIDATION_ERROR` | 400 | Невалидное тело. **Самая частая причина — забытый `Content-Type: application/json`**: без него тело не разбирается и все поля выглядят отсутствующими | | `FEATURE_NOT_ENABLED` | 400 | У вас не подключён USDT→RUB обмен — свяжитесь с X-Hub | | `FEATURE_DISABLED` | 503 | Направление обменов **временно недоступно у всех мерчантов** (не про твой аккаунт). Это не ошибка запроса — повтори позже; сроки уточняй у команды | | `CLIENT_NOT_REGISTERED` | 422 | Клиент не прошёл KYC — запустите [/kyc/register](./kyc.md#kyc-register) | | `CLIENT_BLOCKED` | 403 | Клиент заблокирован платёжной системой | | `RATE_INVALID` | 502 | Не удалось получить курс для обмена — повторите через 30–60 сек | | `PROVIDER_UNAVAILABLE` | 503 | Временная ошибка внешней платёжной системы — повторите через минуту | | `EXCHANGE_IN_PROGRESS` | 409 | У клиента уже есть активный обмен — дождитесь завершения | | `AMOUNT_TOO_LOW` | 400 | Итоговый `amount_rub` < 1000 ₽ | | `AMOUNT_TOO_HIGH` | 400 | `amount_rub` > 1 000 000 ₽ | | `IDEMPOTENCY_TERMINAL` | 409 | Обмен с этим ключом идемпотентности уже завершён — используйте новый ключ | !!! warning "`FEATURE_NOT_ENABLED` (400) и `FEATURE_DISABLED` (503) — разные вещи" `400 FEATURE_NOT_ENABLED` — опция обменов не подключена **твоему аккаунту**. Само по себе не пройдёт: нужно обращение в X-Hub. `503 FEATURE_DISABLED` — направление временно недоступно **у всех** мерчантов. Твоя интеграция исправна; ретрай с тем же `Idempotency-Key` безопасен. Не пиши в поддержку по первому такому ответу — сначала подожди. ## Получить обмен — GET /exchanges/:id ```bash curl https://api.x-hub.online/api/v1/exchanges/exch_abc123 \ -H "X-Api-Key: xh_live_..." \ -H "X-Api-Secret: ..." ``` Ответ: ```json { "id": "exch_abc123", "status": "completed", "amount_usdt": "100.00", "amount_usdt_received": "100.00", "amount_rub": "8000.00", "rate": "80.00", "crypto_deposit_address": "TR...", "network": "tron", "client_phone": "+79990001234", "expires_at": "2026-04-20T11:00:00Z", "completed_at": "2026-04-20T10:15:00Z", "external_id": "order-42", "created_at": "2026-04-20T10:00:00Z" } ``` Поля `amount_usdt_received`, `completed_at` — `null` пока обмен не в финальном статусе. ## Список обменов — GET /exchanges ``` GET /exchanges?page=1&per_page=20&status=completed ``` Параметры: - `page` — номер страницы (1+) - `per_page` — размер (1-100, default 20) - `status` — фильтр по статусу: `pending`, `awaiting_usdt`, `usdt_received`, `rub_sending`, `completed`, `expired`, `failed`, `cancelled`, `partial` (регистр не важен). Незнакомое значение — `400 VALIDATION_ERROR` с перечнем допустимых, а не «фильтр пропущен» ## Отменить обмен — POST /exchanges/:id/cancel Работает только для `awaiting_usdt` / `pending`. После получения USDT отменить нельзя. ```bash curl -X POST https://api.x-hub.online/api/v1/exchanges/exch_abc123/cancel \ -H "X-Api-Key: xh_live_..." \ -H "X-Api-Secret: ..." ``` Ответ `200`: ```json { "status": "cancelled" } ``` ## Клиенты обмена — GET /exchange-clients { #get-exchange-clients } Список клиентов, которых ты уже заводил, с состоянием их проверки. Нужен, чтобы до создания обмена понять, проходил ли клиент KYC, и не гонять его по кругу. ```bash curl "https://api.x-hub.online/api/v1/exchange-clients?q=79991234567&kyc_status=verified" \ -H "X-Api-Key: xh_live_..." \ -H "X-Api-Secret: ..." ``` Параметры: - `page` — номер страницы (1+, макс `250000`) - `per_page` — размер (1–100, default 20) - `kyc_status` — фильтр: `pending`, `verified`, `rejected`, `blocked` - `q` — поиск **по телефону**. Всё, кроме цифр, отбрасывается: `+7 (999) 12` превращается в `799912` и ищется как подстрока. Меньше трёх цифр после отбрасывания — вернётся пустой список, а не вся таблица Ответ `200`: ```json { "data": [ { "id": "3f2b8c1a-9d4e-4a7b-8c5d-1e2f3a4b5c6d", "phone": "+79991234567", "nickname": "Иван", "telegram_user_id": 123456789, "kyc_status": "verified", "kyc_verified_at": "2026-08-01T10:00:00.000Z", "kyc_details": { "level": 2, "passport_state": "passed", "address_state": "passed" }, "client_ref": null, "created_at": "2026-07-28T09:12:00.000Z", "updated_at": "2026-08-01T10:00:00.000Z" } ], "pagination": { "page": 1, "per_page": 20, "total": 1 } } ``` `kyc_status` выводится из состояния клиента: заблокирован — `blocked`, есть дата проверки — `verified`, отказ по документу — `rejected`, всё остальное — `pending`. В `kyc_details` то же самое подробнее: `failed` в `passport_state` или `address_state` и есть тот самый отказ. `client_ref` — номер счёта клиента, если он у нас появился; иначе `null`. !!! note "Это не адресная книга" Ручка знает только про состояние проверки и про то, когда клиент у нас завёлся. Поиска по имени здесь нет намеренно — имена, заметки и привязка к менеджеру живут в твоей системе, и дублировать их у нас незачем. `nickname` мы отдаём как есть, но не ищем по нему. ## Webhook exchange.status_changed X-Hub отправляет webhook на ваш `webhook_url` при финальных статусах (`completed`, `failed`, `expired`, `cancelled`). Промежуточные статусы (`usdt_received`, `rub_sending`, `partial`) — внутренние. ```json { "event": "exchange.status_changed", "exchange_id": "exch_abc123-...", "external_id": "EXCH-2026-001", "direction": "usdt_to_rub", "status": "completed", "previous_status": "rub_sending", "amount_usdt": "100.00", "amount_usdt_received": "100.00", "amount_rub": "8245.00", "rate": "82.45", "client_phone": "+79991234567", "crypto_deposit_address": "TQr...", "network": "tron", "expires_at": "2026-04-19T15:16:00Z", "timestamp": "2026-04-19T15:05:12Z" } ``` **Поле `direction`** — `usdt_to_rub` или `rub_to_usdt`. Для RUB→USDT direction статусы: `awaiting_rub`, `rub_received`, `usdt_sending`, `completed`. Для USDT→RUB остаются `awaiting_usdt`, `usdt_received`, `rub_sending`, `completed`. Используй direction чтобы не полагаться на угадывание по статусу. Верификация подписи — [как для платежей](../webhooks/index.md#verify-signature). Полный пример запроса (заголовки + тело) — [здесь](../webhooks/index.md#polnyy-primer-zaprosa). ## Экономика обмена Курс клиенту = базовый курс × (1 − `markup_percent`/100). Пример: - Базовый курс: 85 RUB/USDT - Ваш `markup_percent`: 3% - Клиенту: 85 × 0.97 = **82.45 RUB/USDT** - Клиент прислал 100 USDT → получил **8 245 RUB** - Платёж закрыт на 8 500 RUB (100 × 85 по базовому курсу) - Остаток **255 RUB** — ваш заработок, конвертируется в ≈ **3 USDT** и зачисляется на ваш баланс ## Важные ограничения - **Минимум** 1000 RUB, **максимум** 1 000 000 RUB на обмен - **Один активный** обмен на клиента (по `client_phone`) - **Таймаут** 15 минут — если USDT не получены полностью, статус → `expired` - **Сеть** USDT — только `tron` (TRC20) - **KYC обязателен** — клиент должен быть зарегистрирован через [/kyc/register](./kyc.md#kyc-register) ---