> Источник: https://apipay.kz/guides/pesochnitsa-i-rabochiy-rezhim · Обновлено: 2026-10-25 · apipay.kz
> ApiPay — независимый сервис приёма платежей поверх вашего Kaspi Pay.

# Чем песочница отличается от рабочего режима и как перейти без потерь

**TL;DR.** Песочница (тестовый режим) — тренировочный зал ApiPay: счета **не передаются в Kaspi**, покупателю ничего не приходит, оплату вы имитируете сами. Она **бесплатна всегда** и не тратит дни триала; лимит — **1000 тестовых счетов** на организацию. Рабочий режим включается в Настройках одним переключателем, и для обычного клиента он обратим: тот же аккаунт, тот же API-ключ. Если вернулись в тестовый режим — выключайте его сразу после проверки: пока он включён, боевые счета покупателям в Kaspi не уходят и деньги не поступают. Каждый переход в рабочий режим **стирает sandbox-данные организации** — тестовые счета, возвраты по ним, тестовые подписки и позиции каталога; выгружайте нужное заранее. У партнёров сверх этого при переходе в production удаляются **все тестовые организации вместе с их API-ключами**. Как подготовиться — ниже.

## Коротко

| Вопрос | Песочница | Рабочий режим |
|---|---|---|
| Счета уходят в Kaspi | Нет — покупателю ничего не приходит | Да — push в Kaspi покупателю |
| Оплата | Имитируете сами (в кабинете или методом `simulate-status`) | Реальные деньги на ваш Kaspi-счёт |
| Цена | Бесплатно всегда | 3 дня триала, затем тариф |
| Признаки счёта | `is_sandbox: true` (у счёта по номеру ещё `kaspi_invoice_id` с `SANDBOX-`) | Обычные номера счетов Kaspi |
| Лимит | 1000 тестовых счетов на организацию | По тарифу |
| Доставка `invoice.status_changed` | До 3 попыток: первая и 2 повтора через 5 и 15 секунд | До 11 попыток: первая и 10 повторов, интервалы до 1 часа |
| API-ключ | Тот же, что и в рабочем (у клиента ЛК) | Тот же ключ |
| Судьба данных при выходе в бой | Тестовые счета, возвраты, подписки и позиции каталога стираются | Боевые данные не затрагиваются |

## Что такое песочница и зачем она нужна?

Песочница — режим, в котором вы создаёте счета, получаете вебхуки и смотрите статусы, но в Kaspi ничего не уходит и настоящих денег нет.

Оплату тестового счёта вы имитируете сами: в личном кабинете отметьте счёт оплаченным или отменённым, а из кода — методом `simulate-status` (подробный прогон — в разделе для разработчиков ниже). Так проверяются все ветки: успех, отмена, истечение, ошибка. Тот же цикл для автономного ИИ-агента — «[Интеграция с ApiPay с помощью ИИ](/guides/integratsiya-apipay-s-pomoshchyu-ii)» и страница [apipay.kz/for-ai](/for-ai).

## Клиент не получил счёт? Сначала проверьте режим

Счёт создаётся, а оплата в Kaspi не приходит — проверьте три признака тестового режима:

1. **Плашка в кабинете.** Вверху горит «ТЕСТОВЫЙ РЕЖИМ … Счета не передаются в Kaspi» — это он.
2. **Ответ API.** У счёта `is_sandbox: true`.
3. **Номер счёта.** У счёта по номеру телефона `kaspi_invoice_id` начинается с `SANDBOX-`. У тестового QR-счёта номер похож на обычный — для него надёжен только `is_sandbox`.

Любой из трёх признаков означает: счёт живёт в песочнице, покупатель его не увидит. Включите рабочий режим — правки кода для этого не нужны.

## Как включить рабочий режим

