Обменный терминал: приём рублей по СБП с обменом на USDT
Что это
Терминал с обменом при оплате — способ принимать оплату клиентов в рублях по СБП, когда обмен происходит прямо при оплате: клиент платит ₽. Сумма платежа всегда задаётся в рублях: считать что-либо в USDT заранее не нужно.
Терминал доступен на аккаунтах, для которых это включено, — напиши в X-Hub, если он тебе нужен.
Интеграция по шагам
1. Найди терминал — GET /payment-methods
[
{ "method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "payment_method": "sbp", "label": "Терминал 1", "required_fields": ["client_phone", "payer"] }
]
Описание всех полей — выбор терминала; пока опция выбора не включена, запрос отвечает 403 METHOD_SELECTION_DISABLED.
required_fields показывает, какие данные плательщика твой терминал требует в каждом платеже. В примере выше это два значения: client_phone — телефон плательщика, payer — объект с ФИО и номером документа плательщика (поля и правила). Список может быть и пустым — тогда требований нет.
2. Создай платёж — POST /payments
Прочитай required_fields своего терминала и передавай в каждом платеже все перечисленные там поля. Если в списке client_phone и payer, оба обязательны в каждом платеже, а платёж без них не создаётся: запрос отвечает 422 PAYER_DATA_REQUIRED, в details.missing_fields — недостающие поля. Если список пуст, эти поля необязательны, но их всё равно можно передавать.
| Что передать | Поле запроса | Внутри объекта |
|---|---|---|
| Телефон плательщика | client_phone — формат E.164: + и 10–15 цифр |
— |
| ФИО плательщика | payer.full_name |
обязательно |
| Номер документа плательщика | payer.document_number |
обязательно |
| Идентификатор проверки клиента в твоей системе | payer.kyc_reference |
нет |
| Время завершения проверки клиента | payer.kyc_completed_at — ISO 8601 с часовым поясом |
нет |
Допустимые знаки, длины и формат каждого поля — в описании объекта payer.
curl -X POST https://api.x-hub.online/api/v1/payments \
-H "X-Api-Key: xh_live_..." -H "X-Api-Secret: ..." \
-H "Idempotency-Key: 5b1f0c1e-9a3b-4c2d-8e4f-0123456789ab" \
-H "Content-Type: application/json" \
-d '{
"amount_rub": "10000.00",
"payment_method": "sbp",
"method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"external_id": "deal-771",
"client_phone": "+79990001234",
"payer": {
"full_name": "Тестов Тест Тестович",
"document_number": "0000 000000"
},
"metadata": {"deal_id": "771"}
}'
Ответ 201:
{
"id": "pay_9a2f6c3e-1b4d-4e8a-9c2f-7d5e4b3a2c10",
"status": "pending",
"amount_rub": "10000.00",
"actual_amount_rub": null,
"amount_usdt": null,
"external_id": "deal-771",
"payment_method": "sbp",
"method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"payment_url": "https://pay.x-hub.online/r/9a2f6c3e-1b4d-4e8a-9c2f-7d5e4b3a2c10",
"expires_at": "2026-10-05T13:15:00Z",
"refund_pending": false,
"metadata": {"deal_id": "771"}
}
- Сумма — только
amount_rub, в рублях. У терминала свои границы суммы: сейчас от 10 000 до 80 000 ₽ включительно. Вне границ —400 VALIDATION_ERROR, платёж не создаётся; действующие границы названы вmessageответа (amount_rub must be between 10000 and 80000 for this payment method). Общие лимиты запроса — в описании запроса. - Зачисление, как и везде, приходит в
amount_usdtпосле расчёта.
3. Отдай клиенту ссылку оплаты
payment_url — ссылка на домене X-Hub (https://pay.x-hub.online/r/…): она ведёт на страницу оплаты. Отдай её клиенту как есть — редиректом, кнопкой или в сообщении; не парси её и не рисуй по ней свой QR.
- Срок оплаты — поле
expires_at(по умолчанию 15 минут от создания). Статус платежа бери из API или вебхука, а не со страницы оплаты. GET /payments/:id/sbp-linkдля такого терминала отвечает"sbp_link": null: НСПК-ссылки у такого платежа нет, свой QR по нему не нарисовать — клиент платит черезpayment_url.
4. Узнай исход
Исход приходит вебхуком payment.status_changed, и его всегда можно перечитать запросом GET /api/v1/payments/:id. В обоих — имя плательщика:
{
"event": "payment.status_changed",
"payment_id": "pay_9a2f6c3e-1b4d-4e8a-9c2f-7d5e4b3a2c10",
"external_id": "deal-771",
"status": "paid",
"previous_status": "pending",
"amount_rub": "10000.00",
"actual_amount_rub": "10000.00",
"amount_usdt": null,
"payer_name": "Иван Иванович Петров",
"payment_method": "sbp",
"refund_pending": false,
"timestamp": "2026-10-05T13:07:41Z",
"metadata": {"deal_id": "771"}
}
payer_name— имя плательщика, как его сообщил банк. Поля может не быть: оно приходит не у каждого платежа; если в уведомленииpaidего нет, перечитай платёжGET /api/v1/payments/:id— отдельного события о появлении имени нет (подробнее).- Как проверить подпись и построить приёмник — Уведомления.
5. Передай хэш перевода клиенту
Когда клиент получил перевод USDT, передай хэш этой транзакции. Хэш подтверждает, что клиент получил перевод, и используется, если по платежу возникает спор. Платёж, расчёт и выплата его не ждут: ничего в них от хэша не зависит.
Хэш можно передать по платежу в статусе paid или settled. Если хэш не передан, через 24, 48 и 72 часа после оплаты придёт напоминание — событие payment.delivery_proof_reminder; по нему можно передать хэш автоматически, примеры приёма и ответа — там же. Напоминание только для удобства: платёж, расчёт и выплата не зависят от хэша.
curl -X POST https://api.x-hub.online/api/v1/payments/pay_9a2f6c3e-1b4d-4e8a-9c2f-7d5e4b3a2c10/delivery-proof \
-H "X-Api-Key: xh_live_..." -H "X-Api-Secret: ..." \
-H "Content-Type: application/json" \
-d '{"network": "TRC20", "tx_hash": "b1f0c4d2a7e83a6f5d9c0e1b2a3f4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d"}'
{
"network": "TRC20",
"tx_hash": "b1f0c4d2a7e83a6f5d9c0e1b2a3f4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d",
"status": "accepted"
}
network— сеть перевода:TRC20,ERC20илиBEP20. Другое значение —400 VALIDATION_ERROR.tx_hash— хэш транзакции, до 200 символов. Формат зависит от сети: дляTRC20— 64 шестнадцатеричных символа (префикс0x, если он есть, отбрасывается); дляERC20иBEP20—0xи 64 шестнадцатеричных символа (без0xмы добавим его сами). Регистр не важен: в ответе хэш приходит в нижнем регистре. Хэш не того формата —400 VALIDATION_ERRORсdetails.pathравнымtx_hash.- Повторная отправка того же хэша в той же сети безопасна: ответ тот же, ничего не дублируется.
- Прочитать текущее состояние можно запросом
GET /api/v1/payments/:id/delivery-proof. Ответ в том же формате. Пока хэш не передан, он такой:{"network": null, "tx_hash": null, "status": "waiting"}.
Статусы. Поле status показывает, что сейчас с хэшем:
status |
Что значит | Что делать |
|---|---|---|
waiting |
Хэш по платежу не передан | Передай хэш, когда клиент получит перевод |
accepted |
Хэш принят, проверка ещё не началась | Ничего |
in_review |
Хэш на проверке | Ничего. Если статус не меняется дольше суток, отправь тот же хэш ещё раз |
awaiting_confirmations |
Перевод пока не найден или не набрал подтверждений, проверка продолжается | Ничего |
confirmed |
Перевод по хэшу найден и подтверждён | Ничего |
confirmed_amount_differs |
Перевод по хэшу найден, но его сумма не совпала с ожидаемой | Проверь, тот ли хэш ты передал; при вопросах напиши в X-Hub |
rejected |
Хэш не принят; причина — в поле reason |
Исправь по причине и передай хэш заново |
Ни один статус не меняет статус платежа, расчёт и выплату.
Причины отказа. При rejected в ответе есть поле reason:
reason |
Что значит | Что делать |
|---|---|---|
not_found_or_unconfirmed |
Перевод по хэшу не найден или не набрал подтверждений за время проверки | Убедись, что перевод подтверждён в сети, и передай хэш заново |
already_used |
Этот хэш уже использован в другой операции | Передай хэш перевода именно этому клиенту по этому платежу |
invalid |
Хэш не принят как заданный | Проверь сеть и хэш и передай заново |
Причина остаётся в ответе GET, пока ты не передашь другой хэш.
Замена хэша. Пока статус accepted, in_review или awaiting_confirmations, хэш можно заменить: передай другой хэш или тот же в другой сети, прежний перестанет действовать. После rejected передай хэш заново. После confirmed и confirmed_amount_differs замена через API закрыта: запрос с другим хэшем отвечает 409 DELIVERY_PROOF_LOCKED. Если подтверждённый хэш передан по ошибке, напиши в X-Hub.
Тестовый режим. По платежам тестового режима хэш не принимается: оба запроса отвечают 400 FEATURE_NOT_ENABLED. Платёж другого режима для твоих ключей — 404 PAYMENT_NOT_FOUND.
Статусы и особые случаи
paid— оплата подтверждена.amount_mismatch— клиент заплатил не ту сумму или сумму оплаты не удалось подтвердить: не выдавай клиенту USDT по этой сделке. Платёж уходит на ручной разбор; после него может прийти ещё один вебхук —paidилиfailed. Деньги не теряются (подробнее).expired— срок оплаты истёк, но статус не терминальный: поздняя оплата может перевести платёж вpaidилиamount_mismatch, и придёт ещё один вебхук. Не закрывай сделку намертво; окно спасения ограничено и не гарантировано для каждого способа оплаты (подробнее).failed— окончательный отказ; причина — в полеfailure_reason(справочник причин).
Коды ошибок этого потока
| HTTP | Код | Когда | Что делать |
|---|---|---|---|
| 403 / 400 | METHOD_SELECTION_DISABLED |
Опция выбора терминала не включена: 403 на GET /payment-methods, 400 на method_id в POST /payments |
Попроси X-Hub включить выбор терминала |
| 400 | METHOD_NOT_AVAILABLE |
method_id нет в твоём списке |
Возьми method_id из GET /payment-methods |
| 400 | VALIDATION_ERROR |
Сумма вне границ терминала (10 000–80 000 ₽) или другое нарушение формата | Сверь сумму и запрос с описанием полей |
| 400 | AMOUNT_BELOW_MINIMUM |
Сумма ниже общего минимума (30 ₽) | Увеличь сумму — точный минимум в message |
| 422 | PAYER_DATA_REQUIRED |
Терминал требует поля плательщика из своего required_fields, их нет в запросе. В details.missing_fields — недостающие имена, например ["client_phone", "payer"], ["client_phone"] или ["payer"] |
Повтори с тем же Idempotency-Key, добавив поля из details.missing_fields |
| 400 / 422 | VALIDATION_ERROR |
client_phone не в формате E.164 (422); объект payer неверной формы: не объект, нет full_name или document_number, лишний ключ (400); значение в payer не прошло правила: допустимые знаки, длина, формат или дата kyc_completed_at (422) |
Исправь поле по details[].path и повтори с тем же Idempotency-Key; правила — в описании объекта payer |
| 400 / 503 | PAYMENT_ROUTE_UNAVAILABLE |
Нет рабочего маршрута (400) или его не удалось проверить из-за временного сбоя (503) | 400 — напиши в X-Hub; 503 — повтори через 30–60 сек с тем же Idempotency-Key |
| 422 | PAYMENT_UNAVAILABLE |
Приём для аккаунта сейчас недоступен | Ретрай не поможет — напиши в X-Hub |
| 502 | PROVIDER_ERROR |
Создание платежа завершилось без определённого исхода | Повтор с тем же Idempotency-Key вернёт ту же ошибку — создай платёж с новым ключом |
| 404 | PAYMENT_NOT_FOUND |
Платёж не найден или не принадлежит твоему аккаунту | Проверь id |
| 400 | FEATURE_NOT_ENABLED |
POST или GET /payments/:id/delivery-proof по платежу, по которому хэш не принимается, в том числе по платежу тестового режима |
Не передавай хэш по такому платежу |
| 409 | INVALID_STATUS |
POST /payments/:id/delivery-proof по платежу не в статусе paid или settled |
Передай хэш после оплаты |
| 409 | DELIVERY_PROOF_LOCKED |
Хэш уже подтверждён, замена через API закрыта | Если хэш передан по ошибке, напиши в X-Hub |
| 409 | DELIVERY_PROOF_CONFLICT |
Хэш изменён параллельным запросом | Прочитай состояние GET и повтори запрос |
Полный справочник и стратегия ретраев — Коды ошибок.
Чек-лист перед запуском
- [ ] Сумма платежа всегда в рублях и в границах 10 000–80 000 ₽.
- [ ] Интеграция читает
required_fieldsсвоего терминала и передаёт в каждом платеже все перечисленные поля; без них платёж не создаётся (422 PAYER_DATA_REQUIRED). - [ ] Если передаётся
payer, его значения проходят правила полей: ФИО без цифр и запятых, номер документа из букв и цифр (5–32 знака),kyc_completed_atс часовым поясом и не позже текущего момента. - [ ] Ошибки
422и400по данным плательщика разбираются поerror.codeиdetails, исправленный запрос повторяется с тем жеIdempotency-Key. - [ ] Вебхук проверяется по подписи (
X-Webhook-Signature), обработка идемпотентна. - [ ]
amount_mismatchне выдаёт клиенту USDT; после ручного разбора готов ещё один вебхук. - [ ]
expiredне закрывает сделку необратимо: поздняя оплата может поднять платёж вpaid. - [ ] Если передаёшь хэш перевода клиенту (напоминание о нём приходит событием
payment.delivery_proof_reminder, можно подключить автоматическую передачу): запросPOST /payments/:id/delivery-proof— послеpaid; статус читается запросомGET,rejectedразбирается по полюreason. Платёж, расчёт и выплата от хэша не зависят. - [ ] Строгая схема вебхука принимает необязательную строку
payer_name; отсутствиеpayer_nameвpaidдобирается запросомGET /payments/:id.