# Sandbox: PIX, BRL custody and crypto

Use `https://api.luniumpay.com` with a `lun_test_` key. Every financial operation is simulated. An incoming QR contains `SANDBOX:NAO_PAGAVEL:` and cannot be paid through a bank. Confirm payment through the simulation API. No withdrawal is sent to an exchange or blockchain.

## Start with a test key

```bash
curl --fail-with-body https://api.luniumpay.com/keys/sandbox \
  -H 'Content-Type: application/json' \
  -d '{"name":"My sandbox integration"}'
```

Save the returned `api_key` in your backend environment as `LUNIUM_API_KEY`. Confirm `key.sandbox=true` using `GET /keys/me` before creating test operations. Do not put the key in browser code or paste it into an AI conversation.

To run the examples in your own environment, [download the English starter kit](/en/starter/lunium-starter.zip):

```bash
node sandbox.mjs all
# Or Python 3.9+, with no third-party dependencies:
python3 sandbox.py all
```

The kit creates a key in memory, checks `GET /keys/me` and rejects production credentials. It also accepts an existing sandbox key through `LUNIUM_API_KEY` and never prints credentials. Replace `all` with `custody`, `cashin`, `payout` or `cashout` to run one journey.

For the public AI connection, call `lunium_start_sandbox_demo` with a random, stable `request_id` and `flow` set to `custody`, `cashin`, `payout` or `cashout`. Continue with `lunium_get_sandbox_demo`; it may create the simulated withdrawal already included in the selected journey after confirming the credit. Read the [AI integration guide](/en/integrar-com-ia).

## Journeys and completion

| Journey | Create | Confirm and track | Success |
|---|---|---|---|
| PIX → BRL custody | `POST /cashin/charge`, `destino: "saldo"` | `POST /sandbox/cashin/{cashin_id}/pay`, then `/cashin/{cashin_id}/status` and `/saldo` | `status=paid`, `settlement_status=sent`; available funds in the correct subaccount |
| PIX → BTC, USDC or another test-catalog route | `POST /cashin/charge`, `destino: "cripto"`, `asset`, `chain`, `payout_address` | Same payment simulation and cash-in status lookup | `paid` and `sent`; hash begins with `sandbox:`, with no real transaction |
| Custody → crypto | `POST /saldo/sacar-cripto`, `amount_cents`, `tax_number`, `asset`, `chain`, `payout_address` | `/cashin/{cashin_id}/status` | `paid` and `sent`; debit includes `fee_cents` |
| Custody → PIX | `POST /payouts`, `amount_cents`, `pix_key` | `GET /payouts/{payout_id}` | `status=sent`; fictitious E2E with ISPB `00000000` |
| Internal transfer | `POST /saldo/transferir`, `from_customer_ref`, `to_customer_ref`, `amount_cents` | `/saldo/extrato`, `/saldo/clientes`, `/saldo/consolidado` | Atomic debit and credit; total custody balance conserved |
| External crypto → PIX | `POST /cash-outs`, then `POST /cash-outs/{cashout_id}/accept` | `GET /cash-outs/{cashout_id}` | `state=COMPLETED`; deposit is also simulated |

Custody does not require a fixed wallet. The balance is BRL: select asset, network, wallet and memo at withdrawal. The kit exercises native BTC, USDC on Polygon and USDC withdrawal on Base. Read the test catalog for other supported routes. Use `chain` for cash-in and crypto withdrawals; use `network` for cash-out.

## Receive into custody without a wallet

After verifying your sandbox key, create the charge below. The tax ID is synthetic and is only for sandbox use. `100000` BRL cents means R$1,000.00.

```bash
curl --fail-with-body https://api.luniumpay.com/cashin/charge \
  -H "X-API-Key: $LUNIUM_API_KEY" -H 'Content-Type: application/json' \
  -d '{"amount_cents":100000,"payer_tax_number":"12345678901","destino":"saldo","customer_ref":"test-customer","external_id":"sandbox-deposit-001"}'
```

Save the returned `cashin_id` as `CASHIN_ID`, then simulate payment:

```bash
curl --fail-with-body -X POST "https://api.luniumpay.com/sandbox/cashin/$CASHIN_ID/pay" \
  -H "X-API-Key: $LUNIUM_API_KEY" -H 'Content-Type: application/json' -d '{}'
```

Query `/cashin/{cashin_id}/status` until `paid` and `sent`, typically after about six seconds. Then read `/saldo?customer_ref=test-customer`. The credit is net of the key's fees; do not compare it with the gross charge amount. Repeating the payment confirmation does not duplicate credit.

