Перейти к содержанию

Обмены (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-эндпоинта нет.

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)

{
  "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
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 Обмен с этим ключом идемпотентности уже завершён — используйте новый ключ

FEATURE_NOT_ENABLED (400) и FEATURE_DISABLED (503) — разные вещи

400 FEATURE_NOT_ENABLED — опция обменов не подключена твоему аккаунту. Само по себе не пройдёт: нужно обращение в X-Hub.

503 FEATURE_DISABLED — направление временно недоступно у всех мерчантов. Твоя интеграция исправна; ретрай с тем же Idempotency-Key безопасен. Не пиши в поддержку по первому такому ответу — сначала подожди.

Получить обмен — GET /exchanges/:id

curl https://api.x-hub.online/api/v1/exchanges/exch_abc123 \
  -H "X-Api-Key: xh_live_..." \
  -H "X-Api-Secret: ..."

Ответ:

{
  "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 отменить нельзя.

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:

{ "status": "cancelled" }

Клиенты обмена — GET /exchange-clients

Список клиентов, которых ты уже заводил, с состоянием их проверки. Нужен, чтобы до создания обмена понять, проходил ли клиент KYC, и не гонять его по кругу.

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:

{
  "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.

Это не адресная книга

Ручка знает только про состояние проверки и про то, когда клиент у нас завёлся. Поиска по имени здесь нет намеренно — имена, заметки и привязка к менеджеру живут в твоей системе, и дублировать их у нас незачем. nickname мы отдаём как есть, но не ищем по нему.

Webhook exchange.status_changed

X-Hub отправляет webhook на ваш webhook_url при финальных статусах (completed, failed, expired, cancelled). Промежуточные статусы (usdt_received, rub_sending, partial) — внутренние.

{
  "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 чтобы не полагаться на угадывание по статусу.

Верификация подписи — как для платежей. Полный пример запроса (заголовки + тело) — здесь.

Экономика обмена

Курс клиенту = базовый курс × (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