Обмены (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"
}
Что делать дальше
- Покажите клиенту
crypto_deposit_addressи суммуamount_usdt - Клиент присылает USDT на адрес (сеть
tron) - Банковская сеть определяет поступление → X-Hub фиксирует факт оплаты
- X-Hub инициирует SBP-выплату клиенту на телефон+банк
- Клиент получает
amount_rubна карту - Вы получаете 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
Параметры:
- 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:
Клиенты обмена — 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,blockedq— поиск по телефону. Всё, кроме цифр, отбрасывается:+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