Integración de Lunium

# Sandbox: PIX, custodia y cripto

Usa la misma base `https://api.luniumpay.com` con una clave `lun_test_`. Toda operación financiera es ficticia. El QR de entrada contiene `SANDBOX:NAO_PAGAVEL:`: no es un PIX bancario. Confirma el pago mediante la API de simulación. No se envía ningún retiro al exchange asociado ni a la blockchain.

## Probar ahora

Abre [la integración guiada](https://luniumpay.com/es/integrar), elige el flujo, indica tu correo electrónico y ejecuta la demostración. Mediante la conexión pública de IA, usa `lunium_start_sandbox_demo` con `flow` igual a `custody`, `cashin`, `payout` o `cashout`, además de un `request_id` aleatorio y estable y del `email` de quien hará las pruebas (opcional en las herramientas MCP, pero muy recomendable: con él, LuniumPay envía a la persona la guía de integración —qué es LuniumPay, manual, diagrama de flujo y próximos pasos— una sola vez; sin él, crea la clave igualmente y recoge el correo después mediante `lunium_contact`). Continúa con `lunium_get_sandbox_demo`; puede crear el retiro ficticio ya previsto en el recorrido después de confirmar el crédito.

Para probar tu backend, [descarga el kit](https://docs.luniumpay.com/es/starter/lunium-starter.zip):

```
node sandbox.mjs all
# o Python 3.9+, sin dependencias:
python3 sandbox.py all
```

El kit crea una clave en memoria con el correo de quien hará las pruebas (define `LUNIUM_SANDBOX_EMAIL=voce@empresa.com`; sin él, el kit se detiene con un mensaje claro), confirma `GET /keys/me` antes de operar y rechaza las credenciales de producción. También acepta `LUNIUM_API_KEY` que contenga exclusivamente una clave de prueba. No imprime credenciales. Selecciona `custody`, `cashin`, `payout` o `cashout` en lugar de `all` para un recorrido específico.

## Recorridos y finalización

| Recorrido | Crear | Confirmar y seguir | Éxito |
| --- | --- | --- | --- |
| PIX → custodia BRL | `POST /cashin/charge`, `destino: "saldo"` | `POST /sandbox/cashin/{cashin_id}/pay`, después `/cashin/{cashin_id}/status` y `/saldo` | `status=paid`, `settlement_status=sent`; disponible en la subcuenta correcta |
| PIX → BTC, USDC u otra ruta del catálogo de prueba | `POST /cashin/charge`, `destino: "cripto"`, `asset`, `chain`, `payout_address` | La misma confirmación de PIX y consulta de cash-in | `paid` y `sent`; el hash comienza por `sandbox:`, sin transacción real |
| Custodia → criptomonedas | `POST /saldo/sacar-cripto`, `amount_cents`, `tax_number`, `asset`, `chain`, `payout_address` | `/cashin/{cashin_id}/status` | `paid` y `sent`; el débito incluye `fee_cents` |
| Custodia → PIX | `POST /payouts`, `amount_cents`, `pix_key` | `GET /payouts/{payout_id}` | `status=sent`; E2E ficticio con ISPB `00000000` |
| Transferencia interna | `POST /saldo/transferir`, `from_customer_ref`, `to_customer_ref`, `amount_cents` | `/saldo/extrato`, `/saldo/clientes`, `/saldo/consolidado` | Débito y crédito atómicos; total en custodia conservado |
| DePix/Liquid → PIX | `POST /cash-outs`, `asset: "DEPIX"`, `network: "liquid"`, destinatario Pix y, si quieres, `refund_address` | `POST /cash-outs/{cashout_id}/accept`, después `GET /cash-outs/{cashout_id}` | `state=COMPLETED`; depósito y PIX simulados |
| Criptomonedas externas → PIX | `POST /cash-outs`, después `POST /cash-outs/{cashout_id}/accept` | `GET /cash-outs/{cashout_id}` | `state=COMPLETED`; depósito también simulado |

La custodia no requiere una billetera fija. El saldo es BRL. Elige la moneda, la red, la dirección y el memo solo al retirar. El kit recorre BTC nativo, USDC en Polygon y retiro de USDC en Base; el catálogo de pruebas ofrece otras rutas identificadas. En cash-in y retiro usa `chain`; en cash-out usa `network`.

## Probar PIX → DePix/Liquid

Usa una clave lun\_test\_ y confirma key.sandbox=true en GET /keys/me. La ruta DEPIX/liquid aparece en instant en GET /cashin/catalog?asset=depix&network=liquid. La prueba cubre PIX → DePix; el retiro de la custodia hacia Liquid sigue sin estar disponible.

Envía asset: "depix", chain: "liquid", delivery\_mode: "split" y una dirección Liquid en payout\_address. Sustituye SEU\_ENDERECO\_LIQUID a continuación por la dirección de tu billetera. El CPF del ejemplo es sintético, exclusivo de sandbox. La dirección no recibe monedas: este flujo no usa Liquid mainnet ni testnet.

Consulta POST /cashin/preview con los mismos parámetros antes de crear la solicitud de cobro. La tarifa básica de compra de DePix es del 2% + R$ 1,00, con un mínimo de R$ 2,00 por PIX, salvo condiciones propias de la clave. La parte fija se descuenta antes del porcentaje: R$ 500,00 dan como resultado 489,02 DePix con la política estándar. Comprueba net\_cents y fee\_total\_cents devueltos; la tarifa de compra de USDC/USDT y la comisión del socio no se suman a DePix.

```
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":50000,"payer_tax_number":"12345678901","destino":"cripto","asset":"depix","chain":"liquid","delivery_mode":"split","payout_address":"SEU_ENDERECO_LIQUID","external_id":"sandbox-depix-001"}'

# Usa el cashin_id devuelto al crear la operación:
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 '{}'

curl --fail-with-body "https://api.luniumpay.com/cashin/$CASHIN_ID/status" \
  -H "X-API-Key: $LUNIUM_API_KEY"
```

Confirma el pago mediante la ruta /sandbox/cashin/{cashin\_id}/pay y consulta el estado hasta paid y sent. La API y los webhooks cashin.paid/cashin.settled conservan delivery\_mode, net\_cents y fee\_total\_cents. El hash comienza por sandbox: y settlement\_tx\_url es null. No pagues el QR en un banco: no se realiza ningún PIX ni envío en blockchain.

## Probar DePix/Liquid → PIX

Usa `asset: "DEPIX"`, `network: "liquid"` y una clave `lun_test_`. Consulta `GET /catalog?asset=DEPIX&network=liquid`: la ruta aparece en `fast`. Indica `amount` en DePix o `brl_amount` para cotizar a partir del PIX deseado, sin combinar ambos. La paridad es 1 DePix = R$ 1,00 antes de las tarifas; comprueba el importe efectivo devuelto, también en la cotización inversa.

La venta usa su propia tarifa vigente en la clave, indicada en `fee_bps`, `fee_fixed_cents`, `fee_total_cents` y `net_cents`. Con la política estándar de venta del 1,5% + R$ 1,00, enviar 100,00 DePix da como resultado un PIX de R$ 97,51. La tarifa de compra de DePix es independiente.

Al crear la orden, indica la clave Pix. `refund_address`, una billetera Liquid de devolución confirmada por el cliente, es opcional desde la API 1.44.0: sin ella, una venta que deba devolverse se detiene en `AWAITING_REFUND_ADDRESS` hasta que indiques la billetera en `POST /cash-outs/{cashout_id}/refund-address`. Para una clave Pix de correo electrónico, teléfono o aleatoria, `payee_tax_number` es obligatorio; para CPF/CNPJ, el documento procede de la propia clave. El ejemplo usa un destinatario y un CPF sintéticos. Sustituye `SUA_CARTEIRA_LIQUID_DE_DEVOLUCAO` por una dirección Liquid válida (o elimina el campo); el sandbox no le envía ningún activo.

```
# Vista previa: no crea una orden ni exige destinatario o billetera de devolución.
curl --fail-with-body https://api.luniumpay.com/cash-outs/preview \
  -H "X-API-Key: $LUNIUM_API_KEY" -H 'Content-Type: application/json' \
  -d '{"asset":"DEPIX","network":"liquid","amount":"100.00"}'

# Crear la cotización simulada:
curl --fail-with-body https://api.luniumpay.com/cash-outs \
  -H "X-API-Key: $LUNIUM_API_KEY" -H 'Content-Type: application/json' \
  -d '{"asset":"DEPIX","network":"liquid","amount":"100.00","pix_key":"depix@example.invalid","pix_key_type":"email","payee_tax_number":"12345678901","refund_address":"SUA_CARTEIRA_LIQUID_DE_DEVOLUCAO","external_id":"sandbox-depix-pix-001","sandbox_scenario":"success"}'

# Usa el cashout_id devuelto al crear la operación:
curl --fail-with-body -X POST "https://api.luniumpay.com/cash-outs/$CASHOUT_ID/accept" \
  -H "X-API-Key: $LUNIUM_API_KEY" -H 'Content-Type: application/json' -d '{}'

curl --fail-with-body "https://api.luniumpay.com/cash-outs/$CASHOUT_ID" \
  -H "X-API-Key: $LUNIUM_API_KEY"
```

La aceptación inicia automáticamente el depósito simulado. `deposit_address` comienza por `SANDBOX:LIQUID:NAO_ENVIAR:` y no es una dirección que admita pagos. No envíes monedas ni busques una red testnet: la simulación ocurre en la API. Consulta la orden hasta `COMPLETED`, normalmente unos 14 segundos después de aceptar. Recorre `AWAITING_DEPOSIT` → `DEPOSIT_DETECTED` → `PROCESSING` → `COMPLETED`. El `pix_e2e` final usa el ISPB ficticio `00000000`; la verificación también identifica el sandbox. No hay pago bancario, comprobante bancario real ni transacción Liquid.

Los eventos son `cashout.awaiting_deposit`, `cashout.deposit_detected`, `cashout.processing` y `cashout.completed`. En una devolución, también hay `cashout.refunding` y `cashout.refunded`; el hash de devolución comienza por `sandbox:`. Configura el callback antes de crear la operación o inspecciona `GET /sandbox/events`.

Para DePix → PIX, elige `sandbox_scenario`: `success`, `delayed` (unos 54 segundos), `failed`, `expired` (deja la cotización sin aceptar durante más de 5 segundos), `refunded`, `settlement_uncertain`, `provider_unavailable` (503 sin crear una orden), `route_unavailable` o `no_deposit` (sin depósito hasta caducar). El escenario explícito prevalece sobre los activadores por centavos descritos a continuación; sin escenario explícito, esos activadores siguen activos.

Para probar la devolución sin billetera, crea la cotización sin `refund_address` y con el escenario `refunded`. Después de aceptar, la orden pasa por `REFUNDING` y se detiene en `AWAITING_REFUND_ADDRESS` (webhook `cashout.awaiting_refund_address`), con `refund_amount`, `refund_asset` y `refund_network` completados. Indica una billetera Liquid en `POST /cash-outs/{cashout_id}/refund-address`: la orden continúa hacia `REFUNDING` y, unos 4 segundos después, `REFUNDED`, con un hash de devolución que comienza por `sandbox:`. Repetir la misma billetera responde `200`; otra billetera, `409 refund_address_imutavel`.

En `settlement_uncertain`, la orden permanece en `PROCESSING`. Resuelve con `POST /sandbox/operations/{cashout_id}/resolve` y `{"outcome":"sent"}` para completar, o `{"outcome":"failed"}` para simular la devolución (`REFUNDING` → `REFUNDED` tras unos 4 segundos; sin `refund_address`, la orden se detiene en `AWAITING_REFUND_ADDRESS`, como se indicó antes). Repetir la misma resolución o aceptación no duplica la operación. Repetir `external_id` con un destinatario, documento, billetera, importe o escenario distintos devuelve 409. Recupera la intención mediante `GET /cash-outs?external_id=...`.

Este flujo de DePix usa clave PIX; `br_code` es una función de las rutas USDT/USDC. `sandbox_scenario` y `/sandbox/operations/.../resolve` se rechazan con una clave de producción. Consulta también `GET /sandbox`, dentro de `scenarios_by_flow.cashout_depix`.

## Recibir en custodia sin billetera

Crea la clave con `POST /keys/sandbox`, enviando el `email` de quien hará las pruebas (obligatorio: es a quien enviamos el manual, el diagrama de flujo y los pasos de integración), y guarda `api_key` en `LUNIUM_API_KEY` solo en el backend. Confirma `key.sandbox=true` en `GET /keys/me` antes de las llamadas siguientes. El documento de este ejemplo es sintético y sirve únicamente para sandbox.

```
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":"cliente-teste","external_id":"sandbox-deposito-001"}'
```

Guarda `cashin_id`. Para simular el pago:

```
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 '{}'
```

Consulta `/cashin/{cashin_id}/status` hasta `paid` y `sent`, generalmente unos 6 segundos. Después consulta `/saldo?customer_ref=cliente-teste`. El crédito es neto de las tarifas de la clave; no compares el saldo con el importe bruto de la solicitud de cobro. Repetir el pago no duplica el crédito.

## Escenarios deterministas de cash-in, custodia y payout

Envía `sandbox_scenario` al crear la operación. Consulta `GET /sandbox` para ver la matriz de escenarios por flujo.

| Escenario | Resultado |
| --- | --- |
| `success` | Éxito; predeterminado cuando se omite |
| `delayed` | Retención/demora de aproximadamente 30 segundos antes de continuar |
| `held` | Solo depósito en custodia: crédito bloqueado durante 30 segundos y después disponible |
| `expired` | El cash-in caduca al intentar simular el pago |
| `payer_mismatch` | Cash-in devuelto por discrepancia del pagador, sin crédito/entrega |
| `refunded` | Cash-in devuelto sin crédito; payout devuelto con una única reversión |
| `failed` | Fallo de pago; el payout devuelve el débito una vez |
| `settlement_failed` | El pago puede estar confirmado, pero falla la entrega; el retiro revierte el débito y las tarifas |
| `settlement_uncertain` | La entrega permanece `incerto`; el payout sigue `processing`. No se revierte antes de la conciliación |
| `route_unavailable` | Creación rechazada, sin débito |
| `provider_unavailable` | HTTP 503 simulado, sin operación creada; mientras continúe el escenario, continúa el rechazo |

Para resolver una incertidumbre **solo en pruebas**, llama a `POST /sandbox/operations/{operation_id}/resolve` con `{"outcome":"sent"}` o `{"outcome":"failed"}`. Repetir la misma resolución es idempotente. La ruta exige una operación incierta de la propia clave. No existe confirmación ficticia en producción.

Prueba también payloads no válidos, mínimos por moneda/red, saldo insuficiente, gasto simultáneo, cuenta bloqueada, ID de otra clave y `external_id` reutilizado con parámetros distintos. Usa una intención y un `external_id` estables para los reintentos; se rechazan los parámetros divergentes. La solicitud de cobro y el retiro en cripto comparten el espacio de referencias. Tras un timeout, recupera mediante el ID o las listas filtradas por `external_id`.

## Escenarios de cripto → PIX

El cash-out mantiene los activadores en los dos decimales de `amount` (o `brl_amount` en la cotización inversa): `.00` éxito, `.01` retraso, `.02` fallo, `.03` caducidad, `.04` límite, `.05` lento, `.06` rechazo del proveedor, `.07` el primer intento de aceptación devuelve 503 y el segundo funciona, `.08` límite del pagador. En la vista previa no se crea ningún registro. Nunca envíes cripto a la dirección devuelta por el simulador.

## Billetera USDT/USDC

La clave de prueba usa el simulador de la billetera: las mismas rutas `/carteira-usdt`, las mismas estructuras y estados de producción, sin nada real. El depósito devuelve una dirección ficticia (`SANDBOX-NAO-ENVIE-FUNDOS`; nunca envíes fondos) y se acredita automáticamente en unos 15 segundos. El PIX pagado con el saldo de prueba se completa en unos 15 segundos, con E2E de prueba y `receipt_url: null`. Activadores por centavos: el depósito `.02` no llega y caduca en aproximadamente 1 minuto; el PIX `.01` pasa unos 40 segundos en `revisao`, `.02` falla antes del envío y devuelve la reserva, `.05` tarda unos 2 minutos. Los webhooks `wallet.deposit_credited`, `wallet.payment_completed` y `wallet.payment_failed` llegan con `sandbox: true`.

## Webhooks reales sobre eventos ficticios

Configura una URL HTTPS pública que controles en `PATCH /keys/me` con `webhook_url` y guarda `webhook_secret` de forma segura. Los eventos del sandbox usan la misma cola de entrega y firma del producto; incluyen `sandbox:true`. Valida el HMAC sobre el cuerpo sin procesar, el timestamp, `event_id` y la tolerancia a duplicados según el [manual](https://docs.luniumpay.com/es/manual#webhooks). No confundas un evento ficticio entregado por HTTP real con una liquidación financiera real.

Usa `GET /sandbox/events` para inspeccionar los eventos generados, incluso sin callback configurado. `GET /webhooks/deliveries` muestra intentos y `POST /webhooks/deliveries/{event_id}/retry` reenvía. Los eventos anteriores a la configuración del callback no se envían retroactivamente: crea una operación nueva o usa `/webhooks/test`. Cash-in publica `cashin.paid`, `cashin.settled` o el fallo correspondiente. La retención/liberación y las transferencias tienen sus propios eventos. Payout publica su resultado final; cash-out publica su progreso y finalización.

## Persistencia, precios y límites de las pruebas

Cash-in, custodia, payout y transferencias persisten aislados del libro financiero real, incluso después de reiniciar. Cash-out también se guarda, con una retención de dos horas. Hay un límite de 1.000 operaciones por clave para los nuevos recorridos y 1.000 cash-outs. `POST /sandbox/reset` con `{"confirm":true}` elimina únicamente el estado ficticio de tu clave; las entregas de webhook ya encoladas conservan su historial.

Las cotizaciones y los catálogos son instantáneas sintéticas identificadas en la respuesta. Los precios, mínimos, disponibilidad y análisis de riesgo de producción pueden cambiar. El sandbox no llama al exchange asociado, a los bancos ni a las billeteras. El coste del proveedor del payout de prueba se fija en 100 centavos y se indica en `/saldo`; no representa la tarifa real. La escala por CPF no consulta el historial de personas; `/cashin/limits` indica esta diferencia. Las pruebas de retención tienen tiempos reducidos.

El simulador cubre activos identificados de su catálogo; no simula tokens DEX arbitrarios por contrato, lectura bancaria de QR dinámicos, la red blockchain ni políticas privadas de cada proveedor. En producción, vuelve a leer `/keys/me`, `/saldo`, los límites y los catálogos con la clave real. Un piloto con PIX realmente pagado y cripto enviada por el exchange asociado es una operación real, con importe y destinatario autorizados. Completar el sandbox valida la integración del software, no certifica la liquidez ni la ejecución de los proveedores.
