--- title: "Песочница" url: "https://docs.x-hub.online/start/sandbox/" description: "Тестовый режим: отдельные ключи, симуляция статусов, чем песочница отличается от боя." relatedDocuments: - "https://docs.x-hub.online/start/quickstart/" - "https://docs.x-hub.online/start/auth/" - "https://docs.x-hub.online/subscriptions/manage/" - "https://docs.x-hub.online/subscriptions/" - "https://docs.x-hub.online/payments/" --- # Sandbox (тестовый режим) Sandbox позволяет **протестировать интеграцию до реальных платежей** — без реальных денег и вызовов внешних платёжных систем. Все endpoints работают как в prod, но платежи симулируются: по умолчанию автоматически (magic-токены, см. ниже), а вручную — кнопками на [песочной странице оплаты](#sandbox-pay-page), которая открывается по `payment_url`. Админ X-Hub для этого не нужен. ## Как получить Попросите админа X-Hub создать аккаунт — новый аккаунт стартует в тестовом режиме. После этого на ваш email придёт magic-link → зайдите в кабинет → нажмите «Сгенерировать API-ключи». Sandbox-ключи имеют префикс **`xh_test_`** (production — `xh_live_`). Процесс тот же что в [«Как получить ключи»](./quickstart.md#kak-poluchit-klyuchi) выше, отличается только префикс. ## Отличия от production | Поле | Sandbox | Production | |------|---------|------------| | API key prefix | `xh_test_...` | `xh_live_...` | | Платёжная система | Не вызывается | Реальная | | Payment URL | `https://app.x-hub.online/sandbox/pay/{payment_id}` — **рабочая песочная страница оплаты** (не заглушка), см. [ниже](#sandbox-pay-page). Точный адрес всегда приходит в `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](../subscriptions/manage.md#sandbox-podpiski) перед тем как переносить интеграцию в бой. Отдельно включать подписки для песочницы не нужно. В боевом режиме подписки сейчас недоступны — см. [Подписки](../subscriptions/index.md#subscriptions). ## 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-токены — сценарии ошибок { #magic-tokens } Чтобы протестировать не только 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` | — | авто-симуляция **выключена**: статус двигаешь сам кнопками на [песочной странице оплаты](#sandbox-pay-page) | `xhub_test_fail` намеренно отдаёт самую частую и самую дорогую причину — «не хватило денег». Это тот случай, где подписку отменять **не** надо, а надо повторить списание через день-два; написать эту ветку удобнее здесь, чем на живых подписчиках. Полный список кодов — [причина отказа](../payments/index.md#failure-reason). Поиск токена — **case-insensitive** и **word-boundary** (regex `\b\b`): токен должен быть отдельным «словом», отделённым пробелом, дефисом или пунктуацией. Например `description: "Заказ #42 — xhub_test_fail"` сработает, а `description: "prexhub_test_failpost"` — нет (токен склеен с другими символами). Если токенов в `external_id`/`description` несколько, побеждает первый по порядку из таблицы (`paid` → `fail` → `expire` → `mismatch`); `xhub_test_manual` имеет приоритет над всеми и просто отключает автомат. ### Песочная страница оплаты { #sandbox-pay-page } `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` не передаются. ```bash # Прочитать состояние песочного платежа 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"}' ``` !!! warning "В URL — «голый» UUID, без префикса `pay_`" В остальном API идентификатор платежа выглядит как `pay_550e8400-…`. Эти два endpoint'а принимают **только** UUID-часть — префикс надо отрезать. `GET /api/sandbox/payment-info/{id}` → `200`: ```json { "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`: ```json { "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` | !!! danger "Не встраивай `/api/sandbox/*` в боевой код" Эти endpoint'ы существуют только для песочницы и намеренно не требуют авторизации: `payment_id` — UUIDv4, а боевые платежи отсекаются по `is_sandbox`. В production аналога нет — платёж туда двигает сама платёжная система. Практическое следствие: **любой, кому известен id твоего песочного платежа, может перевести его в `paid`**. Реальных денег за этим нет, но не используй песочные платежи как доказательство оплаты. ## Webhook payload Тот же формат что в prod, плюс **дополнительное поле `sandbox: true`**: ```json { "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=` — та же HMAC-SHA256, что в production. !!! note "Поле `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` и человекочитаемым сообщением, какой режим сейчас активен. Тестовые и боевые данные (платежи, баланс, идемпотентность) полностью изолированы друг от друга. ---