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

# Test in the sandbox

Every account has a sandbox. Sandbox keys (`rl_sk_test_...`) see only sandbox objects, nothing in the sandbox moves money, and a payment provider connects at once. In the sandbox `stripe` is Runledger's own Stripe sandbox, shared by every account, or a fake when the deployment has no Stripe key, so test payments don't appear in your own Stripe dashboard. The sandbox is where a coding agent is allowed to write: with the right [scopes](/api-reference/access-and-audit#keys-and-scopes) it applies [configuration](/api-reference/how-configuration-works) directly, and each write is recorded as made by that agent.

## Test clocks

Settlement runs on clocks: confirmation windows, reversal horizons, dispute deadlines, hold and grant expiry, billing periods and invoice finalization. A test clock lets you move time forward and watch all of it happen.

1. Create a clock with `POST /v1/test_clocks` and a `frozen_time`.
2. Create customers with `test_clock` set to its ID. Everything time-based for those customers follows the clock.
3. Advance it with `POST /v1/test_clocks/{id}/advance`. Clocks only move forward, up to 90 days per call. Everything that falls due runs in order, the clock is `advancing` until it finishes, and then `test_clock.ready` fires.
4. Delete it when you're done, which deletes its customers too. Clocks are deleted automatically after 30 days.

Windows, spend limit periods, credit expiry and auto top-up retries all follow the clock too, so a week of [usage windows](/guides/usage-windows) runs in one call.

Use a separate clock for each scenario, so test runs don't interfere with each other. Each account has one sandbox, so a separate sandbox for continuous integration is a second account.

## Start clean and check your work

* `POST /v1/sandbox/reset` deletes the sandbox's customers, events, test clocks and request logs so a test run can start from nothing. It works only with a sandbox key. The dashboard asks a person to type a confirmation first; the API takes `confirm: true`.
* `POST /v1/integration_checks` reviews the sandbox's recent traffic against the going-live checklist and returns a pass, warn, fail or unknown result for each item, with the evidence and the fix. It is the same result as the dashboard's Get started page and the MCP `check_integration` tool.

## Simulate the payment provider

| Endpoint                                                | Simulates                                                                                    |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `POST /v1/test_helpers/checkout_sessions/{id}/complete` | The customer paying, or failing to pay, on the hosted page                                   |
| `POST /v1/test_helpers/invoices/{id}/simulate_payment`  | The provider collecting, or failing to collect, an invoice                                   |
| `POST /v1/test_helpers/auto_top_ups/{id}/resolve`       | An [auto top-up](/guides/auto-top-up-and-alerts) purchase succeeding, or its payment failing |

All of them send the same webhooks as the real thing. They work only with sandbox keys; a live key gets `403`.

## Two scenarios worth running

### Two agents, one balance

Give a prepaid customer 100 credits, then fire two `check()` calls at once, each holding 60. One is allowed and one is denied with `insufficient_balance`. Track the allowed one and the balance ends at 40. [Checks and holds](/concepts/checks-and-holds#two-agents-one-balance) explains why.

### The unhappy customer

1. Set up the `chat_resolution` definition and postpaid plan from the [quickstart](/quickstart), on a clock. The definition's `dispute_settings.decision_window` of `P7D` sets the deadline below.
2. Track `resolution.chat` with `reference: "conv_9001"`. The charge is `pending`.
3. Track `csat.submitted` with `reference: "conv_9001"` and `properties: {"score": 1}`. The `low_csat` rule opens a dispute; the charge is `disputed` and won't confirm.
4. Advance the clock six days. `dispute.decision_due` fires.
5. Either resolve the dispute with `uphold`, and the charge confirms, or let the deadline pass, and the default `reverse` applies: the dispute shows `decided_by: "default"` and the charge is `reversed`. Resolving a dispute needs `disputes:resolve`, which the key from **Get started** doesn't carry, so use a secret key from **API keys**.
6. Advance to the end of the month. The finalized invoice has no line for the reversed charge.

## What the sandbox doesn't simulate

* Payment provider behavior beyond success and failure, such as card authentication steps and bank-specific declines.
* Bank debits and PayTo, which Runledger doesn't support.
* Load. Sandbox rate limits are lower, so don't load test against it.

Each account has one sandbox, which comes with the account; a second sandbox is a second account.

## See also

* [Quickstart](/quickstart) runs a charge through a test clock.
* [Going live](/going-live/backtest-and-shadow#backtest-on-your-history) uses test clocks for a backtest.