--- title: "Обменный терминал" url: "https://docs.x-hub.online/exchange-terminal/" description: "Приём рублей по СБП с обменом на USDT при оплате: выбор терминала, данные плательщика, создание платежа, вебхук, хэш перевода клиенту для спора и напоминание о нём." relatedDocuments: - "https://docs.x-hub.online/payments/" - "https://docs.x-hub.online/webhooks/" - "https://docs.x-hub.online/reference/errors/" --- # Обменный терминал: приём рублей по СБП с обменом на USDT { #exchange-terminal } ## Что это { #what-is-this } Терминал с обменом при оплате — способ принимать оплату клиентов в рублях по СБП, когда обмен происходит прямо при оплате: клиент платит ₽. Сумма платежа всегда задаётся в рублях: считать что-либо в USDT заранее не нужно. Терминал доступен на аккаунтах, для которых это включено, — напиши в X-Hub, если он тебе нужен. ## Интеграция по шагам { #integration } ### 1. Найди терминал — GET /payment-methods { #step-list } ```bash curl https://api.x-hub.online/api/v1/payment-methods \ -H "X-Api-Key: ..." -H "X-Api-Secret: ..." ``` ```json [ { "method_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "payment_method": "sbp", "label": "Терминал 1", "required_fields": ["client_phone", "payer"] } ] ``` Описание всех полей — [выбор терминала](../payments/index.md#terminal-selection); пока опция выбора не включена, запрос отвечает `403 METHOD_SELECTION_DISABLED`. `required_fields` показывает, какие данные плательщика твой терминал требует в каждом платеже. В примере выше это два значения: `client_phone` — телефон плательщика, `payer` — объект с ФИО и номером документа плательщика ([поля и правила](../payments/index.md#payer-object)). Список может быть и пустым — тогда требований нет. ### 2. Создай платёж — POST /payments { #step-create } **Прочитай `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`](../payments/index.md#payer-object). ```bash 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`: ```json { "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`). Общие лимиты запроса — в [описании запроса](../payments/index.md#payments). - Зачисление, как и везде, приходит в `amount_usdt` после расчёта. ### 3. Отдай клиенту ссылку оплаты { #step-link } `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. Узнай исход { #step-outcome } Исход приходит вебхуком `payment.status_changed`, и его всегда можно перечитать запросом `GET /api/v1/payments/:id`. В обоих — имя плательщика: ```json { "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` — отдельного события о появлении имени нет ([подробнее](../webhooks/index.md#webhook-payer-name)). - Как проверить подпись и построить приёмник — [Уведомления](../webhooks/index.md#verify-signature). ### 5. Передай хэш перевода клиенту { #step-delivery-proof } Когда клиент получил перевод USDT, передай хэш этой транзакции. Хэш подтверждает, что клиент получил перевод, и используется, если по платежу возникает спор. Платёж, расчёт и выплата его не ждут: ничего в них от хэша не зависит. Хэш можно передать по платежу в статусе `paid` или `settled`. Если хэш не передан, через 24, 48 и 72 часа после оплаты придёт напоминание — событие [`payment.delivery_proof_reminder`](../webhooks/index.md#webhook-delivery-proof-reminder); по нему можно передать хэш автоматически, примеры приёма и ответа — [там же](../webhooks/index.md#webhook-delivery-proof-reminder-examples). Напоминание только для удобства: платёж, расчёт и выплата не зависят от хэша. ```bash 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"}' ``` ```json { "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`. ## Статусы и особые случаи { #statuses } - **`paid`** — оплата подтверждена. - **`amount_mismatch`** — клиент заплатил не ту сумму или сумму оплаты не удалось подтвердить: **не выдавай клиенту USDT по этой сделке**. Платёж уходит на ручной разбор; после него может прийти ещё один вебхук — `paid` или `failed`. Деньги не теряются ([подробнее](../payments/index.md#edge-cases)). - **`expired`** — срок оплаты истёк, но статус **не терминальный**: поздняя оплата может перевести платёж в `paid` или `amount_mismatch`, и придёт ещё один вебхук. Не закрывай сделку намертво; окно спасения ограничено и не гарантировано для каждого способа оплаты ([подробнее](../payments/index.md#edge-case-expired)). - **`failed`** — окончательный отказ; причина — в поле `failure_reason` ([справочник причин](../payments/index.md#failure-reason)). ## Коды ошибок этого потока { #errors } | 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 ₽) или другое нарушение формата | Сверь сумму и запрос с [описанием полей](../payments/index.md#payments) | | 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`](../payments/index.md#payer-object) | | 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` и повтори запрос | Полный справочник и стратегия ретраев — [Коды ошибок](../reference/errors.md). ## Чек-лист перед запуском { #checklist } - [ ] Сумма платежа всегда в рублях и в границах 10 000–80 000 ₽. - [ ] Интеграция читает `required_fields` своего терминала и передаёт в каждом платеже все перечисленные поля; без них платёж не создаётся (`422 PAYER_DATA_REQUIRED`). - [ ] Если передаётся `payer`, его значения проходят [правила полей](../payments/index.md#payer-object): ФИО без цифр и запятых, номер документа из букв и цифр (5–32 знака), `kyc_completed_at` с часовым поясом и не позже текущего момента. - [ ] Ошибки `422` и `400` по данным плательщика разбираются по `error.code` и `details`, исправленный запрос повторяется с тем же `Idempotency-Key`. - [ ] Вебхук проверяется по подписи ([`X-Webhook-Signature`](../webhooks/index.md#verify-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`.