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

Обменный терминал: приём рублей по СБП с обменом на USDT

Что это

Терминал с обменом при оплате — способ принимать оплату клиентов в рублях по СБП, когда обмен происходит прямо при оплате: клиент платит ₽. Сумма платежа всегда задаётся в рублях: считать что-либо в USDT заранее не нужно.

Терминал доступен на аккаунтах, для которых это включено, — напиши в X-Hub, если он тебе нужен.

Интеграция по шагам

1. Найди терминал — GET /payment-methods

curl https://api.x-hub.online/api/v1/payment-methods \
  -H "X-Api-Key: ..." -H "X-Api-Secret: ..."
[
  { "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 после расчёта.

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.