1. Заполните [анкету о бизнесе](/guides/anketa-o-biznese-i-limit) и дождитесь её одобрения. В песочнице интеграцию можно тестировать до одобрения. Затем убедитесь, что **кассир подключён**: Настройки → «Авторизация Kaspi» (какой номер подойдёт — в статье «[Требования к номеру кассира](/guides/trebovaniya-k-nomeru-kassira)»). Без кассира счетам физически не через что уходить в Kaspi.
2. Проверьте тестовую интеграцию и готовность организации. В **Настройках** самостоятельно подтвердите переключение на «Рабочий» с учётом удаления sandbox-данных. Если интеграцию делает ИИ-помощник, переключаете режим всё равно вы: помощник ваше подтверждение не заменяет.
3. При желании выставьте пробный счёт на свой личный номер на минимальную сумму и оплатите его: так видно, что push пришёл и счёт перешёл в `paid` (опросом `GET /invoices/{id}` или вебхуком, если он подключён). Оплата пройдёт настоящими деньгами.

Переключение **обратимо**, аккаунт, API-ключ и настройки вебхука не меняются: отдельного «sandbox-ключа» в ApiPay нет. Но каждый выход в рабочий режим стирает sandbox-данные организации — что именно, в разделе ниже; выгружайте нужное до переключения.

Ещё три правила переключателя:

- менять режим можно не чаще одного раза в **5 минут** — кабинет покажет, сколько осталось ждать; повтор того же значения ничего не меняет и sandbox-данные не удаляет
- включить тестовый режим может только верифицированная организация
- для выхода в рабочий режим нужна живая привязка кассира: если она истекла — переподключите кассира, если проверка не прошла — повторите позже; в обоих случаях режим не меняется и sandbox-данные остаются на месте

С момента активации рабочего доступа у вас есть **3 бесплатных дня** триала, дальше — тариф. Подробности — «[Тестовый период](/guides/testovyy-period)».

## Что удаляется при переходе в рабочий режим

**У обычного клиента** (личный кабинет apipay.kz) режим — переключатель, API-ключ и настройки вебхука не меняются. Но данные пропадают: **каждый переход в рабочий режим удаляет sandbox-данные организации** — тестовые счета, возвраты по ним, тестовые подписки и тестовые позиции каталога. Всё, что нужно сохранить, выгружайте до переключения. Боевые счета, возвраты и подписки не затрагиваются. В рабочем режиме отбрасывайте события с `is_sandbox: true` в корне тела: отвечайте `2xx` и не исполняйте. Событие по песочному объекту, отправленное до перевода организации в рабочий режим, может прийти уже после него. Ссылки на оплату и печатные QR-листы, выпущенные в песочнице, остаются тестовыми и после перехода в рабочий режим: в API у них `is_sandbox: true`, покупатель увидит «Тестовый лист», деньги не спишутся. Для покупателей выпустите новые уже в рабочем режиме, а старые отключите — «[Перешёл в рабочий режим — не работает](/guides/posle-prod-ne-rabotaet)».

**У партнёров** сверх этого есть свой сценарий. Тестовые организации — отдельные сущности со своими API-ключами. При переводе партнёрского аккаунта в production **все тестовые организации удаляются полностью** — вместе с их тестовыми счетами, возвратами, подписками — а их API-ключи деактивируются. Восстановлению они не подлежат: не храните в тестовых организациях ничего ценного, держите конфигурацию (URL вебхука, соответствия «ваш клиент → организация ApiPay») в своей системе и планируйте перевыпуск ключей для боевых организаций. Тестовая организация архитектурно **не может стать боевой**: её ключи с боевой никогда не заработают. Подробнее о партнёрском контуре — «[Партнёрский API](/guides/partner-api-white-label)».

Если после перехода «ключ перестал работать» — проверьте два случая: ключ принадлежал удалённой тестовой организации либо был перевыпущен повторным вызовом (повторная выдача ключа организации перегенерирует его и перезаписывает Webhook URL с секретом — старый ключ умирает мгновенно).

## Технические отличия песочницы (для разработчиков)

- **Отмена QR-счёта:** и в песочнице, и в рабочем режиме — `409 qr_cancel_unsupported`: QR-счёт гаснет сам.
- **Повторы `invoice.status_changed` короче:** до 3 попыток вместо 11; возвраты по счетам на номер и подписки сохраняют полную сетку — медленно «просыпающийся» endpoint в тесте может не дождаться повтора.
- **Подписки в песочнице** — не больше **10** на организацию, превышение → `400 sandbox_subscription_limit`. Создание подписки в любом режиме требует верифицированной организации, иначе `403`.
- **Имена тестовых организаций** генерируются автоматически (вида «ТОО Синие птицы») — так их не спутаешь с боевыми.
- Настройка вебхуков, HMAC-подпись и локальное тестирование — «[Настройка вебхуков ApiPay](/guides/nastroyka-webhookov-apipay)» и страница [/local-testing](/local-testing).

