# Sandbox

The sandbox is a separate environment for testing the whole integration without moving
money. The API, signing, limits and errors work as in production.

| | Sandbox |
| --- | --- |
| API | the sandbox `{{base_url}}`, issued with your access |
| Cabinet | the sandbox `{{cabinet_url}}` |
| Token | starts with `nl_test_` |
| Request signing | required, as in production |

!!! info "This section is being finished"
    The sandbox is still being built. Below is how it will work. Places that may still
    change are marked **TBD**. Watch the [changelog](changelog.md).

## How it works { #how-it-works }

1. **Test project.** Turn on test mode for a project in the sandbox cabinet. Its payments
   never make real payments. A test payment has `is_test: true` in the response.
2. **Test requisites.** A create response carries test requisites: a test phone number for
   SBP or a test card number. Nothing needs to be transferred.
3. **You choose the outcome.** Mark the payment as paid or canceled — in the cabinet or via
   an API call (**TBD**).
4. **Read the result through the API.** Test payments get no automatic callback (**TBD**:
   a way to receive a callback for a test payment is still being designed). Read the final
   status with `GET /api/v1/payments/{id}`; check the callback body format and signature
   against the [test vector](callbacks.md#test-vector).

## Set a payment outcome { #outcome }

!!! warning "TBD: outcome emulation API"
    The API method to set a test payment's outcome is still being designed. For now use
    the buttons on the payment card in the sandbox cabinet. When the method ships, the
    request, response and samples in every language will appear here.

| What to test | What to do |
| --- | --- |
| Successful payment | Mark the payment paid → status `completed` |
| Decline or cancellation | Cancel the payment → status `canceled` |
| Expired deadline | Create a payment and leave it past the project's payment lifetime (15 minutes by default) → `canceled` |
| Idempotency | Send the same request twice → `201`, then `200` with the same payment |
| Id conflict | The same `merchant_payment_id` with another amount → `409 payment_idempotency_conflict` |
| Nonce replay | Send one signed request twice → `409 request_replayed` |
| Bad signature | Change the body after signing → `401 signature_invalid` |

## Differences from production { #differences }

- No money moves; requisites are not real.
- Test payments get no automatic callback and are marked `is_test: true`.
  The sandbox has its own `{{base_url}}` and an `nl_test_` token.
- The sandbox has its own token, signing key and callback secret. Issue new ones for
  production.
- The API address differs: keep it in configuration — see [Environments](index.md#environments).