# Test subscriptions

> A synthetic subscription in Stripe test mode — sign-up with a test card, your real emails at Checkbox's inbox, a test clock through trial end and renewal, and online cancellation — so every lifecycle check is observed, like a CMS preview.

A test run is a preview of your whole subscription lifecycle before your customers see it. Checkbox subscribes a test
customer in your billing platform's **test mode**, receives your emails at its own inbox, fast-forwards a Stripe test
clock through the trial end and the first renewal, cancels online, and reports every step with evidence, check results
and fixes your agent can apply. Nothing is charged and nobody has to log in.

## 1. Give Checkbox a Stripe test key {#credentials}

Create a **restricted key in test mode** (Stripe Dashboard → Developers → API keys) with:

| Permission | Access | Used for |
|---|---|---|
| Customers | Write | The test subscriber (attached to the run's test clock) |
| Checkout Sessions | Write | The mirrored test-mode Checkout Session |
| Subscriptions | Write | Reading trial, period and cancellation; creating the subscription if the hosted page cannot be completed |
| Test clocks | Write | Creating, advancing and deleting the run's clock |
| Customer portal | Write | A portal session for the online cancellation |
| Prices, Products | Read | Resolving the mirrored prices |
| Billing portal configuration | Read | Comparing the test portal with live |

Store it for the site. Live keys are refused (`test_key_not_test_mode`); the key is verified with Stripe, kept encrypted
and never returned (only a hint such as `rk_test_…a1b2`):

```bash
curl https://app.checkbox.site/v1/sites/site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b/test_credentials \
  -u "$CHECKBOX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{ "type": "stripe_test", "secret_key": "rk_test_..." }'
```

`checkbox init` and `setup_checkbox` do this for you when the human ticks "use my Stripe test key" on the approval page.

## 2. Start a run {#start}

```bash
curl -X POST https://app.checkbox.site/v1/sites/site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b/test_runs \
  -u "$CHECKBOX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{}'
```

Checkbox copies your checkout into a test-mode Checkout Session. The source is, in order: a config you registered with
[`createStripeCheckoutConfig`](https://checkbox.site/docs/api/test-runs#createStripeCheckoutConfig) (exactly what your code passes to
`stripe.checkout.sessions.create`), a test price you pass, or your latest live Checkout Session read through the
read-only connection. It also tells you whether the copy matches live (`stripe.matches_live`).

## 3. Follow it {#follow}

A run takes about 5 to 20 minutes, mostly waiting for emails. Poll `GET /v1/test_runs/{run}` every 15–30 seconds, or
subscribe to the `test_run.completed` / `test_run.failed` [webhooks](https://checkbox.site/docs/webhooks). List a site's runs:

```curl
curl -G https://app.checkbox.site/v1/sites/site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b/test_runs \
  -u "$CHECKBOX_SECRET_KEY:" \
  -d limit=3
```
```node
import Checkbox from '@checkbox-site/cli';

const checkbox = new Checkbox(process.env.CHECKBOX_SECRET_KEY);

const runs = await checkbox.testRuns.list('site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b', { limit: 3 });
for (const run of runs.data) console.log(run.id, run.status, run.next_action?.type);
```
```python
import os
from checkbox import Checkbox

checkbox = Checkbox(os.environ["CHECKBOX_SECRET_KEY"])

runs = checkbox.test_runs.list("site_3f9a1c2e7b4d4e0f9a8b7c6d5e4f3a2b", limit=3)
for run in runs.data:
    print(run["id"], run["status"], (run.get("next_action") or {}).get("type"))
```

## What a run does {#steps}

| Step | What happens | Evidence for |
|---|---|---|
| `credentials` | The test key works | |
| `checkout_config` | Mirror the checkout and compare it with live | |
| `checkout_page` | The synthetic shopper scans the hosted test-mode page | `signup.*` |
| `subscribe` | Complete the page with test card 4242; if the page cannot be completed, the subscription is created through the API and flagged | |
| `acknowledgment_email` | Wait for your email at the run's inbox | `confirmation.*` |
| `trial_reminder` | Move the test clock to every notice-window edge before the trial ends (from the legal catalog), collect mail after each stop | `reminder.trial_ending` |
| `renewal` | The same around the first renewal | `reminder.annual_renewal`, `reminder.charge_notice` |
| `cancel_online` | Open the customer portal (or your own cancel page) and cancel with the shopper | `cancel.*` |
| `cancellation_email` | Wait for the confirmation | `cancel.confirmation` |
| `cleanup` | Delete the test clock (Stripe deletes its customer and subscription with it) | |

**Exact timing.** The clock stops on window edges, so an email that arrives between two stops was sent inside, before
or after each state's window — `timing` is `in_window`, `too_early` or `too_late`, exactly, even though the run takes
minutes. The run is judged in test-clock time: a 35-day trial is evaluated as 35 days.

### The acknowledgment email {#acknowledgment}

Stripe sends no customer emails in test mode, so a run only sees emails **your** code or email provider sends. If your
acknowledgment comes from Stripe's own receipts, the run reports it as a warning with a fix — send the acknowledgment
from your `checkout.session.completed` webhook handler (or turn on [Checkbox Notices](https://checkbox.site/docs/guides/emails-and-notices#notices)) —
and it is not counted against your site. Each step's `fix` has `summary`, ordered `steps`, `where`, and the
`operation_id` to call to try again.

## What counts for monitoring {#provenance}

Everything a run observes is labelled `provenance: "test_run"`. Your emails always count (they are your real emails);
the hosted checkout counts only when the mirrored config matches live; the cancellation flow counts when it is your
own cancel page or a test portal configured like live. Missing evidence that is only missing because of test mode is
never recorded as a failure of your site.

Each response's `next_action` (`wait`, `fix_and_retry`, `apply_fixes`, `review_warnings`, `none`) tells your agent what
to do next. After applying fixes, start another run to confirm them.