## Полный цикл прогона в песочнице (для разработчиков)

Прогоните перед продом весь путь реальными запросами: **создать счёт → дождаться `pending` → симулировать статус → проверить вебхук → сделать возврат**. Каждый шаг — обычный запрос к публичному API (`https://api.apipay.kz/api/v1`, заголовок `X-API-Key`), без единого телефонного номера живого человека.

**Главный нюанс порядка — `processing → pending` перед симуляцией.** `POST /invoices` (счёт по номеру) обрабатывается асинхронно: `201` приходит со `status: "processing"`. Метод `simulate-status` переводит счёт **из `pending`**, поэтому сразу после создания симулировать нельзя — сначала опросите `GET /invoices/{id}` (лимит 1000/мин, читает из кэша) до `status: "pending"`. Симуляция не-`pending` счёта вернёт `400 invalid_status_transition` (в ответе — `current_status` и `allowed_from: ["pending"]`).

```bash
BASE="https://api.apipay.kz/api/v1"
KEY="$APIPAY_API_KEY"   # ключ — из переменной окружения, значение в команду не вписывайте

# 1) создать sandbox-счёт (номер — sandbox-константа, не реальный человек)
ID=$(curl -s -X POST "$BASE/invoices" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"amount":10000,"phone_number":"87770000001","description":"Sandbox test"}' | jq -r '.id')

# 2) дождаться pending ПЕРЕД симуляцией (сразу после создания счёт в processing)
#    выход по error и по таймауту — бесконечный цикл недопустим
for i in $(seq 1 30); do
  STATUS=$(curl -s "$BASE/invoices/$ID" -H "X-API-Key: $KEY" | jq -r '.status')
  [ "$STATUS" = "pending" ] && break
  [ "$STATUS" = "error" ] && { echo "Счёт в error — смотрите error_code"; exit 1; }
  sleep 2
done
[ "$STATUS" = "pending" ] || { echo "Счёт не перешёл в pending за минуту"; exit 1; }

# 3) симулировать оплату (только песочница)
curl -s -X POST "$BASE/invoices/$ID/simulate-status" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" -d '{"status":"paid"}'

# 4) проверить, что вебхук ушёл
curl -s "$BASE/webhook-logs?invoice_id=$ID&event=invoice.status_changed" \
  -H "X-API-Key: $KEY" | jq '.data[] | {event, status, response_status}'

# 5) вернуть (без поля simulate — успешный возврат; счёт должен быть paid)
#    повтор после таймаута — только с тем же Idempotency-Key
curl -s -X POST "$BASE/invoices/$ID/refund" -H "X-API-Key: $KEY" \
  -H "Idempotency-Key: sandbox-refund-001" \
  -H "Content-Type: application/json" -d '{"amount":5000,"reason":"Sandbox refund"}'
```

**Метод `POST /invoices/{id}/simulate-status`** — только для тестовых счетов (`is_sandbox: true`; боевой счёт → `403 not_sandbox`), свой лимит 60 запросов/мин на ключ (общий бюджет 200/мин не тратит). Тело — `{ "status": <enum> }`; опционально `kaspi_source_type` (`GOLD`|`RED`|`LOAN`|`BUSINESSACCOUNT`|`BANKINTEGRATIONACCOUNT`) и `kaspi_sale_type` (`Remote`|`QR`|`Static`|`Restaurant`); при `paid` без них они выбираются случайно. Полная схема метода — в [документации API](/docs).

