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

Sandbox (тестовый режим)

Sandbox позволяет протестировать интеграцию до реальных платежей — без реальных денег и вызовов внешних платёжных систем. Все endpoints работают как в prod, но платежи симулируются: по умолчанию автоматически (magic-токены, см. ниже), а вручную — кнопками на песочной странице оплаты, которая открывается по payment_url. Админ X-Hub для этого не нужен.

Как получить

Попросите админа X-Hub создать аккаунт — новый аккаунт стартует в тестовом режиме. После этого на ваш email придёт magic-link → зайдите в кабинет → нажмите «Сгенерировать API-ключи». Sandbox-ключи имеют префикс xh_test_ (production — xh_live_). Процесс тот же что в «Как получить ключи» выше, отличается только префикс.

Отличия от production

Поле Sandbox Production
API key prefix xh_test_... xh_live_...
Платёжная система Не вызывается Реальная
Payment URL https://app.x-hub.online/sandbox/pay/{payment_id} — рабочая песочная страница оплаты (не заглушка), см. ниже. Точный адрес всегда приходит в payment_url Реальная ссылка СБП
Смена статуса Авто-симуляция magic-токенами (см. ниже) или кнопками на песочной странице оплаты — в любой момент, без участия админа X-Hub Автоматически по факту оплаты
Реальные USDT Нет, только на тестовом балансе Да
Settlement (T+1) Пропускается Выполняется
Webhook мерчанту Присылается Присылается
sandbox в API response true false (всегда присутствует)
sandbox в webhook payload true поле отсутствует

