> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.paymentkit.com/guides/integration/simulations-test-clocks/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server. # Simulations (Test Clocks) # Overview Simulations provide a deterministic testing environment for subscription lifecycle flows. Unlike production subscriptions that run asynchronously, simulations execute all evaluations synchronously within a single database transaction, allowing you to fast-forward time and observe exact state changes. **Key features:** * **Virtual time control**: Advance time to specific moments without waiting * **Deterministic execution**: Same input state always produces the same effects * **Full transaction isolation**: All changes happen atomically — simulations never affect production data * **Event timeline**: Complete audit trail of every state change and evaluation * **Synchronous evaluation**: No background workflows — results are immediate and predictable ## Use cases | Scenario | How simulations help | | ------------------------------- | -------------------------------------------------------------------------------------------------- | | **Testing renewal flows** | Jump to the next billing date and verify invoice creation, payment attempts, and state transitions | | **Dunning behavior validation** | Advance through retry schedules to verify subscription transitions and email notifications | | **Trial expiration** | Fast-forward to trial end date and confirm payment collection or cancellation | | **Contract renewals** | Validate fixed-term subscription behavior when `total_billing_cycles` completes | | **Scheduled changes** | Test pending plan changes by advancing to their scheduled execution date | --- # Create a simulation Create a test clock with a virtual timeline for deterministic subscription testing. #### Request ```bash curl -X POST https://app.paymentkit.com/api/simulations \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "account_external_id": "acc_test_abc123", "start_time": "2024-01-01T00:00:00Z", "description": "Test subscription renewal flow" }' ``` #### Response ```json { "external_id": "sim_test_xyz789", "account_external_id": "acc_test_abc123", "virtual_clock": "2024-01-01T00:00:00Z", "start_time": "2024-01-01T00:00:00Z", "status": "active", "description": "Test subscription renewal flow" } ``` **Request parameters:** | Field | Type | Required | Description | | --------------------- | -------- | -------- | --------------------------------------------- | | `account_external_id` | string | Yes | Account this simulation belongs to | | `start_time` | datetime | Yes | When the simulation's virtual timeline begins | | `description` | string | No | Human-readable description for tracking | **Important:** All entities created within a simulation (subscriptions, invoices, payments) are automatically linked to the simulation and isolated from production data via the `simulation_id` foreign key. --- # Create test subscriptions Once you have a simulation, create subscriptions that will be evaluated on the virtual timeline. ```bash curl -X POST https://app.paymentkit.com/api/{account_id}/subscriptions \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "customer_id": "cus_test_abc123", "items": [ { "price_id": "price_test_monthly", "quantity": 1 } ], "simulation_id": "sim_test_xyz789", "start_date": "2024-01-01T00:00:00Z", "trial_end": "2024-01-15T00:00:00Z" }' ``` **Key points:** * Pass the `simulation_id` to link the subscription to the simulation * The subscription's `start_date` should align with or follow the simulation's `start_time` * All subscription evaluations will use the simulation's `virtual_clock` instead of real time --- # Advance simulation time Move the virtual clock forward to trigger subscription evaluations and state changes. #### Request ```bash curl -X POST https://app.paymentkit.com/api/simulations/sim_test_xyz789/advance \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{ "target_time": "2024-02-01T00:00:00Z" }' ``` #### Response ```json { "simulation_external_id": "sim_test_xyz789", "new_virtual_clock": "2024-02-01T00:00:00Z", "subscriptions_evaluated": 3, "events_recorded": 12 } ``` ## How time advancement works When you advance time, PaymentKit: 1. **Validates the target**: `target_time` must be in the future relative to the current `virtual_clock` 2. **Updates the virtual clock**: Sets `virtual_clock` to `target_time` 3. **Evaluates subscriptions**: Finds all subscriptions linked to this simulation and evaluates each one using the new `virtual_clock` 4. **Executes effects synchronously**: All effects (invoice creation, payment attempts, state transitions) run immediately in a single transaction 5. **Records events**: Logs each evaluation and effect to the simulation's event timeline 6. **Returns results**: Reports how many subscriptions were evaluated and events recorded **Important invariants:** * **Time causality**: Virtual clock only moves forward (never backward) * **Determinism**: Same subscription state + same target time → same effects * **Atomicity**: All evaluations complete or the entire operation rolls back * **Synchronous execution**: All subscription evaluations and effects execute immediately within a single transaction --- # Retrieve simulation details Fetch the current state of a simulation. #### Request ```bash curl -X GET https://app.paymentkit.com/api/simulations/sim_test_xyz789 \ -H "Authorization: Bearer sk_test_..." ``` #### Response ```json { "external_id": "sim_test_xyz789", "account_external_id": "acc_test_abc123", "virtual_clock": "2024-02-01T00:00:00Z", "start_time": "2024-01-01T00:00:00Z", "status": "active", "description": "Test subscription renewal flow" } ``` --- # View simulation timeline Retrieve a chronological log of all events that occurred during time advancement. #### Request ```bash curl -X GET https://app.paymentkit.com/api/simulations/sim_test_xyz789/events \ -H "Authorization: Bearer sk_test_..." ``` #### Response ```json { "simulation_external_id": "sim_test_xyz789", "events": [ { "event_type": "subscription_evaluated", "virtual_timestamp": "2024-01-15T00:00:00Z", "entity_type": "subscription", "entity_id": "sub_test_abc123", "payload": { "current_state": "trialing", "next_action": "end_trial", "scheduled_for": "2024-01-15T00:00:00Z" } }, { "event_type": "invoice_created", "virtual_timestamp": "2024-01-15T00:00:01Z", "entity_type": "invoice", "entity_id": "inv_test_xyz456", "payload": { "amount_due": 1999, "currency": "usd", "subscription_id": "sub_test_abc123" } }, { "event_type": "payment_attempted", "virtual_timestamp": "2024-01-15T00:00:02Z", "entity_type": "payment", "entity_id": "pay_test_def789", "payload": { "status": "succeeded", "amount": 1999, "currency": "usd" } }, { "event_type": "subscription_transitioned", "virtual_timestamp": "2024-01-15T00:00:03Z", "entity_type": "subscription", "entity_id": "sub_test_abc123", "payload": { "from_state": "trialing", "to_state": "active" } } ] } ``` **Event types:** | Event Type | Description | | --------------------------- | -------------------------------------------------------- | | `subscription_evaluated` | Subscription lifecycle engine evaluated the subscription | | `invoice_created` | Renewal or prorated invoice was created | | `invoice_finalized` | Draft invoice transitioned to open | | `payment_attempted` | Payment collection was attempted | | `payment_succeeded` | Payment completed successfully | | `payment_failed` | Payment failed (triggers dunning) | | `subscription_transitioned` | Subscription moved to a new state | | `dunning_retry_scheduled` | Next payment retry was scheduled | --- # Inspect simulation objects After advancing time, inspect the customers and invoices created within a simulation. These endpoints return only objects linked to the simulation—they're isolated from production data. ## List simulation customers Retrieve customers created inside the simulation, each with their simulated subscriptions attached. #### Request ```bash curl -X GET "https://app.paymentkit.com/api/simulations/sim_test_xyz789/customers?limit=10" \ -H "Authorization: Bearer sk_test_..." ``` #### Response ```json { "items": [ { "customer": { "id": "cus_test_abc123", "email": "alice@example.com", "first_name": "Alice", "last_name": "Smith", "created_at": "2024-01-01T00:00:00Z" }, "subscriptions": [ { "id": "sub_test_xyz789", "status": "active", "current_period_start": "2024-01-15T00:00:00Z", "current_period_end": "2024-02-15T00:00:00Z", "items": [ { "price_id": "price_test_monthly", "quantity": 1 } ] } ] } ], "total": 1, "limit": 10, "offset": 0 } ``` **Query parameters:** | Parameter | Type | Description | | ----------------------- | ------- | ---------------------------------------------------------------- | | `limit` | integer | Max results per page (default: 50, max: 250) | | `offset` | integer | Number of results to skip | | `filter[email]` | string | Filter by customer email | | `filter[first_name]` | string | Filter by first name | | `filter[last_name]` | string | Filter by last name | | `filter[business_name]` | string | Filter by business name | | `sort` | string | Sort by `created_at` or `email` (prefix with `-` for descending) | | `search` | string | Search across email, first\_name, last\_name, business\_name | ## List simulation invoices Retrieve invoices created inside the simulation, with full invoice details and line items. #### Request ```bash curl -X GET "https://app.paymentkit.com/api/simulations/sim_test_xyz789/invoices?limit=10" \ -H "Authorization: Bearer sk_test_..." ``` #### Response ```json { "items": [ { "id": "inv_test_xyz456", "status": "paid", "customer_id": "cus_test_abc123", "customer_email": "alice@example.com", "subscription_id": "sub_test_xyz789", "billing_reason": "subscription_cycle", "currency": "usd", "subtotal": 1999, "total": 1999, "amount_due": 0, "amount_paid": 1999, "period_start": "2024-01-15T00:00:00Z", "period_end": "2024-02-15T00:00:00Z", "created_at": "2024-01-15T00:00:01Z" } ], "total": 1, "limit": 10, "offset": 0 } ``` **Query parameters:** | Parameter | Type | Description | | ------------------------- | ------- | -------------------------------------------------------------------------------------- | | `limit` | integer | Max results per page (default: 50, max: 250) | | `offset` | integer | Number of results to skip | | `filter[status]` | string | Filter by invoice status (`draft`, `open`, `paid`, `void`, `uncollectible`) | | `filter[billing_reason]` | string | Filter by billing reason (`subscription_cycle`, `subscription_create`, etc.) | | `filter[subscription_id]` | string | Filter by subscription ID | | `filter[customer_id]` | string | Filter by customer ID | | `sort` | string | Sort by `created_at`, `period_start`, or `period_end` (prefix with `-` for descending) | --- # Testing patterns ## Test case: Trial expiration with successful payment ```bash # 1. Create simulation starting Jan 1 curl -X POST https://app.paymentkit.com/api/simulations \ -d '{"account_external_id": "acc_test_123", "start_time": "2024-01-01T00:00:00Z"}' # Response: sim_test_abc # 2. Create subscription with 14-day trial curl -X POST https://app.paymentkit.com/api/acc_test_123/subscriptions \ -d '{ "customer_id": "cus_test_456", "items": [{"price_id": "price_monthly", "quantity": 1}], "simulation_id": "sim_test_abc", "trial_end": "2024-01-15T00:00:00Z" }' # 3. Advance to trial end date curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-01-15T00:00:00Z"}' # 4. Verify subscription transitioned to active curl -X GET https://app.paymentkit.com/api/acc_test_123/subscriptions/sub_test_xyz # Expected: status = "active", trial_end reached, first invoice paid ``` ## Test case: Dunning retry schedule ```bash # 1. Create simulation and subscription (same as above) # 2. Advance to renewal date with payment failure curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-02-01T00:00:00Z"}' # 3. Verify subscription moved to past_due curl -X GET https://app.paymentkit.com/api/acc_test_123/subscriptions/sub_test_xyz # Expected: status = "past_due" # 4. Advance to first retry (1 hour later) curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-02-01T01:00:00Z"}' # 5. Check event timeline for retry attempt curl -X GET https://app.paymentkit.com/api/simulations/sim_test_abc/events # 6. Advance to second retry (4 days later) curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-02-05T00:00:00Z"}' ``` ## Test case: Fixed-term contract completion ```bash # 1. Create subscription with 3 billing cycles, auto-renew disabled curl -X POST https://app.paymentkit.com/api/acc_test_123/subscriptions \ -d '{ "customer_id": "cus_test_456", "items": [{"price_id": "price_monthly", "quantity": 1}], "simulation_id": "sim_test_abc", "total_billing_cycles": 3, "contract_auto_renew": false }' # 2. Advance through 3 billing cycles curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-02-01T00:00:00Z"}' # Month 1 renewal curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-03-01T00:00:00Z"}' # Month 2 renewal curl -X POST https://app.paymentkit.com/api/simulations/sim_test_abc/advance \ -d '{"target_time": "2024-04-01T00:00:00Z"}' # Month 3 renewal (final) # 3. Verify subscription ended after 3 cycles curl -X GET https://app.paymentkit.com/api/acc_test_123/subscriptions/sub_test_xyz # Expected: remaining_billing_cycles = 0, billing stopped ``` --- # Limitations and differences from production | Aspect | Production | Simulation | | ----------------------- | ---------------------------------------------- | ------------------------------------------------------- | | **Execution** | Asynchronous background processing via Restate | Synchronous, immediate execution within transaction | | **Retries** | Automatic retries with exponential backoff | Deterministic, no retries | | **Timing** | Real-time delays and schedules | Virtual clock (instant time jumps) | | **Rollback** | Changes are permanent once committed | Full rollback on error (single transaction) | | **Domain events** | Published to event stream in real-time | Published to event stream with virtual timestamps | | **Webhooks** | Delivered to customer endpoints in real-time | Delivered to customer endpoints with simulation context | | **Email notifications** | Sent to customers via Restate workflows | Sent to customers via same email delivery system | **Important:** Simulations achieve 100% behavior parity with production subscription logic, including event publishing, webhook delivery, and email notifications. The key difference is execution mode: production uses async Restate workflows while simulations execute synchronously within a single transaction. --- # Best practices #### Use simulations for integration tests Create a simulation before each test, advance time to trigger the scenario, then assert on the resulting subscription/invoice state. Simulations are isolated and can be safely run in parallel. #### Always verify via API, not just events The event timeline shows what happened, but always fetch the subscription/invoice state via the API to confirm the final result matches expectations. #### Test edge cases with multiple subscriptions Create multiple subscriptions in the same simulation with different billing dates, trial periods, and dunning settings. Advance time once and verify all subscriptions evaluated correctly. #### Clean up after testing Simulations and their linked entities are isolated from production, but they consume database space. Delete test simulations after validation is complete (future API will support simulation deletion). #### Use descriptive descriptions The `description` field helps track what each simulation is testing. Use names like "Dunning exhaustion test" or "Trial → Active transition with proration" to make timelines easier to debug. > **Simulations are for testing only** > > Simulations trigger domain events, webhooks, and email notifications just like production, but execute synchronously in controlled virtual time. Never use simulations for production subscriptions — they exist solely for deterministic testing environments where you can fast-forward time and verify exact state transitions. > Test subscription lifecycle behavior deterministically by controlling time advancement with virtual test clocks.