> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.recurrente.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recurrente.com/_mcp/server.

# Sandboxes y Test Clocks

> Guía completa para probar suscripciones, renovaciones y webhooks sin dinero real

Un Sandbox es un tenant aislado de una cuenta LIVE. Tiene sus propias llaves TEST,
objetos, actividad, balances simulados y endpoints de webhook. Una llave de Sandbox
nunca puede leer IDs de LIVE ni de otro Sandbox, y ninguna simulación llama a un
procesador, banco, blockchain o proveedor fiscal.

> **Note**
>
> ¿Es tu primera integración? Empieza con [Primer pago de prueba](/guias-espanol/comenzar/primer-pago-de-prueba).
> Esta guía cubre escenarios avanzados de suscripciones y Test Clocks.

## Crear y autenticar un Sandbox

Abre **Pruebas (Sandbox)** desde Llaves API. Un administrador puede administrar hasta cinco ambientes desde **Administrar ambientes**. Puede
empezar vacío o copiar marca, configuración segura de checkout, productos, precios,
campos personalizados y cupones. Clientes, métodos de pago, suscripciones,
movimientos, credenciales y balances nunca se copian.

La llave TEST se revela una sola vez. Guárdala fuera del código y úsala contra la
misma URL base de LIVE:

```bash
export RECURRENTE_API="https://app.recurrente.com/api"
export SANDBOX_KEY="sk_test_..."

curl "$RECURRENTE_API/test" \
  -H "X-SECRET-KEY: $SANDBOX_KEY"
```

Todas las respuestas y webhooks creados con esta llave pertenecen únicamente a ese
Sandbox.

Al entrar al ambiente, un banner persistente mantiene visible el nombre del Sandbox y
recuerda que los datos, llaves y dinero son de prueba. En mobile aparece debajo de la
navegación fija para que el contexto nunca quede oculto.

Los Sandboxes aparecen en una sección separada de ambientes de prueba al iniciar
sesión. Usa **Abrir Sandbox** desde tu cuenta o el enlace directo de tu invitación.
El selector global de cuentas mantiene los negocios de producción separados de los ambientes de prueba.

## Llaves TEST

Las llaves TEST solo existen dentro de un Sandbox; tu cuenta de producción tiene únicamente llaves LIVE. Si usabas la llave TEST de tu cuenta principal, ya se retiró: crea una dentro del Sandbox. Consulta [llaves TEST retiradas](/guias-espanol/guias/test-legacy).

## Qué puedes probar

La API pública está disponible con llaves Sandbox, pero no permite enviar fondos.
Los recursos de catálogo y configuración viven naturalmente dentro del tenant. Los
cobros, refunds, conversiones, cuentas conectadas, terminales y configuración fiscal
persisten estados y balances locales determinísticos. Los hijos de una plataforma
forman un grafo conectado dentro del mismo Sandbox y nunca se mezclan con cuentas
LIVE.

> **Warning**
>
> El balance de un Sandbox es simulado, no transferible y no retirable. El dashboard
> no ofrece **Enviar**. La API rechaza transferencias a cuentas o teléfonos, splits de
> checkout/suscripción, swaps a dólares digitales y retiros. Las conversiones internas
> siguen disponibles porque no sacan fondos del ledger.

Estas simulaciones conservan el contrato observable sin contactar procesadores,
bancos, Bridge, blockchain, hardware POS, INFILE u otros proveedores reales. Consulta
la matriz de paridad para distinguir operaciones nativas de simulaciones locales.

## Elegir cómo corre el tiempo

Una suscripción de Sandbox puede usar una de dos líneas de tiempo:

| Modo        | Cuándo usarlo                                               | Cómo se cobra                                                            |
| ----------- | ----------------------------------------------------------- | ------------------------------------------------------------------------ |
| Tiempo real | Smoke tests que pueden esperar hasta la fecha real          | El collector horario procesa la suscripción normalmente con el simulador |
| Test Clock  | Pruebas determinísticas de meses, renovaciones y reintentos | Solo avanza cuando llamas `POST /test_clocks/{id}/advance`               |

Un Test Clock pertenece a un Sandbox y se asocia a un customer. Todas las
suscripciones que se creen después para ese customer heredan el clock. El tiempo
efectivo aplica solo a ese customer durante el request o job: otros clocks, otros
customers y LIVE continúan con su propia hora.

> **Warning**
>
> Asocia el customer al clock antes de crear la suscripción. Un customer con una
> suscripción ya creada no puede cambiar de clock; crea otro customer para empezar una
> línea de tiempo distinta.

## Tutorial: ciclo completo de una suscripción

### 1. Registrar el webhook de prueba

Registra el endpoint con la llave del Sandbox, no con una llave LIVE:

```bash
curl "$RECURRENTE_API/webhook_endpoints" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://uat.example.com/recurrente/webhooks",
    "description": "Pruebas de suscripciones"
  }'
```

Guarda el `signingSecret`: solo aparece en la respuesta de creación. Este endpoint
recibirá únicamente eventos TEST del Sandbox autenticado.

### 2. Crear el clock y el customer