Обмены и KYC в sandbox

  • Обмены (POST /api/v1/exchanges) полностью работают и в sandbox — на изолированном тестовом балансе. Если опция обменов не включена на аккаунте — 400 FEATURE_NOT_ENABLED (как и в production).
  • KYC (/api/v1/kyc/*) — зависит от конфигурации аккаунта: у части аккаунтов sandbox-KYC отключён (400 SANDBOX_NOT_SUPPORTED), у остальных работает.
  • Подписки (/api/v1/subscriptions) работают в sandbox, но с важным расхождением с боем — списания при привязке мандата в песочнице не происходит, а сами списания становятся paid автоматически через 30 секунд. Обязательно прочитай Подписки → Sandbox перед тем как переносить интеграцию в бой. Отдельно включать подписки для песочницы не нужно. В боевом режиме подписки сейчас недоступны — см. Подписки.

Flow

  1. Создаёте платёж через API как в prod: POST /api/v1/payments
  2. В ответе видите "sandbox": true и payment_url на песочную страницу оплаты
  3. Дальше — два пути, они друг друга не исключают:
    • ничего не делать: sandbox сам переведёт платёж в терминальный статус (по умолчанию — happy-path paid через 30 секунд, см. magic-токены);
    • открыть payment_url и нажать нужную кнопку — статус сменится немедленно. Кто сработал первым, тот и выиграл: второй путь после этого просто ничего не делает (idempotent).
  4. Ваш webhook endpoint получает payload и обрабатывает как обычно.
  5. Проверяете ваш обработчик, баланс, интеграцию.

Magic-токены — сценарии ошибок

Чтобы протестировать не только happy-path, передавайте ключевое слово в external_id или description. Sandbox опознаёт его и автоматически триггерит нужный статус:

Ключевое слово Delay Итог
(ничего) 30 сек paid ← default
xhub_test_paid 5 сек paid (быстро)
xhub_test_fail 5 сек failed c failure_reason: "insufficient_funds"
xhub_test_expire 60 сек expired
xhub_test_mismatch 5 сек amount_mismatch (фактическая сумма = amount_rub - 1)
xhub_test_manual — авто-симуляция выключена: статус двигаешь сам кнопками на песочной странице оплаты

xhub_test_fail намеренно отдаёт самую частую и самую дорогую причину — «не хватило денег». Это тот случай, где подписку отменять не надо, а надо повторить списание через день-два; написать эту ветку удобнее здесь, чем на живых подписчиках. Полный список кодов — причина отказа.

Поиск токена — case-insensitive и word-boundary (regex \b<token>\b): токен должен быть отдельным «словом», отделённым пробелом, дефисом или пунктуацией. Например description: "Заказ #42 — xhub_test_fail" сработает, а description: "prexhub_test_failpost" — нет (токен склеен с другими символами).

Если токенов в external_id/description несколько, побеждает первый по порядку из таблицы (paid → fail → expire → mismatch); xhub_test_manual имеет приоритет над всеми и просто отключает автомат.

Песочная страница оплаты

payment_url песочного платежа ведёт на рабочую песочную страницу оплаты на app.x-hub.online. Открой его в браузере — увидишь сумму, статус, обратный отсчёт до expires_at и три кнопки:

Кнопка Итоговый статус
Имитировать успешную оплату paid
Имитировать ошибку failed
Имитировать истечение срока expired

Каждое нажатие проходит тот же путь, что и настоящая оплата, — то есть уведомление придёт настоящее, с подписью и с sandbox: true. Страница сама опрашивает статус раз в 3 секунды, поэтому смена статуса авто-симуляцией видна на ней без перезагрузки.

Статус amount_mismatch кнопкой не ставится — его даёт только magic-токен xhub_test_mismatch (или админ X-Hub по запросу).

Те же действия из кода: /api/sandbox/*

Страница — тонкая обёртка над двумя публичными endpoint'ами. Их можно дёргать напрямую из автотестов: авторизация не нужна, заголовки X-Api-Key / X-Api-Secret / Idempotency-Key не передаются.

# Прочитать состояние песочного платежа
curl https://api.x-hub.online/api/sandbox/payment-info/550e8400-e29b-41d4-a716-446655440000

# Перевести его в нужный статус
curl -X POST https://api.x-hub.online/api/sandbox/simulate/550e8400-e29b-41d4-a716-446655440000 \
  -H "Content-Type: application/json" \
  -d '{"action": "paid"}'

В URL — «голый» UUID, без префикса pay_

В остальном API идентификатор платежа выглядит как pay_550e8400-…. Эти два endpoint'а принимают только UUID-часть — префикс надо отрезать.

GET /api/sandbox/payment-info/{id} → 200:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "PENDING",
  "amount_rub": "500.00",
  "is_sandbox": true,
  "created_at": "2026-04-20T10:00:00.000Z",
  "expires_at": "2026-04-20T10:15:00.000Z",
  "paid_at": null
}

POST /api/sandbox/simulate/{id} — тело {"action": "paid" | "failed" | "expired"} → 200:

{ "ok": true, "previous_status": "PENDING", "new_status": "PAID" }

Ошибки:

HTTP Code Причина
400 VALIDATION_ERROR action не одно из paid / failed / expired
404 NOT_FOUND Платежа нет или он не песочный. Боевые платежи через эти ручки недоступны в принципе
409 INVALID_TRANSITION Переход недопустим из текущего статуса (например, платёж уже PAID)
409 STATE_CHANGED Статус поменялся параллельно (авто-симуляция успела раньше) — перечитай payment-info

Не встраивай /api/sandbox/* в боевой код

Эти endpoint'ы существуют только для песочницы и намеренно не требуют авторизации: payment_id — UUIDv4, а боевые платежи отсекаются по is_sandbox. В production аналога нет — платёж туда двигает сама платёжная система.

Практическое следствие: любой, кому известен id твоего песочного платежа, может перевести его в paid. Реальных денег за этим нет, но не используй песочные платежи как доказательство оплаты.

Webhook payload

Тот же формат что в prod, плюс дополнительное поле sandbox: true:

{
  "event": "payment.status_changed",
  "payment_id": "pay_abc123",
  "external_id": "ORD-001",
  "status": "paid",
  "previous_status": "pending",
  "amount_rub": "1000.00",
  "actual_amount_rub": null,
  "amount_usdt": null,
  "payment_method": "sbp",
  "metadata": null,
  "sandbox": true,
  "timestamp": "2026-04-20T10:00:00Z"
}

Подпись через заголовок X-Webhook-Signature: sha256=<hex> — та же HMAC-SHA256, что в production.

Поле amount_usdt в sandbox

В sandbox amount_usdt всегда null — реальная конвертация RUB→USDT не выполняется, settlement пропускается (см. таблицу «Отличия от production» выше). Тестируй только обработку статусов и не опирайся на значение amount_usdt в sandbox.

Переключение Тест↔Боевой

Sandbox и production живут в одном аккаунте — это два слота ключей и переключатель режима, которым ты управляешь сам:

  1. Подтверждаешь, что интеграция работает в sandbox (ключи xh_test_...)
  2. В кабинете генерируешь боевые ключи xh_live_... (второй слот; тестовые ключи никуда не пропадают)
  3. Нажимаешь «Активировать боевой режим» — аккаунт переходит в боевой режим
  4. Меняешь ключи в своём коде: xh_test_ → xh_live_
  5. Вернуться в тест можно в любой момент кнопкой «Вернуться в тест» — и обратно, сколько угодно раз

В каждый момент активен один режим. Запрос с ключом не-активного режима → 401 с кодом MODE_MISMATCH и человекочитаемым сообщением, какой режим сейчас активен. Тестовые и боевые данные (платежи, баланс, идемпотентность) полностью изолированы друг от друга.