> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.recurrente.com/guides-english/guides/sandboxes-and-test-clocks/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.recurrente.com/_mcp/server. # Sandboxes and Test Clocks > Test subscriptions, renewals, and webhooks without real money A Sandbox is a tenant isolated from a LIVE account. It has its own TEST keys, objects, activity, simulated balances, and webhook endpoints. A Sandbox key can never read IDs from LIVE or another Sandbox, and simulations never call a payment processor, bank, blockchain, or tax provider. > **Note** > > New integration? Start with [First test payment](/guides-english/getting-started/first-test-payment). > This guide covers advanced subscription and Test Clock scenarios. ## Create and authenticate a Sandbox Open **Pruebas (Sandbox)** from API keys. An administrator can manage up to five environments under **Administrar ambientes**. A Sandbox can start empty or copy branding, safe checkout settings, products, prices, custom fields, and coupons. Customers, payment methods, subscriptions, movements, credentials, and balances are never copied. The TEST key is shown once. Store it outside your code and use it against the same base URL as 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" ``` Every response and webhook created with this key belongs only to that Sandbox. After entering the environment, a persistent banner keeps the Sandbox name visible and reminds the developer that its data, keys, and money are simulated. On mobile it sits below the fixed navigation so the environment context is never obscured. Sandboxes appear in a separate testing section when you sign in. Use **Abrir Sandbox** from your account or your invitation link. The global account selector keeps live business accounts separate from testing environments. ## TEST keys TEST keys only exist inside a Sandbox; your live account holds LIVE keys only. If you used your main account's TEST key, it was retired: create one inside the Sandbox. See [retired TEST keys](/guides-english/guides/legacy-test). ## What you can test The public API is available with Sandbox keys, but it cannot send funds. Catalog and configuration resources live naturally inside the tenant. Charges, refunds, conversions, connected accounts, terminals, and tax configuration persist deterministic local states and balances. A platform's children form a connected graph inside that same Sandbox and never mix with LIVE accounts. > **Warning** > > A Sandbox balance is simulated, non-transferable, and non-redeemable. The dashboard > does not offer **Send**. The API rejects account and phone transfers, checkout and > subscription splits, stablecoin sends, and bank withdrawals. Internal currency > conversions remain available because they do not move funds outside the ledger. These simulations preserve the observable contract without contacting payment processors, banks, Bridge, blockchain, POS hardware, INFILE, or other real providers. See the parity matrix to distinguish native operations from local simulations. ## Choose how time moves A Sandbox subscription can use one of two timelines: | Mode | When to use it | How billing runs | | ---------- | ----------------------------------------------------- | ------------------------------------------------------------------ | | Real time | Smoke tests that can wait until the real billing date | The hourly collector processes the subscription with the simulator | | Test Clock | Deterministic tests for months, renewals, and retries | Billing moves only when you call `POST /test_clocks/{id}/advance` | A Test Clock belongs to one Sandbox and is attached to a customer. Subscriptions created afterward for that customer inherit the clock. Effective time applies only to that customer during the request or job; other clocks, customers, and LIVE keep their own time. > **Warning** > > Attach the customer to the clock before creating the subscription. A customer with > an existing subscription cannot change clocks; create another customer to start a > different timeline. ## Tutorial: test a complete subscription lifecycle ### 1. Register the test webhook Register the endpoint with the Sandbox key, not a LIVE key: ```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": "Subscription tests" }' ``` Store the `signingSecret`: it is returned only when the endpoint is created. This endpoint receives only TEST events from the authenticated Sandbox. ### 2. Create the clock and customer ```bash curl "$RECURRENTE_API/test_clocks" \ -X POST \ -H "X-SECRET-KEY: $SANDBOX_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Monthly subscription UAT", "frozen_at": "2026-07-01T00:00:00Z" }' ``` Save the returned `clock_...` ID and use it when creating the 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" } }' ``` Save the customer ID (`cus_...`). Customer metadata is separate from the metadata that will persist on the subscription. ### 3. Create a recurring product ```bash curl "$RECURRENTE_API/products" \ -X POST \ -H "X-SECRET-KEY: $SANDBOX_KEY" \ -H "Content-Type: application/json" \ -d '{ "product": { "name": "Monthly Sandbox Plan", "prices_attributes": [{ "amount_in_cents": 2500, "currency": "USD", "charge_type": "recurring", "billing_interval": "month", "billing_interval_count": 1 }] } }' ``` Save `prices[0].id` from the response (`price_...`). ### 4. Create and complete the initial checkout ```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" }' ``` Open the returned `checkout_url` and use: | Field | Value | Outcome | | ---------- | --------------------- | -------- | | Card | `4242 4242 4242 4242` | Success | | Card | `4000 0000 0000 0002` | Decline | | CVC | Any three digits | Accepted | | Expiration | Any future date | Accepted | After a successful payment, assert: * the checkout is `paid`; * the subscription is `active` and has `test_clock_id`, `current_period_start`, and `current_period_end`; * one invoice and payment intent succeeded; * `integration_id` and `scenario` metadata are present on the subscription and its webhooks; * `payment_intent.succeeded` and `subscription.create` include `live_mode: false` and the expected `sandbox_id`. > **Note** > > Checkout metadata survives renewals, retries, and cancellation. Metadata sent when > creating the customer stays on the customer and is not copied automatically to the > subscription. ### 5. Advance to the first renewal Fetch the subscription and use its `current_period_end` as the clock destination: ```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": "" }' ``` Advance responds with `202` and runs asynchronously. Poll the clock until it moves from `advancing` to `ready`: ```bash curl "$RECURRENTE_API/test_clocks/clock_..." \ -H "X-SECRET-KEY: $SANDBOX_KEY" ``` When it is `ready`, assert a new paid invoice, another `payment_intent.succeeded`, an updated period, and a simulated balance credit. If it is `failed`, inspect `last_error` before rerunning the scenario. ### 6. Test a decline and recovery The outcome supplied while advancing applies to one charge and then resets to `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_charge_outcome": "decline" }' ``` After completion, assert `payment_intent.failed`, `subscription.past_due`, `payment_retries: 1`, and a `next_payment_attempt_at`. To test recovery, advance the same clock to that `next_payment_attempt_at` without `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": "" }' ``` The pending invoice should become paid, the subscription should return to `active`, and another `payment_intent.succeeded` should be emitted. ### 7. Test a refund and cancellation Use the `pa_...` ID from a `payment_intent.succeeded` event to issue a full refund: ```bash curl "$RECURRENTE_API/refunds" \ -X POST \ -H "X-SECRET-KEY: $SANDBOX_KEY" \ -H "Content-Type: application/json" \ -d '{ "payment_intent_id": "pa_..." }' ``` Assert a `succeeded` refund, `refund.create`, and the simulated balance reversal. Then cancel the subscription: ```bash curl "$RECURRENTE_API/subscriptions/sub_..." \ -X DELETE \ -H "X-SECRET-KEY: $SANDBOX_KEY" ``` Assert `subscription.cancel`, a `cancelled` status, and no new invoices after advancing past the next period. ## Expected sequence | Scenario | Main webhooks | Observable state | | --------------- | ------------------------------------------------- | ---------------------------------------------- | | Initial payment | `payment_intent.succeeded`, `subscription.create` | checkout `paid`, subscription `active` | | Renewal | `payment_intent.succeeded` | new paid invoice and billing period | | Decline | `payment_intent.failed`, `subscription.past_due` | `past_due`, retry count, and next attempt date | | Recovery | `payment_intent.succeeded` | invoice paid and subscription `active` | | Refund | `refund.create` | refund `succeeded`, simulated balance reversed | | Cancellation | `subscription.cancel` | subscription `cancelled`, no future charges | Each message includes `eventType`, an idempotent `eventId`, and `data.event_type`. Verify its signature with the endpoint secret, deduplicate by `eventId`, and confirm that `data.sandbox_id` matches the environment under test. ## Test a webhook without creating its domain flow The tutorial above is the recommended subscription test because it validates natural objects, states, and events. If you only need to test signature verification, routing, retries, or idempotency for an event that is difficult to trigger, emit a 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" } }' ``` The helper accepts any `event_type` in the public catalog, persists an `Activity`, and delivers it through the same Svix endpoint. The `201` response contains the normalized base payload. Recurrente generates and replaces `data.id` and `data.created_at`; Svix delivery adds `data.event_type` and `data.sandbox_id`. > **Warning** > > A fixture validates the delivery contract, but it does not create a dispute, > withdrawal, or other domain graph. Use the real endpoint and assert its objects when > you need an end-to-end behavior test. Natural payloads include fields such as `live_mode`, metadata, and `test_clock_id` when that domain serializer publishes them. The helper does not invent those fields: it delivers the submitted fields plus `event_type`, `sandbox_id`, `id`, and `created_at`. ## Test Clock rules * Each Sandbox can have up to ten clocks, and a customer can belong to at most one. * A clock can move only forward. * The maximum advance is twice the shortest billing interval among its subscriptions; without subscriptions, it is two years. * An advance processes renewals, retries, and resumptions before the destination in chronological order. * Clock states are `ready`, `advancing`, `failed`, and `deleted`. * A clock cannot be deleted while advancing or while it has subscriptions. * LIVE endpoints and endpoints from another Sandbox never receive its events. ## Automation checklist * Use a dedicated Sandbox and TEST key per test suite or team. * Create unique data per case; never reuse LIVE IDs. * Register the webhook before starting checkout and store its signing secret. * Wait for `ready` before advancing the same clock again. * Use dates returned by the API (`current_period_end` and `next_payment_attempt_at`) instead of assuming fixed dates. * Assert objects and webhooks: status, amount, currency, metadata, `sandbox_id`, and `test_clock_id`. * Treat webhooks as retryable and idempotent. * Use fixtures only for consumer tests; use real flows for behavior tests. * End each scenario by canceling the subscription and archive the Sandbox when its data is no longer needed. ## Archiving and retention Archiving a Sandbox revokes its keys, blocks new requests, and marks its clocks as deleted. It does not purge customers, subscriptions, invoices, activities, or historical webhooks; those records are retained for audit and debugging. Create a new Sandbox for a clean suite and never use real customer data as fixtures. ## Differences from Stripe Test Clocks Recurrente follows customer attachment, asynchronous forward-only advances, and the two-interval maximum. It intentionally differs because: * clock state is observed by polling; separate clock-status webhooks are not published; * a clock with subscriptions cannot be deleted; * deleting or archiving retains the historical graph instead of deleting it; * clocks are not automatically deleted after a fixed retention period; * objects and event names follow Recurrente's public contract. See Stripe's documentation on [subscription simulations](https://docs.stripe.com/billing/testing/test-clocks/simulate-subscriptions) and [advanced Test Clock API usage](https://docs.stripe.com/billing/testing/test-clocks/api-advanced-usage) to compare the contracts. See the endpoint reference and Sandbox parity matrix to learn which operations are native and which persist a local simulation. > Test subscriptions, renewals, and webhooks without real money