```bash
curl "$RECURRENTE_API/test_clocks" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Suscripción mensual UAT",
    "frozen_at": "2026-07-01T00:00:00Z"
  }'
```

Guarda el `id` retornado como `clock_...` y úsalo al crear el customer:

```bash
curl "$RECURRENTE_API/customers" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "buyer@example.test",
    "full_name": "Sandbox Buyer",
    "test_clock_id": "clock_...",
    "metadata": { "test_case": "monthly-renewal" }
  }'
```

Guarda el `id` del customer (`cus_...`). La metadata del customer es independiente
de la metadata que luego persistirá en la suscripción.

### 3. Crear un producto recurrente

```bash
curl "$RECURRENTE_API/products" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "product": {
      "name": "Plan mensual Sandbox",
      "prices_attributes": [{
        "amount_in_cents": 2500,
        "currency": "USD",
        "charge_type": "recurring",
        "billing_interval": "month",
        "billing_interval_count": 1
      }]
    }
  }'
```

Guarda el `prices[0].id` de la respuesta (`price_...`).

### 4. Crear y completar el checkout inicial

```bash
curl "$RECURRENTE_API/checkouts" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{ "price_id": "price_...", "quantity": 1 }],
    "customer_id": "cus_...",
    "metadata": {
      "integration_id": "sub-uat-123",
      "scenario": "renewal-and-recovery"
    },
    "success_url": "https://uat.example.com/success",
    "cancel_url": "https://uat.example.com/cancel"
  }'
```

Abre el `checkout_url` retornado y usa:

| Dato       | Valor                        | Resultado |
| ---------- | ---------------------------- | --------- |
| Tarjeta    | `4242 4242 4242 4242`        | Éxito     |
| Tarjeta    | `4000 0000 0000 0002`        | Rechazo   |
| CVC        | Cualquier valor de 3 dígitos | Aceptado  |
| Expiración | Cualquier fecha futura       | Aceptada  |

Después del pago exitoso valida:

* checkout `paid`;
* suscripción `active` con `test_clock_id`, `current_period_start` y
  `current_period_end`;
* una invoice y un payment intent exitosos;
* metadata `integration_id` y `scenario` en la suscripción y sus webhooks;
* eventos `payment_intent.succeeded` y `subscription.create` con
  `live_mode: false` y el `sandbox_id` esperado.

> **Note**
>
> La metadata que sobrevive renovaciones, reintentos y cancelación es la metadata del
> checkout. La metadata enviada al crear el customer permanece en el customer, pero no
> se copia automáticamente a la suscripción.

### 5. Adelantar hasta la primera renovación

Obtén la suscripción y usa su `current_period_end` como destino del clock:

```bash
curl "$RECURRENTE_API/subscriptions/sub_..." \
  -H "X-SECRET-KEY: $SANDBOX_KEY"

curl "$RECURRENTE_API/test_clocks/clock_.../advance" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "frozen_at": "<current_period_end>" }'
```

El avance responde `202` y es asíncrono. Consulta el clock hasta que cambie de
`advancing` a `ready`:

```bash
curl "$RECURRENTE_API/test_clocks/clock_..." \
  -H "X-SECRET-KEY: $SANDBOX_KEY"
```

Cuando queda `ready`, debe existir una nueva invoice pagada, un nuevo
`payment_intent.succeeded`, un período actualizado y un crédito en el balance
simulado. Si queda `failed`, revisa `last_error` antes de volver a ejecutar el caso.

### 6. Probar rechazo y recuperación

El resultado configurado al avanzar aplica a un solo cobro y después vuelve a
`success`:

```bash
curl "$RECURRENTE_API/test_clocks/clock_.../advance" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "frozen_at": "<next current_period_end>",
    "next_charge_outcome": "decline"
  }'
```

Al terminar valida `payment_intent.failed`, `subscription.past_due`,
`payment_retries: 1` y un `next_payment_attempt_at`. Para probar recovery, adelanta
el mismo clock a ese `next_payment_attempt_at` sin enviar `next_charge_outcome`:

```bash
curl "$RECURRENTE_API/test_clocks/clock_.../advance" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "frozen_at": "<next_payment_attempt_at>" }'
```

La invoice pendiente debe quedar pagada, la suscripción debe volver a `active` y se
emite otro `payment_intent.succeeded`.

### 7. Probar refund y cancelación

Usa el ID `pa_...` de un `payment_intent.succeeded` para reembolsar el monto completo:

```bash
curl "$RECURRENTE_API/refunds" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "payment_intent_id": "pa_..." }'
```

Valida un refund `succeeded`, el evento `refund.create` y la reversión del balance
simulado. Después cancela la suscripción:

```bash
curl "$RECURRENTE_API/subscriptions/sub_..." \
  -X DELETE \
  -H "X-SECRET-KEY: $SANDBOX_KEY"
```

Valida `subscription.cancel`, estado `cancelled` y ausencia de nuevas invoices al
adelantar el clock más allá del siguiente período.

## Secuencia esperada