| `status` | Что происходит со счётом | Какой вебхук уходит | Условия / ошибки |
|---|---|---|---|
| `paid` | → `paid` (с `paid_at`) | `invoice.status_changed` (`paid`) | из `pending` |
| `cancelled` | → `cancelled` | `invoice.status_changed` (`cancelled`) | из `pending` |
| `expired` | → `expired` | `invoice.status_changed` (`expired`) | из `pending` |
| `error` | → `error`, `error_code: sandbox_simulated_error`, `error_message` (свой текст параметром `error_message` ≤255; дефолт «Симулированная ошибка (sandbox).») | `invoice.status_changed` (`error`) — как при реальной ошибке | из `pending` |
| `qr_scanned` | остаётся `pending` (транзиентное суб-событие) | `invoice.qr_scanned` (`qr_substate: "scanned"`) | **только QR-счёт**: не-QR → `400 not_qr_invoice`; повтор → `400 already_scanned` |

Из этих статусов симуляция делает и обратные переходы боя: `cancelled` и `expired` → `paid` (поздняя оплата, уходит `invoice.status_changed` с `paid`) и `error` → `pending` (счёт ожил при сверке). На `error` → `pending` приходит `invoice.status_changed` со `status: pending`, как в рабочем режиме. Это новый переход, а не дубль, даже если `pending` по счёту уже приходил.

Критерий успеха каждого шага — в `GET /webhook-logs` по нужному `event` появляется запись `status: success`. События `subscription.*` и `receipt.*` тоже пишутся в этот журнал. Событие счёта, возврата или QR-возврата за паузу отправки ложится строкой `skipped` и не повторяется — итог пропущенного сверяйте чтением (`GET /invoices/{id}`, `GET /invoices/{id}/refunds`, `GET /qr-refunds/{id}`); `subscription.*` и `receipt.*` паузу не соблюдают и уходят с обычными повторами, недоставленное сверяйте `GET /subscriptions/{id}` и `GET /receipts/{id}`. Доставки `catalog.item_processed` — в отдельном журнале `GET /catalog/webhook-logs`.

## Возврат в песочнице: запрос и заказанный исход

Возврат по счёту на номер делают в песочнице запросом `POST /invoices/{id}/refund`; исход заказывается полем `simulate` (`completed`, `failed` с кодом, `uncertain`). Возврат возможен только у счёта в `paid`, поэтому порядок такой: `simulate-status paid` → затем `refund` (параметры запроса — «[Возвраты Kaspi через API](/guides/vozvraty-kaspi-cherez-api)»).

У возврата по ссылке «Возврат ApiPay» ветка другая (см. «[Возврат по QR](/guides/vozvrat-po-qr-cherez-api)»). Ссылка `POST /qr-refunds/links` рождается в состоянии `awaiting_customer` — ждёт покупателя. Шаги покупателя двигает ручка `POST /qr-refunds/{id}/simulate`: первым — `event: activated` (покупатель нажал «вернуть»; то же даёт кнопка на странице `customer_url`), сессия переходит в `awaiting_scan`; затем `event: identified` (покупатель подтвердил, `client_name` = «Иван И.») или `event: expired` (окно скана пропущено). Неприменимое событие отвечает `409 qr_refund_simulation_not_applicable` и ничего не меняет. После `expired` ссылка, у которой ещё остались окна, возвращается в `awaiting_customer`, как в бою. Терминальный `expired` и вебхук `qr_refund.expired` приходят, когда окна ссылки исчерпаны или истёк `link_expires_at`. На боевой сессии ручка отдаёт `403 not_sandbox`.

В песочнице бэкенд сам доставляет вебхуки возврата: `invoice.refunded` (со `status: completed` либо `failed`) и — на **первый частичный** возврат — дополнительно `invoice.status_changed` со `status: partially_refunded`. У `simulate: uncertain` вебхука нет: возврат остаётся в `processing` с `refund_execution_uncertain`, пока его не разберут `POST /invoices/{id}/refunds/{refund}/simulate-resolve`. Проверить возвраты по счёту — `GET /invoices/{id}/refunds`. Число попыток зависит от события: у `invoice.refunded` и `subscription.*` — до 11 в обоих режимах; у `invoice.status_changed`, `invoice.qr_scanned` и `qr_refund.*` — до 3 в песочнице и до 11 в рабочем режиме. У `catalog.*` и `receipt.*` — до 3 в обоих режимах. Это общее число попыток, включая первую; подробные условия и интервалы — в [настройке вебхуков](/guides/nastroyka-webhookov-apipay).

## QR-счёт: создание и отсканирование в песочнице

