Simulations (Test Clocks)

Test subscription lifecycle behavior deterministically by controlling time advancement with virtual test clocks.
View as Markdown

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

ScenarioHow simulations help
Testing renewal flowsJump to the next billing date and verify invoice creation, payment attempts, and state transitions
Dunning behavior validationAdvance through retry schedules to verify subscription transitions and email notifications
Trial expirationFast-forward to trial end date and confirm payment collection or cancellation
Contract renewalsValidate fixed-term subscription behavior when total_billing_cycles completes
Scheduled changesTest 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.

$curl -X POST https://api.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"
>}'

Request parameters:

FieldTypeRequiredDescription
account_external_idstringYesAccount this simulation belongs to
start_timedatetimeYesWhen the simulation’s virtual timeline begins
descriptionstringNoHuman-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.

$curl -X POST https://api.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.

$curl -X POST https://api.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"
>}'

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.

$curl -X GET https://api.paymentkit.com/api/simulations/sim_test_xyz789 \
>-H "Authorization: Bearer sk_test_..."

View simulation timeline

Retrieve a chronological log of all events that occurred during time advancement.

$curl -X GET https://api.paymentkit.com/api/simulations/sim_test_xyz789/events \
>-H "Authorization: Bearer sk_test_..."

Event types:

Event TypeDescription
subscription_evaluatedSubscription lifecycle engine evaluated the subscription
invoice_createdRenewal or prorated invoice was created
invoice_finalizedDraft invoice transitioned to open
payment_attemptedPayment collection was attempted
payment_succeededPayment completed successfully
payment_failedPayment failed (triggers dunning)
subscription_transitionedSubscription moved to a new state
dunning_retry_scheduledNext payment retry was scheduled

Testing patterns

Test case: Trial expiration with successful payment

$# 1. Create simulation starting Jan 1
$curl -X POST https://api.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://api.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://api.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://api.paymentkit.com/api/acc_test_123/subscriptions/sub_test_xyz
$
$# Expected: status = "active", trial_end reached, first invoice paid

Test case: Dunning retry schedule

$# 1. Create simulation and subscription (same as above)
$
$# 2. Advance to renewal date with payment failure
$curl -X POST https://api.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://api.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://api.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://api.paymentkit.com/api/simulations/sim_test_abc/events
$
$# 6. Advance to second retry (4 days later)
$curl -X POST https://api.paymentkit.com/api/simulations/sim_test_abc/advance \
> -d '{"target_time": "2024-02-05T00:00:00Z"}'

Test case: Fixed-term contract completion

$# 1. Create subscription with 3 billing cycles, auto-renew disabled
$curl -X POST https://api.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://api.paymentkit.com/api/simulations/sim_test_abc/advance \
> -d '{"target_time": "2024-02-01T00:00:00Z"}' # Month 1 renewal
$
$curl -X POST https://api.paymentkit.com/api/simulations/sim_test_abc/advance \
> -d '{"target_time": "2024-03-01T00:00:00Z"}' # Month 2 renewal
$
$curl -X POST https://api.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://api.paymentkit.com/api/acc_test_123/subscriptions/sub_test_xyz
$# Expected: remaining_billing_cycles = 0, billing stopped

Limitations and differences from production

AspectProductionSimulation
ExecutionAsynchronous background processing via RestateSynchronous, immediate execution within transaction
RetriesAutomatic retries with exponential backoffDeterministic, no retries
TimingReal-time delays and schedulesVirtual clock (instant time jumps)
RollbackChanges are permanent once committedFull rollback on error (single transaction)
Domain eventsPublished to event stream in real-timePublished to event stream with virtual timestamps
WebhooksDelivered to customer endpoints in real-timeDelivered to customer endpoints with simulation context
Email notificationsSent to customers via Restate workflowsSent 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

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.

The event timeline shows what happened, but always fetch the subscription/invoice state via the API to confirm the final result matches expectations.

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.

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).

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.