| Escenario    | Webhooks principales                              | Estado observable                              |
| ------------ | ------------------------------------------------- | ---------------------------------------------- |
| Pago inicial | `payment_intent.succeeded`, `subscription.create` | checkout `paid`, suscripción `active`          |
| Renovación   | `payment_intent.succeeded`                        | nueva invoice pagada y nuevo período           |
| Rechazo      | `payment_intent.failed`, `subscription.past_due`  | `past_due`, retry y próxima fecha de intento   |
| Recuperación | `payment_intent.succeeded`                        | invoice pagada y suscripción `active`          |
| Refund       | `refund.create`                                   | refund `succeeded`, balance simulado revertido |
| Cancelación  | `subscription.cancel`                             | suscripción `cancelled`, sin cobros futuros    |

Cada mensaje tiene `eventType`, un `eventId` idempotente y `data.event_type`.
Verifica la firma con el secret del endpoint, deduplica por `eventId` y confirma que
`data.sandbox_id` corresponde al ambiente bajo prueba.

## Probar un webhook sin crear su flujo de dominio

El tutorial anterior es la prueba recomendada para suscripciones porque valida
objetos, estados y eventos naturales. Si solo necesitas probar la firma, routing,
reintentos o idempotencia de un evento difícil de provocar, emite un fixture:

```bash
curl "$RECURRENTE_API/test_helpers/webhook_events" \
  -X POST \
  -H "X-SECRET-KEY: $SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "dispute.create",
    "data": {
      "status": "needs_response",
      "integration_case": "chargeback-v1"
    }
  }'
```

El helper acepta cualquier `event_type` del catálogo público, persiste una
`Activity` y entrega el mensaje por el mismo endpoint Svix. La respuesta `201`
contiene el payload base normalizado. Recurrente genera y reemplaza `data.id` y
`data.created_at`; la entrega Svix agrega `data.event_type` y `data.sandbox_id`.

> **Warning**
>
> Un fixture valida el contrato de entrega, pero no crea una disputa, retiro u otro
> grafo de dominio. Usa el endpoint real y verifica sus objetos cuando necesitas una
> prueba de comportamiento end-to-end.

Los payloads naturales incluyen campos como `live_mode`, metadata y `test_clock_id`
cuando el serializer de ese dominio los publica. El helper no inventa esos campos:
entrega los enviados más `event_type`, `sandbox_id`, `id` y `created_at`.

## Reglas de los Test Clocks

* Cada Sandbox puede tener hasta diez clocks y cada customer pertenece como máximo a uno.
* Un clock solo avanza hacia adelante.
* El máximo por avance es dos veces el intervalo de facturación más corto de sus suscripciones; sin suscripciones, es dos años.
* Un avance procesa cronológicamente renovaciones, reintentos y reanudaciones que ocurran antes del destino.
* Los estados son `ready`, `advancing`, `failed` y `deleted`.
* No puedes eliminar un clock mientras avanza o mientras tenga suscripciones.
* Los endpoints LIVE y los de otro Sandbox nunca reciben sus eventos.

## Checklist para automatizar pruebas

* Usa un Sandbox y una llave TEST dedicados por suite o equipo.
* Crea datos únicos por caso; no reutilices IDs de LIVE.
* Registra el webhook antes de iniciar el checkout y conserva su signing secret.
* Espera `ready` antes de hacer otro avance del mismo clock.
* Usa las fechas devueltas por la API (`current_period_end` y
  `next_payment_attempt_at`) en vez de asumir días fijos.
* Afirma objetos y webhooks: estado, monto, moneda, metadata, `sandbox_id` y
  `test_clock_id`.
* Trata los webhooks como reintentables e idempotentes.
* Usa fixtures solo para probar consumers; usa flujos reales para probar comportamiento.
* Termina cada escenario cancelando la suscripción y archivando el Sandbox cuando
  ya no necesites sus datos.

## Archivo y retención

Archivar un Sandbox revoca sus llaves, bloquea nuevos requests y marca sus clocks
como eliminados. No purga clientes, suscripciones, invoices, actividades ni
webhooks históricos: se conservan para auditoría y diagnóstico. Crea un Sandbox
nuevo para una suite limpia y nunca uses datos reales de clientes como fixtures.

## Diferencias frente a Stripe Test Clocks

Recurrente sigue la asociación del clock al customer, avances asíncronos y solo
hacia adelante, y el máximo de dos intervalos. Recurrente se diferencia en que:

* el estado se observa consultando el clock; no se publican webhooks separados de estado;
* no se puede eliminar un clock que tenga suscripciones;
* eliminar o archivar conserva el grafo histórico en vez de borrarlo;
* los clocks no se auto-eliminan después de un período fijo;
* los objetos y nombres de evento siguen el contrato público de Recurrente.

Consulta la documentación de Stripe sobre [simulación de suscripciones](https://docs.stripe.com/billing/testing/test-clocks/simulate-subscriptions)
y [uso avanzado de Test Clocks](https://docs.stripe.com/billing/testing/test-clocks/api-advanced-usage)
para comparar los contratos.

Consulta la referencia de cada endpoint y la matriz de paridad de Sandbox para saber
qué operaciones son nativas y cuáles persisten una simulación local.