- **Создать QR-счёт** — `POST /invoices/qr`: запрос синхронный, `201` приходит сразу со `status: "pending"` и QR-полями (отдельного `pending`-вебхука у QR нет).
- **Sandbox-шорткат:** в теле `POST /invoices/qr` можно передать `simulate: paid|cancelled|expired` — QR-счёт создастся сразу в терминальном статусе с мгновенной отправкой вебхука (когда сам скан проверять не нужно).
- **Отсканирование:** чтобы проверить событие `invoice.qr_scanned`, вызовите `simulate-status` со `status: qr_scanned` (только для QR; статус остаётся `pending`, один раз — повтор даст `400 already_scanned`). После скана транзиентно возможны и `paid`, и `cancelled` — симулируйте нужную ветку следом.

## Проверка номера телефона: sandbox-моки

`POST /clients/check` в песочнице не ходит в Kaspi и ничего не пишет в кэш/счётчики — отдаёт **детерминированный** ответ ровно по двум номерам:

- `87770000001` → `{ "has_kaspi": true, "client_name": "Иван И." }`
- `87770000002` → `{ "has_kaspi": false, "client_name": null }`
- любой другой валидный номер → `{ "has_kaspi": false, "client_name": null }`

Это единственные телефонные значения, которые стоит использовать в тестах.

## Чек-лист выхода в рабочий режим

Перед переключением убедитесь, что в песочнице прошло:

- [ ] `POST /invoices` → счёт дошёл до `pending` (поллинг `GET /invoices/{id}` работает).
- [ ] Симулированы **все** терминалы — `paid`, `cancelled`, `expired`, `error` — и по каждому в `GET /webhook-logs` есть `status: success` с нужным `event`.
- [ ] Для QR (если используете): `qr_scanned` → затем `paid`/`cancelled`.
- [ ] Возврат: `simulate-status paid` → `refund` → пришёл `invoice.refunded` (`completed`), а первый частичный дал `partially_refunded`.
- [ ] Приёмник вебхуков проверяет подпись и отвечает `2xx` быстрее 5 секунд (детали — «[Настройка вебхуков ApiPay](/guides/nastroyka-webhookov-apipay)»).
- [ ] В рабочем режиме обработчик отбрасывает события с `is_sandbox: true` в корне тела: отвечает `2xx` и не исполняет.
- [ ] Обработчик дедуплицирует по `(invoice.id, invoice.status)` и идемпотентен.
- [ ] Ветки ошибок разобраны по `error_code` (включая виденный `sandbox_simulated_error`).
- [ ] Из кода, который пойдёт в рабочий режим, убраны поле `simulate` и заголовок `X-Sandbox-Scenario`. У боевой организации они не делают запрос пробным: счёт уходит покупателю, возврат — настоящими деньгами.
- [ ] Всё, что нужно из песочницы, выгружено — переход его удалит.

## Вопросы и ответы

**Оплатил тариф, а счета всё ещё `SANDBOX-`.**
Оплата тарифа режим не переключает — переключите его сами в Настройках.

**Идёт ли триал, пока я в песочнице?**
Нет. 3 бесплатных дня относятся к рабочему доступу; песочница бесплатна независимо от них.

**Нужен ли отдельный API-ключ для песочницы?**
Нет. У клиента личного кабинета ключ один и тот же для обоих режимов — переключается только режим. Отдельные тестовые ключи есть только у тестовых организаций партнёров, и они удаляются при переходе в production.

Смотрите также: [Требования к номеру кассира](/guides/trebovaniya-k-nomeru-kassira) · [Привязка кассира разорвалась](/guides/pochemu-sletala-sessiya-kassira) · [Настройка вебхуков ApiPay](/guides/nastroyka-webhookov-apipay) · [Безопасность и как работает ApiPay](/guides/bezopasnost-i-kak-rabotaet-apipay) · пиллар «[Как принимать оплату Kaspi через API](/kaspi-api)» · [Локальное тестирование вебхуков](/local-testing).

---

ApiPay — независимый сервис и не аффилирован с АО «Kaspi Bank». Kaspi и Kaspi Pay — товарные знаки их правообладателя.
База знаний: https://apipay.kz/guides