## Deterministic cash-in, custody and payout scenarios

Supply `sandbox_scenario` when creating an operation. Read `GET /sandbox` for the scenario matrix supported by each flow.

| Scenario | Result |
|---|---|
| `success` | Success; default when omitted |
| `delayed` | Approximately 30-second delay before processing continues |
| `held` | Custody deposit only: credit blocked for 30 seconds, then available |
| `expired` | Cash-in expires when payment is simulated |
| `payer_mismatch` | Cash-in refunded for a payer mismatch, without credit or delivery |
| `refunded` | Cash-in refunded without credit; payout refunded with a single reversal |
| `failed` | Payment fails; payout debit reversed once |
| `settlement_failed` | Payment may be confirmed but delivery fails; withdrawal debit and fees reversed |
| `settlement_uncertain` | Delivery remains `incerto`; payout remains `processing`. No reversal before reconciliation |
| `route_unavailable` | Creation refused without a debit |
| `provider_unavailable` | Simulated HTTP 503 with no operation created; remains unsuccessful while this scenario is selected |

To resolve uncertainty **in sandbox only**, call `POST /sandbox/operations/{operation_id}/resolve` with `{"outcome":"sent"}` or `{"outcome":"failed"}`. Repeating the same resolution is idempotent. The operation must belong to this key and already have an uncertain result. There is no simulated confirmation route for production payments.

Also test invalid bodies, asset/network minimums, insufficient funds, concurrent spending, a blocked account, another key's operation ID and an `external_id` reused with different parameters. Keep one business intent, a stable `external_id` and the same request body for retries. Charges and crypto withdrawals share an idempotency namespace. After a timeout, recover by operation ID or a list filtered by `external_id`.

## Crypto-to-PIX scenarios

Cash-out uses the last two decimal places of `amount`, or `brl_amount` for reverse quotes: `.00` success, `.01` delay, `.02` failure, `.03` expiration, `.04` limit, `.05` slow processing, `.06` provider refusal, `.07` first acceptance returns 503 and the second succeeds, `.08` payer limit. Preview creates no operation. Never send crypto to an address returned by the simulator.

## Real webhook delivery for simulated events

Configure a public HTTPS URL you control using `PATCH /keys/me` and `webhook_url`. Store `webhook_secret` securely. Sandbox events use the normal delivery queue and signature and contain `sandbox:true`. Verify HMAC over the raw request bytes, the timestamp and `event_id`, and handle duplicate events as described in the [webhook manual](/en/manual#webhooks). Real HTTP delivery does not mean real financial settlement.

`GET /sandbox/events` shows generated events even without a configured callback. `GET /webhooks/deliveries` shows delivery attempts; `POST /webhooks/deliveries/{event_id}/retry` retries a delivery. Events generated before callback configuration are not sent retroactively: create a new operation or use `/webhooks/test`. Cash-in emits `cashin.paid`, `cashin.settled` or the relevant failure. Holds, releases and transfers have their own events. Payout reports its final result; cash-out reports progress and completion.

Configure the receiver **before** simulating payment. Confirm separately that an event was generated, delivered over HTTP and processed by your application. The starter scripts alone do not prove that your backend handled a webhook.

## Persistence, prices and test boundaries

Cash-in, custody, payouts and transfers persist separately from the real financial ledger, including across service restarts. Cash-out is persisted for two hours. There is a limit of 1,000 operations per key for the new journeys and 1,000 cash-outs. `POST /sandbox/reset` with `{"confirm":true}` deletes only this key's simulated state; previously queued webhook deliveries retain their history.

Quotes and catalogs are synthetic snapshots identified in responses. Production prices, minimums, availability and risk checks can differ. Sandbox calls no bank, exchange or wallet. Its payout provider fee is 100 BRL cents, reported by `/saldo`; this is not a live provider price. The payer ladder does not query real personal history; `/cashin/limits` describes the difference. Hold durations are compressed for testing.

The simulator supports named catalog assets. It does not simulate arbitrary DEX token contracts, bank decoding of dynamic PIX QRs, blockchain execution or private provider policies. Before production, reread `/keys/me`, `/saldo`, limits and catalogs with the live key. A pilot using a real PIX payment and a real crypto withdrawal moves actual funds and requires an authorized amount and recipient. A completed demo does not certify provider liquidity or your full production integration.

[Integration manual](/en/manual) · [API reference](/en/) · [English OpenAPI](/en/openapi.json) · [Human support](/en/manual#telegram)
